diff --git a/.github/workflows/run_opa_tests.yaml b/.github/workflows/run_opa_tests.yaml new file mode 100644 index 0000000000..ca59d09cc4 --- /dev/null +++ b/.github/workflows/run_opa_tests.yaml @@ -0,0 +1,32 @@ +name: Run OPA Tests +on: + # Run tests on each commit, newly opened/reopened PR, and + # PR review submission (e.g. approval) + push: + paths: + - "**.rego" + pull_request: + types: [opened, reopened] + branches: + - "main" + paths: + - "**.rego" + pull_request_review: + types: [submitted] + branches: + - "main" + +jobs: + Run-OPA-Tests: + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v2 + + - name: Setup OPA + uses: open-policy-agent/setup-opa@v1 + with: + version: latest + + - name: Run OPA Tests + run: opa test Rego/*.rego Testing/**/*.rego -v diff --git a/.github/workflows/run_psscriptanalyzer.yaml b/.github/workflows/run_psscriptanalyzer.yaml new file mode 100644 index 0000000000..3bb70f2b40 --- /dev/null +++ b/.github/workflows/run_psscriptanalyzer.yaml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..7bcac1dd16 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# dependencies + +# testing + +# production + +# msc +*.xml +*.cer +/M365/*.xml +/PowerShell/output +/PowerShell/example +/PowerShell/M365Baseline* +/output +/example +/M365Baseline* +/Reports* \ No newline at end of file diff --git a/AllowBasicAuthentication.ps1 b/AllowBasicAuthentication.ps1 new file mode 100644 index 0000000000..3eadc07a22 --- /dev/null +++ b/AllowBasicAuthentication.ps1 @@ -0,0 +1,29 @@ +#Requires -RunAsAdministrator + +# Run this script to enable basic authentication on your local desktop if you get an error when connecting to Exchange Online. +# See README file Troubleshooting section for details. +# +# This script requires administrative privileges on your local desktop and updates a registry key. +# +$regPath = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WinRM\Client' +$regKey = 'AllowBasic' + +if (Test-Path -LiteralPath $regPath){ + try { + $allowBasic = Get-ItemPropertyValue -Path $regPath -Name $regKey -ErrorAction Stop + } + catch [System.Management.Automation.PSArgumentException]{ + Write-Error -Message "Key, $regKey, was not found" + } + catch{ + Write-Error -Message "Unexpected error occured attempting to get registry key, $regKey." + } + + if ($allowBasic -ne '1'){ + Set-ItemProperty -Path $regPath -Name $regKey -Type DWord -Value '1' + } +} +else { + Write-Error -Message "Registry path not found: $regPath" +} + diff --git a/CONTENTSTYLEGUIDE.md b/CONTENTSTYLEGUIDE.md new file mode 100644 index 0000000000..fa8702fc4f --- /dev/null +++ b/CONTENTSTYLEGUIDE.md @@ -0,0 +1,361 @@ +# Content style guide for SCuBA + +Welcome to the content style guide for ScubaGear + +These guidelines are specific to style rules for PowerShell and OPA Rego code. For general style questions or guidance on topics not covered here, ask or go with best guess and bring up at a later meeting. + +Use menu icon on the top left corner of this document to get to a specific section of this guide quickly. + +## The SCuBA approach to style + +- Our style guide aims for simplicity. Guidelines should be easy to apply to a range of scenarios. +- Decisions aren’t about what’s right or wrong according to the rules, but about what’s best practice and improves readability. We're flexible and open to change while maintaining consistency. +- When making a style or structure decision, we consider the readability, maintainability and ability for consitancy in a range of situations. +- When a question specific to help documentation isn’t covered by the style guide, we think it through using these principles, then make a decision and bring it up in the next meeting for deliberation. + +## OPA Rego + +Because there isn't a standard style guide for the Rego language, we are creating one from scratch. For consistency, we will be using many of the same style rules as PowerShell. There are also a few best practice rules that this program will follow. These best practices were deliberated on and chosen to enhance readability. We recognize that the code is in a constant state of improvement, so the best practices are subject to change. + +### Test Cases + +Test names will use the syntax `test_mainVar_In/correct_*V#` to support brevity in naming that highlights the primary variable being tested. Furthermore, for tests with more than one version, the first test will also include a version as `_V1`. Consistent use of a version number promotes clarity and signals the presence of multiple test versions to reviewers. Version numbers are not used if there is only a single test of a given variable and type (Correct/Incorrect) + +``` +test_ExampleVar_Correct_V1 if { + ControlNumber := "Example 2.1" + Requirement := "Example Policy Bullet Point" + + Output := tests with input as { + "example_policies" : [ + { + "Example3" : "ExampleString", + "Example2" : false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Example output" +} + +test_ExampleVar_Correct_V2 if { + ... +} + +test_ExampleVar_Incorrect if { + ControlNumber := "Example 2.1" + Requirement := "Example Policy Bullet Point" + + Output := tests with input as { + "example_policies" : [ + { + "Example3" : "ExampleString", + "Example2" : true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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[{ + "Requirement" : "The matching policy bulletpoint from the baseline document goes here", + "Control" : "Example 2.1", + "Criticality" : "Shall/Not-Implemented", + "Commandlet" : "", + "ActualValue" : [], + "ReportDetails" : "Currently cannot be checked automatically. See (Product) Secure Configuration Baseline policy 2.# for instructions on manual check", + "RequirementMet" : false +}] { + true +} +``` + +``` +# At this time we are unable to test for X because of Y +tests[{ + "Requirement" : "The matching policy bulletpoint from the baseline document goes here", + "Control" : "Example 2.1", + "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 { + ControlNumber := "Example 2.1" + Requirement := "Example Policy Bullet Point" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See (Product) Secure Configuration Baseline policy 2.# for instructions on manual check" +} +``` +``` +test_3rdParty_Correct if { + ControlNumber := "Example 2.1" + Requirement := "Example Policy Bullet Point" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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 { + ControlNumber := "Example 2.1" + Requirement := "Example Requirement String" + + Output := tests with input as { + "example_tag" : { + "ExampleVar" : false + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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[{ + "Requirement" : "Baseline String", + "Control" : "Example 2.2", + "Criticality" : "Should", + "Commandlet" : "Example-Command", + "ActualValue" : ExampleVar.ExampleSetting, + "ReportDetails" : ReportDetailsBoolean(Status), + "RequirementMet" : Status +}] { + ExampleVar := input.ExampleVar + Status := ExampleVar == 15 +} + +tests[{ + "Requirement" : "Baseline String", +... +``` + +2) Two blank lines between subsections + +``` +tests[{ + "Requirement" : "Baseline String", + "Control" : "Example 2.2", + "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: 2.1, 2.2, etc. + +``` +################ +# Baseline 2.1 # +################ +``` +2) Indicate the beginning of every policy bullet point. + +``` +# +# Baseline 2.#: Policy # +#-- +``` + +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[{ + "Requirement" : "Baseline String", + "Control" : "Example 2.1", + "Criticality" : "Should", + "Commandlet" : "Example-Command", + "ActualValue" : ExampleVar.ExampleSetting, + "ReportDetails" : ReportDetailsBoolean(Status), + "RequirementMet" : Status +}] { + ExampleVar := input.ExampleVar + Status := ExampleVar == true +} + +tests[{ + "Requirement" : "Baseline String", + "Control" : "Example 2.2", + "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[{ + "Requirement" : "Baseline String", + "Control" : "Example 2.1", + "Criticality" : "Should", + "Commandlet" : "Example-Command", + "ActualValue" : Status, + "ReportDetails" : ReportDetailsBoolean(Status), + "RequirementMet" : Status +}] { + ExampleVar := input.ExampleVar + Status := ExampleVar == true +} +``` + +#### Correct +``` +tests[{ + "Requirement" : "Baseline String", + "Control" : "Example 2.2", + "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) \ No newline at end of file diff --git a/PSScriptAnalyzerSettings.psd1 b/PSScriptAnalyzerSettings.psd1 new file mode 100644 index 0000000000..6786441d8a --- /dev/null +++ b/PSScriptAnalyzerSettings.psd1 @@ -0,0 +1,8 @@ +# PSScriptAnalyzerSettings.psd1 +@{ + Severity=@('Error','Warning','Information') + ExcludeRules=@( + 'PSUseSingularNouns', + 'PSUseShouldProcessForStateChangingFunctions', + 'PSUseOutputTypeCorrectly') +} diff --git a/PowerShell/ScubaGear/Modules/Connection/Connection.psm1 b/PowerShell/ScubaGear/Modules/Connection/Connection.psm1 new file mode 100644 index 0000000000..fcbfb58437 --- /dev/null +++ b/PowerShell/ScubaGear/Modules/Connection/Connection.psm1 @@ -0,0 +1,95 @@ +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 + #> + param ( + [Parameter(Mandatory=$true)] + [string[]] + $ProductNames, + + [string] + $Endpoint + ) + + # Prevent duplicate sign ins + $EXOAuthRequired = $true + $SPOAuthRequired = $true + $AADAuthRequired = $true + + $N = 0 + $Len = $ProductNames.Length + + foreach ($Product in $ProductNames) { + $N += 1 + $Percent = $N*100/$Len + Write-Progress -Activity "Authenticating to each service" -Status "Authenticating to $($Product); $($n) of $($Len) Products authenticated to." -PercentComplete $Percent + switch ($Product) { + {($_ -eq "exo") -or ($_ -eq "defender")} { + if ($EXOAuthRequired) { + Connect-ExchangeOnline -ShowBanner:$false | Out-Null + Write-Verbose "Defender will require a sign in every single run regardless of what the LogIn parameter is set" + $EXOAuthRequired = $false + } + } + "aad" { + Connect-MgGraph -Scopes User.Read.All, Policy.Read.All, Organization.Read.All, UserAuthenticationMethod.Read.All, RoleManagement.Read.Directory, GroupMember.Read.All, Policy.ReadWrite.AuthenticationMethod, Directory.Read.All -ErrorAction Stop | Out-Null + Select-MgProfile Beta | Out-Null + $AADAuthRequired = $false + } + "powerplatform"{ + if (!$Endpoint) { + Write-Output "Power Platform needs an endpoint please specify one as a script arg" + } + else { + Add-PowerAppsAccount -Endpoint $Endpoint | Out-Null + } + } + {($_ -eq "onedrive") -or ($_ -eq "sharepoint")} { + if ($AADAuthRequired) { + Connect-MgGraph | Out-Null + Select-MgProfile Beta | Out-Null + $AADAuthRequired = $false + } + if ($SPOAuthRequired) { + $InitialDomain = (Get-MgOrganization).VerifiedDomains | Where-Object {$_.isInitial} + $InitialDomainPrefix = $InitialDomain.Name.split(".")[0] + Connect-SPOService -Url "https://$($InitialDomainPrefix)-admin.sharepoint.com" | Out-Null + $SPOAuthRequired = $false + } + } + "teams" { + Connect-MicrosoftTeams | Out-Null + } + default { + Write-Error -Message "Invalid ProductName argument" + } + } + } + Write-Progress -Activity "Authenticating to each service" -Status "Ready" -Completed +} + +function Disconnect-Tenant { + <# + .Description + This function disconnects the various PowerShell module sessions from the + M365 Tenant. Useful to disconnect then connect to other M365 tenants + Currently Disconect-MgGraph is buggy and may not disconnect properly. + .Functionality + Public + #> + Disconnect-MicrosoftTeams # Teams + Disconnect-MgGraph # AAD + Disconnect-ExchangeOnline -Confirm:$false -InformationAction Ignore -ErrorAction SilentlyContinue | Out-Null # Exchange and Defender + Remove-PowerAppsAccount # Power Platform + Disconnect-SPOService # OneDrive and Sharepoint +} + +Export-ModuleMember -Function @( + 'Connect-Tenant', + 'Disconnect-Tenant' +) diff --git a/PowerShell/ScubaGear/Modules/CreateReport/BaselineTitles.json b/PowerShell/ScubaGear/Modules/CreateReport/BaselineTitles.json new file mode 100644 index 0000000000..e540ee5efd --- /dev/null +++ b/PowerShell/ScubaGear/Modules/CreateReport/BaselineTitles.json @@ -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" : "Multifactor Authentication SHALL be required for all users & Phishing-Resistant Authentication SHOULD be used"}, + {"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"} + ] +} \ No newline at end of file diff --git a/PowerShell/ScubaGear/Modules/CreateReport/CreateReport.psm1 b/PowerShell/ScubaGear/Modules/CreateReport/CreateReport.psm1 new file mode 100644 index 0000000000..9b4830398f --- /dev/null +++ b/PowerShell/ScubaGear/Modules/CreateReport/CreateReport.psm1 @@ -0,0 +1,114 @@ +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)] + [string] + $BaselineName, + + [Parameter(Mandatory=$true)] + [string] + $FullName, + + # The location to save the html report in. Defaults to current directory. + [Parameter(Mandatory=$true)] + [string] + $IndividualReportPath, + + # The location to save the html report in. Defaults to current directory. + [Parameter(Mandatory=$true)] + [string] + $OutPath +) + +$FileName = Join-Path -Path $PSScriptRoot -ChildPath "BaselineTitles.json" +$AllTitles = Get-Content $FileName | ConvertFrom-Json +$Titles = $AllTitles.$BaselineName + +$FileName = Join-Path -Path $OutPath -ChildPath "TestResults.json" +$TestResults = Get-Content $FileName | ConvertFrom-Json + +$FileName = Join-Path -Path $OutPath -ChildPath "ProviderSettingsExport.json" +$SettingsExport = Get-Content $FileName | ConvertFrom-Json + +$Fragments = @() + +$MetaData += [pscustomobject]@{ + "Tenant Name"= $SettingsExport.tenant_details.DisplayName; + "Report Date"=$SettingsExport.date; + "Baseline Version"=$SettingsExport.baseline_version; + "Module Version"=$SettingsExport.module_version +} + +$Fragments += $MetaData | ConvertTo-HTML -Fragment + +$ReportSummary = @{ + "Warnings" = 0; + "Failures" = 0; + "Passes" = 0; + "Manual" = 0; + "Date" = $SettingsExport.date; +} + +ForEach ($Title in $Titles) { + $Fragment = @() + ForEach ($test in $TestResults | Where-Object -Property Control -eq $Title.Number) { + if ($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 "

$Number $Name

" -Fragment +} + +$Title = "$($FullName) Baseline Report" +Add-Type -AssemblyName System.Web + +$ReporterPath = $PSScriptRoot +$ReportHTML = Get-Content $(Join-Path -Path $ReporterPath -ChildPath "ReportTemplate.html") +$ReportHTML = $ReportHTML.Replace("{TITLE}", $Title) + +$MainCSS = Get-Content $(Join-Path -Path $ReporterPath -ChildPath "main.css") +$ReportHTML = $ReportHTML.Replace("{MAIN_CSS}", "") + +$MainJS = Get-Content $(Join-Path -Path $ReporterPath -ChildPath "main.js") +$ReportHTML = $ReportHTML.Replace("{MAIN_JS}", "") + +$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' +) diff --git a/PowerShell/ScubaGear/Modules/CreateReport/ParentReportTemplate.html b/PowerShell/ScubaGear/Modules/CreateReport/ParentReportTemplate.html new file mode 100644 index 0000000000..cca2b36317 --- /dev/null +++ b/PowerShell/ScubaGear/Modules/CreateReport/ParentReportTemplate.html @@ -0,0 +1,23 @@ + + + + SCuBA M365 Security Baseline Conformance Reports + {MAIN_CSS} + {PARENT_CSS} + + +
+
+ Return to the report summary +

Secure Configuration Baselines

+
+

SCuBA M365 Security Baseline Conformance Reports

+

{TENANT_NAME} tenant

+ {TABLES} + +
+ + \ No newline at end of file diff --git a/PowerShell/ScubaGear/Modules/CreateReport/ParentStyle.css b/PowerShell/ScubaGear/Modules/CreateReport/ParentStyle.css new file mode 100644 index 0000000000..516437c0e4 --- /dev/null +++ b/PowerShell/ScubaGear/Modules/CreateReport/ParentStyle.css @@ -0,0 +1,47 @@ +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: #005288; + text-decoration: underline; +} + +a.individual_reports:visited { + font-family: Arial, Helvetica, sans-serif; + color: #85B065; +} + +a.individual_reports:hover { + font-family: Arial, Helvetica, sans-serif; + color: #85B065; + text-decoration: none; +} + +a.individual_reports:active { + font-family: Arial, Helvetica, sans-serif; + color: #85B065; + text-decoration: none; +} + +.failure { background-color: #deb8b8; } +.warning { background-color: #fff7d6; } +.pass { background-color: #d5ebd5; } +.manual { background-color: #ebebf2; } \ No newline at end of file diff --git a/PowerShell/ScubaGear/Modules/CreateReport/ReportTemplate.html b/PowerShell/ScubaGear/Modules/CreateReport/ReportTemplate.html new file mode 100644 index 0000000000..53777dd2b8 --- /dev/null +++ b/PowerShell/ScubaGear/Modules/CreateReport/ReportTemplate.html @@ -0,0 +1,18 @@ + + + + {TITLE} + {MAIN_CSS} + {MAIN_JS} + + +
+
+ Return to the report summary +

Secure Configuration Baselines

+
+

{TITLE}

+ {TABLES} +
+ + \ No newline at end of file diff --git a/PowerShell/ScubaGear/Modules/CreateReport/cisa_logo.png b/PowerShell/ScubaGear/Modules/CreateReport/cisa_logo.png new file mode 100644 index 0000000000..ee16d16a81 Binary files /dev/null and b/PowerShell/ScubaGear/Modules/CreateReport/cisa_logo.png differ diff --git a/PowerShell/ScubaGear/Modules/CreateReport/main.css b/PowerShell/ScubaGear/Modules/CreateReport/main.css new file mode 100644 index 0000000000..0d5b1bd673 --- /dev/null +++ b/PowerShell/ScubaGear/Modules/CreateReport/main.css @@ -0,0 +1,80 @@ +body { + background-color: #b9bec2; + -webkit-print-color-adjust:exact !important; + print-color-adjust:exact !important; +} + +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: #005288; +} + +header { + width: 1000px; + margin: auto; + border-bottom: 1px solid black; + margin-bottom: 50px; + display: flex; + justify-content: space-between; + align-items: end; + padding: 5px; +} +header h3 { + width: 150px; + text-align: right; + border-bottom: 5px solid rgba(0, 0, 0, 0); + color: #005288; +} +header a { + text-decoration: none; +} +header h3:hover { + border-bottom: 5px solid #005288; +} + +td { + padding: 4px; + margin: 0px; + overflow-wrap: break-word; +} + +table, th, td { + border: 1px solid; +} + +main { + background-color: white; + width: 1100px; + margin: auto; + position: relative; + padding-bottom: 50px; +} + +h1 { + text-align: center; + font-family: Arial, Helvetica, sans-serif; + color: #005288; + margin-top: 10px; + margin-bottom: 20px; +} + +h2 { + text-align: center; + font-family: Arial, Helvetica, sans-serif; + color: #005288; + font-size: 16px; + margin-top: 50px; +} + +img { + width: 100px; +} \ No newline at end of file diff --git a/PowerShell/ScubaGear/Modules/CreateReport/main.js b/PowerShell/ScubaGear/Modules/CreateReport/main.js new file mode 100644 index 0000000000..7212b7b8eb --- /dev/null +++ b/PowerShell/ScubaGear/Modules/CreateReport/main.js @@ -0,0 +1,24 @@ +function colorRows() { + let rows = document.querySelectorAll('tr'); + for (let i = 0; i < rows.length; i++) { + if (rows[i].children[1].innerHTML == "Fail") { + rows[i].style.background = "#deb8b8"; + } + else if (rows[i].children[1].innerHTML == "Warning") { + rows[i].style.background = "#fff7d6"; + } + else if (rows[i].children[1].innerHTML == "Pass") { + rows[i].style.background = "#d5ebd5"; + } + else if (rows[i].children[2].innerHTML.includes("Not-Implemented")) { + rows[i].style.background = "#ebebf2"; + } + else if (rows[i].children[2].innerHTML.includes("3rd Party")) { + rows[i].style.background = "#ebebf2"; + } + } +} + +window.addEventListener('DOMContentLoaded', (event) => { + colorRows(); +}); \ No newline at end of file diff --git a/PowerShell/ScubaGear/Modules/Orchestrator.psm1 b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 new file mode 100644 index 0000000000..8770145270 --- /dev/null +++ b/PowerShell/ScubaGear/Modules/Orchestrator.psm1 @@ -0,0 +1,643 @@ +function Invoke-SCuBA { + <# + .Description + This is the orchestrator function that runs the Providers, Rego, and Report creation all in one + PowerShell script call + .Parameter ProductNames + Which Baseline Names to run their respective Providers and Rego tests + .Parameter Endpoint + The Endpoint parameter for PowerPlatform authentication + .Parameter OPAPath + Path to the OPA executuable + .Parameter LogIn + Set $true to authenticate yourself to a tenant or if you are already authenticated set to $false + .Example + Invoke-SCuBA -LogIn $True -ProductNames @("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", "onedrive") -Endpoint "usgov" -OPAPath "./" -OutPath output + .Example + Invoke-SCuBA -LogIn $False -ProductNames @("powerplatform", "exo") -Endpoint "prod" -OPAPath "./" -OutPath "/Reports" + .Functionality + Public + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)] + [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", "onedrive", IgnoreCase = $false)] + [string[]] + $ProductNames, + + [string] + $Endpoint = "", + + [Parameter(Mandatory=$true)] + [string] + $OPAPath = $PSScriptRoot, + + [boolean] + $LogIn = $true, + + [switch] + $Version, + + [string] + $OutPath = $PSScriptRoot + ) + process { + # The equivalent of ..\.. + $ParentPath = Split-Path $PSScriptRoot -Parent + $ParentPath = Split-Path $(Split-Path $ParentPath -Parent) -Parent + $ModuleVersion = $MyInvocation.MyCommand.ScriptBlock.Module.Version + + if($Version) { + Write-Output("SCuBA Gear v$ModuleVersion") + return + } + + # Create a folder to dump everything into + $Date = Get-Date + $DateStr = $Date.ToString("yyyy_MM_dd_HH_mm_ss") + + $FormattedTimeStamp = $DateStr + + $OutFolderPath = $OutPath + $FolderName = "M365BaselineConformance_$($FormattedTimeStamp)" + New-Item -Path $OutFolderPath -Name $($FolderName) -ItemType Directory | Out-Null + $OutFolderPath = Join-Path -Path $OutFolderPath -ChildPath $FolderName + + $ProductNames = $ProductNames | Sort-Object + + $ConnectionParams = @{ + 'LogIn' = $LogIn; + 'ProductNames' = $ProductNames; + 'Endpoint' = $Endpoint; + } + + # If a PowerShell module is updated, the changes + # will not reflect until it is reimported into the runtime + Remove-Resources + Import-Resources # Imports Providers, RunRego, CreateReport + + Invoke-Connection @ConnectionParams + + $TenantDetails = Get-TenantDetails -ProductNames $ProductNames + $ProviderParams = @{ + 'ProductNames' = $ProductNames; + 'TenantDetails' = $TenantDetails; + 'OutFolderPath' = $OutFolderPath; + } + $RegoParams = @{ + 'ProductNames' = $ProductNames; + 'OPAPath' = $OPAPath; + 'ParentPath' = $ParentPath; + 'OutFolderPath' = $OutFolderPath; + } + $DisplayName = $TenantDetails | ConvertFrom-Json + $DisplayName = $DisplayName.DisplayName + $ReportParams = @{ + 'ProductNames' = $ProductNames; + 'DisplayName' = $DisplayName + 'OutFolderPath' = $OutFolderPath; + } + Invoke-ProviderList @ProviderParams + Invoke-RunRego @RegoParams + Invoke-ReportCreation @ReportParams + } + } + +$ArgToProd = @{ + teams = "Teams"; + exo = "EXO"; + defender = "Defender"; + aad = "AAD"; + powerplatform = "PowerPlatform"; + sharepoint = "SharePoint"; + onedrive = "OneDrive"; +} + +function Invoke-ProviderList { + <# + .Description + This function runs the various providers modules stored in the Providers Folder + Output will be stored as a ProviderSettingsExport.json in the OutPath Folder + .Functionality + Internal + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)] + [string[]] + $ProductNames, + + [Parameter(Mandatory=$true)] + [string] + $TenantDetails, + + [Parameter(Mandatory=$true)] + [string] + $OutFolderPath + ) + process { + # yes the syntax has to be like this + # fixing the spacing causes PowerShell interpreter errors + $ProviderJSON = @" +"@ + $ModuleVersion = $MyInvocation.MyCommand.ScriptBlock.Module.Version + $N = 0 + $Len = $ProductNames.Length + foreach ($Product in $ProductNames) { + $BaselineName = $ArgToProd[$Product] + $N += 1 + $Percent = $N*100/$Len + Write-Progress -Activity "Running the provider for each baseline" -Status "Running the $($BaselineName) Provider; $($n) of $($Len) Product settings extracted" -PercentComplete $Percent -Id 1 + $RetVal = "" + switch ($Product) { + "aad"{ + $RetVal = Export-AADProvider | Select-Object -Last 1 + } + "exo" { + $RetVal = Export-EXOProvider | Select-Object -Last 1 + } + "defender" { + $RetVal = Export-DefenderProvider | Select-Object -Last 1 + } + "powerplatform"{ + $RetVal = Export-PowerPlatformProvider | Select-Object -Last 1 + } + "onedrive"{ + $RetVal = Export-OneDriveProvider | Select-Object -Last 1 + } + "sharepoint"{ + $RetVal = Export-SharePointProvider | Select-Object -Last 1 + } + "teams" { + $RetVal = Export-TeamsProvider | Select-Object -Last 1 + } + default { + Write-Error -Message "Invalid ProductName argument" + } + } + $ProviderJSON += $RetVal + } + + # Clean up EXO - Defender conflicts + if($ProductNames -contains "defender") { + Disconnect-ExchangeOnline -Confirm:$false -InformationAction Ignore -ErrorAction SilentlyContinue | Out-Null + Disconnect-ExchangeOnline -Confirm:$false -InformationAction Ignore -ErrorAction SilentlyContinue | Out-Null + } + + $ProviderJSON = $ProviderJSON.TrimEnd(",") + $TimeZone = "" + if ((Get-Date).IsDaylightSavingTime()) { + $TimeZone = (Get-TimeZone).DaylightName + } + else { + $TimeZone = (Get-TimeZone).StandardName + } + $BaselineSettingsExport = @" +{ + "baseline_version": "0.1", + "module_version": "$ModuleVersion", + "date": "$(Get-Date) $($TimeZone)", + "tenant_details": $($TenantDetails), + + $ProviderJSON +} +"@ + $FinalPath = Join-Path -Path $OutFolderPath -ChildPath "ProviderSettingsExport.json" + $BaselineSettingsExport | Set-Content -Path $FinalPath + } +} + +function Invoke-RunRego { + <# + .Description + This function runs the RunRego module. + Which runs the various rego files against the + ProviderSettings.json using the specified OPA executable + Output will be stored as a TestResults.json in the OutPath Folder + .Functionality + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [string[]] + $ProductNames, + + [string] + $OPAPath = $PSScriptRoot, + + [Parameter(Mandatory=$true)] + [String] + $ParentPath, + + [Parameter(Mandatory=$true)] + [String] + $OutFolderPath + ) + process { + $TestResults = @() + $N = 0 + $Len = $ProductNames.Length + foreach ($Product in $ProductNames) { + $BaselineName = $ArgToProd[$Product] + $N += 1 + $Percent = $N*100/$Len + Write-Progress -Activity "Running the rego for each baseline" -Status "Running the $($BaselineName) Rego Verification; $($n) of $($Len) Rego verifications completed" -PercentComplete $Percent -Id 1 + $InputFile = Join-Path -Path $OutFolderPath "ProviderSettingsExport.json" + $RegoFile = Join-Path -Path $ParentPath -ChildPath "Rego" + $RegoFile = Join-Path -Path $RegoFile -ChildPath "$($BaselineName)Config.rego" + $params = @{ + 'InputFile' = $InputFile; + 'RegoFile' = $RegoFile; + 'PackageName' = $Product; + 'OPAPath' = $OPAPath + } + $RetVal = Invoke-Rego @params + $TestResults += $RetVal + } + + $TestResultsJson = $TestResults | ConvertTo-Json -Depth 5 + $FileName = Join-Path -path $OutFolderPath "TestResults.json" + $TestResultsJson | Set-Content -Path $FileName + + foreach ($Product in $TestResults) { + foreach ($Test in $Product) { + # ConvertTo-Csv struggles with the nested nature of the ActualValue + # field. Explicitly convert the ActualValues to json strings before + # calling ConvertTo-Csv + $Test.ActualValue = $Test.ActualValue | ConvertTo-Json -Depth 3 -Compress + } + } + + $TestResultsCsv = $TestResults | ConvertTo-Csv -NoTypeInformation + $CSVFileName = Join-Path -Path $OutFolderPath "TestResults.csv" + $TestResultsCsv | Set-Content -Path $CSVFileName + } + } + +function Pluralize { + <# + .Description + This function whether the singular or plural version of the noun + is needed and returns the appropriate version. + .Functionality + Internal + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)] + [string] + $SingularNoun, + + [Parameter(Mandatory=$true)] + [string] + $PluralNoun, + + [Parameter(Mandatory=$true)] + [int] + $Count + ) + process { + if ($Count -gt 1) { + return $PluralNoun + } + else { + return $SingularNoun + } + } +} + +function Invoke-ReportCreation { + <# + .Description + This function runs the CreateReport Module + which creates an HTML report using the TestResults.json. + Output will be stored as various HTML files in the OutPath Folder. + The report Home page will be named BaselineReports.html + .Functionality + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [string[]] + $ProductNames, + + [Parameter(Mandatory=$true)] + [string] + $DisplayName, + + [Parameter(Mandatory=$true)] + [String] + $OutFolderPath + ) + process { + $N = 0 + $Len = $ProductNames.Length + $Fragment = @() + $ModuleVersion = $MyInvocation.MyCommand.Module.Version + + $IndividualReportFolderName = "IndividualReports" + $IndividualReportPath = Join-Path -Path $OutFolderPath -ChildPath $IndividualReportFolderName + New-Item -Path $IndividualReportPath -ItemType "Directory" -ErrorAction "SilentlyContinue" | Out-Null + + $ReporterPath = Join-Path -Path $PSScriptRoot -ChildPath "CreateReport" + $Logo = Join-Path -Path $ReporterPath -ChildPath "cisa_logo.png" + Copy-Item -Path $Logo -Destination $IndividualReportPath -Force + + $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"; + } + + foreach ($Product in $ProductNames) { + $BaselineName = $ArgToProd[$Product] + $N += 1 + $Percent = $N*100/$Len + Write-Progress -Activity "Creating the reports for each baseline" -Status "Running the $($BaselineName) Report creation; $($n) of $($Len) Baselines Reports created" -PercentComplete $Percent -Id 1 + + $FullName = $ProdToFullName[$BaselineName] + + $CreateReportParams = @{ + 'BaselineName' = $BaselineName; + 'FullName' = $FullName; + 'IndividualReportPath' = $IndividualReportPath; + 'OutPath' = $OutFolderPath; + } + + $Report = New-Report @CreateReportParams + $LinkPath = "$($IndividualReportFolderName)/$($BaselineName)Report.html" + $LinkClassName = '"individual_reports"' # uses no escape characters + $Link = "$($FullName)" + + $PassesSummary = "
$($Report.Passes) tests passed
" + $WarningsSummary = "
" + $FailuresSummary = "
" + $ManualSummary = "
" + + if ($Report.Warnings -gt 0) { + $Noun = Pluralize -SingularNoun "warning" -PluralNoun "warnings" -Count $Report.Warnings + $WarningsSummary = "
$($Report.Warnings) $($Noun)
" + } + + if ($Report.Failures -gt 0) { + $Noun = Pluralize -SingularNoun "test" -PluralNoun "tests" -Count $Report.Failures + $FailuresSummary = "
$($Report.Failures) $($Noun) failed
" + } + + if ($Report.Manual -gt 0) { + $Noun = Pluralize -SingularNoun "check" -PluralNoun "checks" -Count $Report.Manual + $ManualSummary = "
$($Report.Manual) manual $($Noun) needed
" + } + + $Fragment += [pscustomobject]@{ + "Baseline Conformance Reports" = $Link; + "Details" = "$($PassesSummary) $($WarningsSummary) $($FailuresSummary) $($ManualSummary)" + } + } + + $Fragment = $Fragment | ConvertTo-Html -Fragment + + $ReportHTML = Get-Content $(Join-Path -Path $ReporterPath -ChildPath "ParentReportTemplate.html") + $ReportHTML = $ReportHTML.Replace("{TENANT_NAME}", $DisplayName) + $ReportHTML = $ReportHTML.Replace("{TABLES}", $Fragment) + $ReportHTML = $ReportHTML.Replace("{REPORT_TIME}", $Report.Date) + $ReportHTML = $ReportHTML.Replace("{MODULE_VERSION}", "v$ModuleVersion") + + $MainCSS = Get-Content $(Join-Path -Path $ReporterPath -ChildPath "main.css") + $ReportHTML = $ReportHTML.Replace("{MAIN_CSS}", "") + + $ParentCSS = Get-Content $(Join-Path -Path $ReporterPath -ChildPath "ParentStyle.css") + $ReportHTML = $ReportHTML.Replace("{PARENT_CSS}", "") + + Add-Type -AssemblyName System.Web + $ReportFileName = Join-Path -Path $OutFolderPath "BaselineReports.html" + [System.Web.HttpUtility]::HtmlDecode($ReportHTML) | Out-File $ReportFileName + Invoke-Item $ReportFileName + } +} + +function Get-TenantDetails { + <# + .Description + This function gets the details of the M365 Tenant using + the various M365 PowerShell modules + .Functionality + Internal + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)] + [string[]] + $ProductNames + ) + + # organized by best tenant details information + if ($ProductNames.Contains("teams")) { + Get-TeamsTenantDetail + } + elseif ($ProductNames.Contains("aad")) { + Get-AADTenantDetail + } + elseif ($ProductNames.Contains("exo")) { + Get-EXOTenantDetail + } + elseif ($ProductNames.Contains("defender")) { + Get-DefenderTenantDetail + } + elseif ($ProductNames.Contains("powerplatform")) { + Get-PowerPlatformTenantDetail + } + elseif ($ProductNames.Contains("sharepoint")) { + Get-AADTenantDetail + } + elseif ($ProductNames.Contains("onedrive")) { + Get-AADTenantDetail + } + else { + $TenantInfo = @{"DisplayName"="Undefined Name";} + $TenantInfo = $TenantInfo | ConvertTo-Json -Depth 3 + $TenantInfo + } +} + +function Invoke-Connection { + <# + .Description + This function uses the Connection.psm1 module + which uses the various PowerShell modules to establish + a connection to an M365 Tenant associated with provided + credentials + .Functionality + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [boolean] + $LogIn, + + [Parameter(Mandatory=$true)] + [string[]] + $ProductNames, + + [string] + $Endpoint + ) + if ($LogIn) { + $ConnectionPath = Join-Path -Path $PSScriptRoot -ChildPath "Connection" + Remove-Module "Connection" -ErrorAction "SilentlyContinue" + Import-Module $ConnectionPath + Connect-Tenant -ProductNames $ProductNames -Endpoint $Endpoint + } +} + +function Import-Resources { + <# + .Description + This function imports all of the various helper Provider, + Rego, and Reporter modules to the runtime + .Functionality + Internal + #> + $ProvidersPath = Join-Path -Path $PSScriptRoot ` + -ChildPath "Providers" ` + -Resolve + $ProviderResources = Get-ChildItem $ProvidersPath -Recurse | Where-Object { $_.Name -like 'Export*.psm1' } + if (!$ProviderResources) + { + Write-Error "Provider files were not found, aborting" + break + } + + foreach ($Provider in $ProviderResources.Name) { + $ProvidersPath = Join-Path -Path $PSScriptRoot -ChildPath "Providers" + $ModulePath = Join-Path -Path $ProvidersPath -ChildPath $Provider + Import-Module $ModulePath + } + $RegoPath = Join-Path -Path $PSScriptRoot -ChildPath "RunRego" + $ReporterPath = Join-Path -Path $PSScriptRoot -ChildPath "CreateReport" + Import-Module $RegoPath + Import-Module $ReporterPath +} + +function Remove-Resources { + <# + .Description + This function cleans up all of the various imported modules + Mostly meant for dev work + .Functionality + Internal + #> + $Providers = @("ExportPowerPlatform", "ExportEXOProvider", "ExportAADProvider", + "ExportDefenderProvider", "ExportTeamsProvider", "ExportSharePointProvider", "ExportOneDriveProvider") + foreach ($Provider in $Providers) { + Remove-Module $Provider -ErrorAction "SilentlyContinue" + } + + Remove-Module "RunRego" -ErrorAction "SilentlyContinue" + Remove-Module "CreateReport" -ErrorAction "SilentlyContinue" + Remove-Module "Connection" -ErrorAction "SilentlyContinue" +} + +function Invoke-RunCached { + <# + .Description + This is the function for Rego testing. Sometimes you don't want to pull the provider + JSON every single time. + This functions comes with the extra ExportProvider parameter to omit exporting the provider + if set to $false + The rego will be run on a static provider JSON in the specified OutPath. + .Functionality + Public + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)] + [ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", "onedrive", IgnoreCase = $false)] + [string[]] + $ProductNames, + + [string] + $Endpoint = "", + + # The path to the OPA executable. Defaults to this directory. + [Parameter(Mandatory=$true)] + [string] + $OPAPath = $PSScriptRoot, + + [boolean] + $LogIn = $true, + + # true to export the provider + # false to not export + [Parameter(Mandatory=$true)] + [boolean] + $ExportProvider, + + # The destination folder for the output. + [string] + $OutPath = $PSScriptRoot + ) + process { + # The equivalent of ..\.. + $ParentPath = Split-Path $PSScriptRoot -Parent + $ParentPath = Split-Path $(Split-Path $ParentPath -Parent) -Parent + + $OutFolderPath = $OutPath + $ProductNames = $ProductNames | Sort-Object + + # Authenticate + $ConnectionParams = @{ + 'LogIn' = $LogIn; + 'ProductNames' = $ProductNames; + 'Endpoint' = $Endpoint; + } + + #Rego Testing + $TenantDetails = @{"DisplayName"="Rego Testing";} + $TenantDetails = $TenantDetails | ConvertTo-Json -Depth 3 + + $ProviderParams = @{ + 'ProductNames' = $ProductNames; + 'TenantDetails' = $TenantDetails; + 'OutFolderPath' = $OutFolderPath; + } + $RegoParams = @{ + 'ProductNames' = $ProductNames; + 'OPAPath' = $OPAPath; + 'ParentPath' = $ParentPath; + 'OutFolderPath' = $OutFolderPath; + } + + $DisplayName = $TenantDetails | ConvertFrom-Json + $DisplayName = $DisplayName.DisplayName + $ReportParams = @{ + 'ProductNames' = $ProductNames; + 'DisplayName' = $DisplayName + 'OutFolderPath' = $OutFolderPath; + } + + # If a PowerShell module is updated, the changes + # will not reflect until it is reimported into the runtime + Remove-Resources + Import-Resources # Imports Providers, RunRego, CreateReport + + if ($ExportProvider) { + Invoke-Connection @ConnectionParams + Invoke-ProviderList @ProviderParams + } + Invoke-RunRego @RegoParams + Invoke-ReportCreation @ReportParams + } + } + +Export-ModuleMember -Function @( + 'Invoke-SCuBA', + 'Invoke-RunCached' +) diff --git a/PowerShell/ScubaGear/Modules/Providers/ExportAADProvider.psm1 b/PowerShell/ScubaGear/Modules/Providers/ExportAADProvider.psm1 new file mode 100644 index 0000000000..a82096a4f7 --- /dev/null +++ b/PowerShell/ScubaGear/Modules/Providers/ExportAADProvider.psm1 @@ -0,0 +1,240 @@ +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 + #> + + try { + # 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 = Get-MgIdentityConditionalAccessPolicy -ErrorAction Stop | ConvertTo-Json -Depth 10 + + # 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 = (Get-MgSubscribedSku).ServicePlans | Where-Object -Property ProvisioningStatus -eq -Value "Success" -ErrorAction Stop + # 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" + $ServicePlans = $ServicePlans | ConvertTo-Json -Depth 3 + + # A list of privileged users and their role assignments is used for 2.11 and 2.12 + # If the tenant has the premium license then we want to process PIM Eligible role assignments - otherwise we don't to avoid an error + if ($RequiredServicePlan) { + $PrivilegedUsers = Get-PrivilegedUser -TenantHasPremiumLicense + } + else{ + $PrivilegedUsers = Get-PrivilegedUser + } + $PrivilegedUsers = $PrivilegedUsers | ConvertTo-Json + + # 2.13 support for role ID and display name mapping + # 2.14 - 2.16 Azure AD PIM role settings + if ($RequiredServicePlan){ + $PrivilegedRoles = Get-PrivilegedRole -TenantHasPremiumLicense + } + else{ + $PrivilegedRoles = Get-PrivilegedRole + } + $PrivilegedRoles = $PrivilegedRoles | ConvertTo-Json -Depth 10 # Depth required to get policy rule object details + + # 2.6 & 2.18 1st/3rd Policy Bullets + $AuthZPolicies = Get-MgPolicyAuthorizationPolicy -ErrorAction Stop | ConvertTo-Json + + # 2.7 third bullet + $DirectorySettings = ConvertTo-Json -Depth 10 @(Get-MgDirectorySetting) -ErrorAction Stop + + # 2.7 Policy Bullet 2] + $AdminConsentReqPolicies = Get-MgPolicyAdminConsentRequestPolicy -ErrorAction Stop | ConvertTo-Json + } + catch { + Write-Error "Check the second error message below and if it appears to be related to permissions, your user account must have a minimum of Global Reader role to run this script. You must also get an administrator to consent to the required MS Graph Powershell application permissions. View the README file for detailed instructions and then try again." + Throw $_ + } + + # Note the spacing and the last comma in the json is important + $json = @" + "conditional_access_policies": $AllPolicies, + "authorization_policies": $AuthZPolicies, + "admin_consent_policies": $AdminConsentReqPolicies, + "privileged_users": $PrivilegedUsers, + "privileged_roles": $PrivilegedRoles, + "service_plans": $ServicePlans, + "directory_settings": $DirectorySettings, +"@ + + # 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 + #> + $TenantInfo = @{} + $TenantInfo.DisplayName = $(Get-MgOrganization).DisplayName + $TenantInfo = $TenantInfo | ConvertTo-Json -Depth 4 + $TenantInfo +} + +function Get-PrivilegedUser { + <# + .Description + Gets the array of the highly privileged users + .Functionality + Internal + #> + param ( + [switch] + $TenantHasPremiumLicense + ) + + $PrivilegedUsers = @{} + $PrivilegedRoles = @("Global Administrator", "Privileged Role Administrator", "User Administrator", "SharePoint Administrator", "Exchange Administrator", "Hybrid identity administrator", "Application Administrator", "Cloud Application Administrator") + $AADRoles = Get-MgDirectoryRole -ErrorAction Stop | Where-Object { $_.DisplayName -in $PrivilegedRoles } + + # Process the Active role assignments + foreach ($Role in $AADRoles) { + + $UsersAssignedRole = Get-MgDirectoryRoleMember -ErrorAction Stop -DirectoryRoleId $Role.Id + + foreach ($User in $UsersAssignedRole) { + + $Objecttype = $User.AdditionalProperties."@odata.type" -replace "#microsoft.graph." + + if ($Objecttype -eq "user") { + $AADUser = Get-MgUser -ErrorAction Stop -UserId $User.Id + + if (-Not $PrivilegedUsers.ContainsKey($AADUser.Id)) { + $PrivilegedUsers[$AADUser.Id] = @{"DisplayName"=$AADUser.DisplayName; "OnPremisesImmutableId"=$AADUser.OnPremisesImmutableId; "roles"=@()} + } + $PrivilegedUsers[$AADUser.Id].roles += $Role.DisplayName + } + + elseif ($Objecttype -eq "group") { + $GroupMembers = Get-MgGroupMember -ErrorAction Stop -GroupId $User.Id + foreach ($GroupMember in $GroupMembers) { + $Membertype = $GroupMember.AdditionalProperties."@odata.type" -replace "#microsoft.graph." + if ($Membertype -eq "user") { + $AADUser = Get-MgUser -ErrorAction Stop -UserId $GroupMember.Id + + if (-Not $PrivilegedUsers.ContainsKey($AADUser.Id)) { + $PrivilegedUsers[$AADUser.Id] = @{"DisplayName"=$AADUser.DisplayName; "OnPremisesImmutableId"=$AADUser.OnPremisesImmutableId; "roles"=@()} + } + $PrivilegedUsers[$AADUser.Id].roles += $Role.DisplayName + } + } + } + } + } + + # Process the Eligible role assignments if the premium license for PIM is there + if ($TenantHasPremiumLicense) { + + foreach ($Role in $AADRoles) { + $PrivRoleId = $Role.RoleTemplateId + $PIMRoleAssignments = Get-MgRoleManagementDirectoryRoleEligibilityScheduleInstance -ErrorAction Stop -Filter "roleDefinitionId eq '$PrivRoleId'" + + foreach ($PIMRoleAssignment in $PIMRoleAssignments) { + $UserObjectId = $PIMRoleAssignment.PrincipalId + try { + $AADUser = Get-MgUser -ErrorAction Stop -Filter "Id eq '$UserObjectId'" + $UserType = "user" + + if (-Not $PrivilegedUsers.ContainsKey($AADUser.Id)) { + $PrivilegedUsers[$AADUser.Id] = @{"DisplayName"=$AADUser.DisplayName; "OnPremisesImmutableId"=$AADUser.OnPremisesImmutableId; "roles"=@()} + } + $PrivilegedUsers[$AADUser.Id].roles += $Role.DisplayName + } + catch { + $UserType = "unknown" + } + + if ($UserType -eq "unknown") { + try { + $GroupMembers = Get-MgGroupMember -ErrorAction Stop -GroupId $UserObjectId + $UserType = "group" + foreach ($GroupMember in $GroupMembers) { + $Membertype = $GroupMember.AdditionalProperties."@odata.type" -replace "#microsoft.graph." + if ($Membertype -eq "user") { + $AADUser = Get-MgUser -ErrorAction Stop -UserId $GroupMember.Id + if (-Not $PrivilegedUsers.ContainsKey($AADUser.Id)) { + $PrivilegedUsers[$AADUser.Id] = @{"DisplayName"=$AADUser.DisplayName; "OnPremisesImmutableId"=$AADUser.OnPremisesImmutableId; "roles"=@()} + } + $PrivilegedUsers[$AADUser.Id].roles += $Role.DisplayName + } + } + } + catch { + $UserType = "unknown" + } + } + } + } + } + + $PrivilegedUsers +} + +function Get-PrivilegedRole { + <# + .Description + Gets the array of the highly privileged roles along with the users assigned to the role and the security policies applied to it + .Functionality + Internal + #> + param ( + [switch] + $TenantHasPremiumLicense + ) + + $PrivilegedRoles = @("Global Administrator", "Privileged Role Administrator", "User Administrator", "SharePoint Administrator", "Exchange Administrator", "Hybrid identity administrator", "Application Administrator", "Cloud Application Administrator") + $AADRoles = Get-MgDirectoryRoleTemplate -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 eligible / active role assigments + if ($TenantHasPremiumLicense) { + $RolePolicyAssignments = Get-MgPolicyRoleManagementPolicyAssignment -ErrorAction Stop -Filter "scopeId eq '/' and scopeType eq 'Directory'" + + # Create an array of the highly privileged roles along with the users assigned to the role and the security policies applied to it + + foreach ($Role in $AADRoles) { + $RolePolicies = @() + $RoleTemplateId = $Role.RoleTemplateId + + # Get role policy assignments + # Note: Each role can only be assigned a single policy at most + $PolicyAssignment = $RolePolicyAssignments | Where-Object -Property RoleDefinitionId -eq -Value $RoleTemplateId + $RoleAssignments = @(Get-MgRoleManagementDirectoryRoleAssignmentScheduleInstance -ErrorAction Stop -Filter "roleDefinitionId eq '$RoleTemplateId'") + + # Append each policy assignment to the role object + if ($PolicyAssignment.length -eq 1) { + $RolePolicies = Get-MgPolicyRoleManagementPolicyRule -ErrorAction Stop -UnifiedRoleManagementPolicyId $PolicyAssignment.PolicyId + } + elseif ($PolicyAssignment.length -gt 1) { + $RolePolicies = "Too many policies found" + } + else { + $RolePolicies = "No policies found" + } + + $Role | Add-Member -Name "Rules" -Value $RolePolicies -MemberType NoteProperty + $Role | Add-Member -Name "Assignments" -Value $RoleAssignments -MemberType NoteProperty + } + } + + $AADRoles +} diff --git a/PowerShell/ScubaGear/Modules/Providers/ExportDefenderProvider.psm1 b/PowerShell/ScubaGear/Modules/Providers/ExportDefenderProvider.psm1 new file mode 100644 index 0000000000..f66e772eae --- /dev/null +++ b/PowerShell/ScubaGear/Modules/Providers/ExportDefenderProvider.psm1 @@ -0,0 +1,106 @@ +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 + #> + + # Sign in for the Defender Provider if not connected + $ExchangeConnected = Get-OrganizationConfig -ErrorAction SilentlyContinue + if(-not $ExchangeConnected) { + Connect-ExchangeOnline -ShowBanner:$false | Out-Null + } + Import-Module ExchangeOnlineManagement + + # Regular Exchange i.e non IPPSSession cmdlets + $AdminAuditLogConfig = Get-AdminAuditLogConfig | ConvertTo-Json + $ProtectionPolicyRule = ConvertTo-Json @(Get-EOPProtectionPolicyRule) + $MalwareFilterPolicy = ConvertTo-Json @(Get-MalwareFilterPolicy) + $AntiPhishPolicy = ConvertTo-Json @(Get-AntiPhishPolicy) + $HostedContentFilterPolicy = ConvertTo-Json @(Get-HostedContentFilterPolicy) + $AllDomains = Get-AcceptedDomain | ConvertTo-Json + + # 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 @(Get-SafeAttachmentPolicy) + $SafeAttachmentRule = ConvertTo-Json @(Get-SafeAttachmentRule) + $SafeLinksPolicy = ConvertTo-Json @(Get-SafeLinksPolicy) + $SafeLinksRule = ConvertTo-Json @(Get-SafeLinksRule) + $ATPPolicy = ConvertTo-Json @(Get-AtpPolicyForO365) + $DefenderLicense = ConvertTo-Json $true + } + else { + # The tenant can't make use of the defender commands + $SafeAttachmentPolicy = ConvertTo-Json @() + $SafeAttachmentRule = ConvertTo-Json @() + $SafeLinksPolicy = ConvertTo-Json @() + $SafeLinksRule = ConvertTo-Json @() + $ATPPolicy = ConvertTo-Json @() + $DefenderLicense = ConvertTo-Json $false + } + + $AllDomains = ConvertTo-Json @(Get-AcceptedDomain) + + # Connect to Security & Compliance + Connect-IPPSSession | Out-Null + + $DLPCompliancePolicy = ConvertTo-Json @(Get-DlpCompliancePolicy) + $DLPComplianceRules = @(Get-DlpComplianceRule) + $ProtectionAlert = Get-ProtectionAlert | ConvertTo-Json + + # Powershell is inconsistent with how they save lists to json. + # This loop ensures that the format of ContentContainsSensitiveInformation + # will *always* be a list. + foreach($Rule in $DLPComplianceRules) { + $Rule.ContentContainsSensitiveInformation = @($Rule.ContentContainsSensitiveInformation) + } + + # We need to specify the depth because the data contains some + # nested tables. + $DLPComplianceRules = ConvertTo-Json -Depth 3 $DLPComplianceRules + + # 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, +"@ + + # We need to remove the backslash characters from the + # json, otherwise rego gets mad. + $json = $json.replace("\`"", "'") + $json = $json.replace("\", "") + $json +} + +function Get-DefenderTenantDetail { + <# + .Description + Gets the tenant details using the AAD PowerShell Module + .Functionality + Internal + #> + Import-Module ExchangeOnlineManagement + $Config = Get-OrganizationConfig + $TenantInfo = @{"DisplayName"=$Config.Name;} + $TenantInfo = $TenantInfo | ConvertTo-Json -Depth 4 + $TenantInfo +} diff --git a/PowerShell/ScubaGear/Modules/Providers/ExportEXOProvider.psm1 b/PowerShell/ScubaGear/Modules/Providers/ExportEXOProvider.psm1 new file mode 100644 index 0000000000..6fda0ba481 --- /dev/null +++ b/PowerShell/ScubaGear/Modules/Providers/ExportEXOProvider.psm1 @@ -0,0 +1,195 @@ +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 + #> + + Import-Module ExchangeOnlineManagement + <# + 2.1 + #> + $RemoteDomains = @(Get-RemoteDomain) + foreach ($d in $RemoteDomains) { + # Need to explicitly convert these values to strings, otherwise + # these fields contain values Rego can't parse. + $d.WhenChanged = $d.WhenChanged.ToString() + $d.WhenCreated = $d.WhenCreated.ToString() + $d.WhenChangedUTC = $d.WhenChangedUTC.ToString() + $d.WhenCreatedUTC = $d.WhenCreatedUTC.ToString() + } + + $RemoteDomains = ConvertTo-Json $RemoteDomains + <# + 2.2 SPF + #> + + $SPFRecords = @() + + $domains = Get-AcceptedDomain + + foreach ($d in $domains) { + try { + $response = Resolve-DnsName $d.DomainName txt -ErrorAction Stop + $rdata = @($response.Strings) + } + catch { + $rdata = "" + } + + $DomainName = $d.DomainName + $SPFRecords += [PSCustomObject]@{ + "domain" = $DomainName; + "rdata" = $rdata + } + } + + $SPFRecords = ConvertTo-Json $SPFRecords + + <# + 2.3 DKIM + #> + $DKIMConfig = @(Get-DkimSigningConfig) + $DKIMRecords = @() + foreach ($d in $domains) { + $DomainName = $d.DomainName + $selectors = "selector1", "selector2" + $selectors += "selector1.$DomainName" -replace "\.", "-" + $selectors += "selector2.$DomainName" -replace "\.", "-" + + $rdata = "" + foreach ($s in $selectors) { + try { + $response = Resolve-DnsName "$s._domainkey.$DomainName" txt -ErrorAction Stop + $rdata = $response.Strings + break + } + catch { + continue + } + } + $DKIMRecords += [PSCustomObject]@{ + "domain" = $DomainName; + "rdata" = "$rdata" + } + } + + $DKIMRecords = ConvertTo-Json $DKIMRecords + $DKIMConfig = ConvertTo-Json $DKIMConfig + <# + 2.4 DMARC + #> + $DMARCRecords = @() + + foreach ($d in $domains) { + try { + $DomainName = $d.DomainName + $response = Resolve-DnsName "_dmarc.$DomainName" txt -ErrorAction Stop + $rdata = $response.Strings + } + catch { + $Labels = $d.DomainName.Split(".") + try { + $Labels = $d.DomainName.Split(".") + $OrgDomain = $Labels[-2] + "." + $Labels[-1] + # Technically the logic above is incomplete. This will work when the tld is single + # label (e.g., com, org, gov). However, when the tld is two-labels (e.g., gov.uk), + # this will cause an error. Leaving cases like that as out-of-scope for now. + $response = Resolve-DnsName "_dmarc.$OrgDomain" txt -ErrorAction Stop + $rdata = $response.Strings + } + catch { + $rdata = "" + } + } + + $DomainName = $d.DomainName + $DMARCRecords += [PSCustomObject]@{ + "domain" = $DomainName; + "rdata" = "$rdata" + } + } + $DMARCRecords = ConvertTo-Json $DMARCRecords + <# + 2.5 + #> + + $TransportConfig = Get-TransportConfig + $TransportConfig.WhenChanged = $TransportConfig.WhenChanged.ToString() + $TransportConfig.WhenCreated = $TransportConfig.WhenCreated.ToString() + $TransportConfig.WhenChangedUTC = $TransportConfig.WhenChangedUTC.ToString() + $TransportConfig.WhenCreatedUTC = $TransportConfig.WhenCreatedUTC.ToString() + $TransportConfig = ConvertTo-Json $TransportConfig + + <# + 2.6 + #> + $SharingPolicy = Get-SharingPolicy + $SharingPolicy.WhenChanged = $SharingPolicy.WhenChanged.ToString() + $SharingPolicy.WhenCreated = $SharingPolicy.WhenCreated.ToString() + $SharingPolicy.WhenChangedUTC = $SharingPolicy.WhenChangedUTC.ToString() + $SharingPolicy.WhenCreatedUTC = $SharingPolicy.WhenCreatedUTC.ToString() + $SharingPolicy = ConvertTo-Json $SharingPolicy + + <# + 2.7 + #> + + $TransportRules = @(Get-TransportRule) + foreach ($Rule in $TransportRules) { + $Rule.WhenChanged = $Rule.WhenChanged.ToString() + } + $TransportRules = ConvertTo-Json $TransportRules + + <# + 2.12 + #> + + $ConnectionFilter = Get-HostedConnectionFilterPolicy | ConvertTo-Json + + <# + 2.13 + #> + $Config = Get-OrganizationConfig + $Config = $Config | ConvertTo-Json + + + <# + 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, + "org_config": $Config, + "conn_filter": $ConnectionFilter, +"@ + + + # 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 + #> + Import-Module ExchangeOnlineManagement + $Config = Get-OrganizationConfig + $TenantInfo = @{"DisplayName"=$Config.Name;} + $TenantInfo = $TenantInfo | ConvertTo-Json -Depth 4 + $TenantInfo +} diff --git a/PowerShell/ScubaGear/Modules/Providers/ExportOneDriveProvider.psm1 b/PowerShell/ScubaGear/Modules/Providers/ExportOneDriveProvider.psm1 new file mode 100644 index 0000000000..eaca9cd74e --- /dev/null +++ b/PowerShell/ScubaGear/Modules/Providers/ExportOneDriveProvider.psm1 @@ -0,0 +1,27 @@ +function Export-OneDriveProvider { + <# + .Description + Gets the OneDrive settings that are relevant + to the SCuBA OneDrive baselines using the SharePoint PowerShell Module + .Functionality + Internal + #> + + $InitialDomain = (Get-MgOrganization).VerifiedDomains | Where-Object {$_.isInitial} + $InitialDomainPrefix = $InitialDomain.Name.split(".")[0] + $SPOTenantInfo = Get-SPOTenant | ConvertTo-Json + $ExpectedResults = Get-SPOSite -Identity "https://$($InitialDomainPrefix).sharepoint.com/" | ConvertTo-Json + $TenantSyncInfo = Get-SPOTenantSyncClientRestriction | ConvertTo-Json + + # Note the spacing and the last comma in the json is important + $json = @" + "SPO_tenant_info": $SPOTenantInfo, + "Expected_results": $ExpectedResults, + "Tenant_sync_info": $TenantSyncInfo, +"@ + + # We need to remove the backslash characters from the json, otherwise rego gets mad. + $json = $json.replace("\`"", "'") + $json = $json.replace("\", "") + $json +} diff --git a/PowerShell/ScubaGear/Modules/Providers/ExportPowerPlatformProvider.psm1 b/PowerShell/ScubaGear/Modules/Providers/ExportPowerPlatformProvider.psm1 new file mode 100644 index 0000000000..426fe9f887 --- /dev/null +++ b/PowerShell/ScubaGear/Modules/Providers/ExportPowerPlatformProvider.psm1 @@ -0,0 +1,70 @@ +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 + #> + + # Note importing the module might have to be done for every provider as + # There are conflicting PowerShell Cmdlet names in EXO and Power Platform + Import-Module Microsoft.PowerApps.Administration.PowerShell -DisableNameChecking + + $TenantDetails = Get-TenantDetailsFromGraph + $TenantID = $TenantDetails.TenantId + + # 2.1 + $EnvironmentCreation = Get-TenantSettings | ConvertTo-Json + + # 2.2 + $EnvironmentList = Get-AdminPowerAppEnvironment | ConvertTo-Json + $DLPPolicy = Get-DlpPolicy + $DLPPolicies = ConvertTo-Json -Depth 7 $DLPPolicy + + if (!$EnvironmentList) { + $EnvironmentList = '"error"' + } + + if (!$DLPPolicies) { + $DLPPolicies = '"error"' + } + + # 2.3 + $TenantIsolation = Get-PowerAppTenantIsolationPolicy -TenantID $TenantID | ConvertTo-Json + + # 2.4 no UI for enabling content security according to doc + # 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, +"@ + + # 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 + # https://stackoverflow.com/questions/64093078/how-to-find-unicode-characters-that-are-not-utf8-in-vs-code + $json +} + +function Get-PowerPlatformTenantDetail { + <# + .Description + Gets the M365 tenant details using the Power Platform PowerShell Module + .Functionality + Internal + #> + Import-Module Microsoft.PowerApps.Administration.PowerShell -DisableNameChecking + $TenantDetails = Get-TenantDetailsFromGraph + $TenantInfo = @{"DisplayName"=$TenantDetails.DisplayName;} + $TenantInfo = $TenantInfo | ConvertTo-Json -Depth 4 + $TenantInfo +} diff --git a/PowerShell/ScubaGear/Modules/Providers/ExportSharePointProvider.psm1 b/PowerShell/ScubaGear/Modules/Providers/ExportSharePointProvider.psm1 new file mode 100644 index 0000000000..ef6c4cab03 --- /dev/null +++ b/PowerShell/ScubaGear/Modules/Providers/ExportSharePointProvider.psm1 @@ -0,0 +1,24 @@ +function Export-SharePointProvider { + <# + .Description + Gets the SharePoint settings that are relevant + to the SCuBA SharePoint baselines using the SharePoint PowerShell Module + .Functionality + Internal + #> + + $InitialDomain = (Get-MgOrganization).VerifiedDomains | Where-Object {$_.isInitial} + $InitialDomainPrefix = $InitialDomain.Name.split(".")[0] + $SPOTenant = Get-SPOTenant | ConvertTo-Json + $SPOSite = Get-SPOSite -Identity "https://$($InitialDomainPrefix).sharepoint.com/" -detailed | Select-Object -Property * | ConvertTo-Json + + # Note the spacing and the last comma in the json is important + $json = @" + "SPO_tenant": $SPOTenant, + "SPO_site": $SPOSite, +"@ + # We need to remove the backslash characters from the json, otherwise rego gets mad. + $json = $json.replace("\`"", "'") + $json = $json.replace("\", "") + $json +} diff --git a/PowerShell/ScubaGear/Modules/Providers/ExportTeamsProvider.psm1 b/PowerShell/ScubaGear/Modules/Providers/ExportTeamsProvider.psm1 new file mode 100644 index 0000000000..044bbcd03a --- /dev/null +++ b/PowerShell/ScubaGear/Modules/Providers/ExportTeamsProvider.psm1 @@ -0,0 +1,52 @@ +function Export-TeamsProvider { + <# + .Description + Gets the Teams settings that are relevant + to the SCuBA Teams baselines using the Teams PowerShell Module + .Functionality + Internal + #> + [CmdletBinding()] + + $TenantInfo = ConvertTo-Json @(Get-CsTenant) + $MeetingPolicies = ConvertTo-Json @(Get-CsTeamsMeetingPolicy) + $FedConfig = ConvertTo-Json @(Get-CsTenantFederationConfiguration) + $ClientConfig = ConvertTo-Json @(Get-CsTeamsClientConfiguration) + $AppPolicies = ConvertTo-Json @(Get-CsTeamsAppPermissionPolicy) + $BroadcastPolicies = ConvertTo-Json @(Get-CsTeamsMeetingBroadcastPolicy) + + # 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, +"@ + + # 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 + #> + $TenantInfo = Get-CsTenant + + # Need to explicitly clear or convert these values to strings, otherwise + # these fields contain values Rego can't parse. + $TenantInfo.AssignedPlan = @() + $TenantInfo.LastSyncTimeStamp = $TenantInfo.LastSyncTimeStamp.ToString() + $TenantInfo.WhenChanged = $TenantInfo.WhenChanged.ToString() + $TenantInfo.WhenCreated = $TenantInfo.WhenCreated.ToString() + $TenantInfo = ConvertTo-Json @($TenantInfo) -Depth 4 + $TenantInfo +} diff --git a/PowerShell/ScubaGear/Modules/RunRego/RunRego.psm1 b/PowerShell/ScubaGear/Modules/RunRego/RunRego.psm1 new file mode 100644 index 0000000000..84cbfb690a --- /dev/null +++ b/PowerShell/ScubaGear/Modules/RunRego/RunRego.psm1 @@ -0,0 +1,46 @@ +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)] + [string] + $InputFile, + + [Parameter(Mandatory=$true)] + [string] + $RegoFile, + + [Parameter(Mandatory=$true)] + [string] + $PackageName, + + # The path to the OPA executable. Defaults to the current directory. + [string] + $OPAPath = $PSScriptRoot +) + +# PowerShell 5.1 compatible Windows OS check +if ("Windows_NT" -eq $Env:OS) { + $Cmd = Join-Path -Path $OPAPath -ChildPath "opa_windows_amd64.exe" +} +else { + # Permissions: chmod 755 ./opa + $Cmd = Join-Path -Path $OPAPath -ChildPath "opa" +} + +$CmdArgs = @("eval", "-i", $InputFile, "-d", $RegoFile, "data.$PackageName.tests", "-f", "values") +$TestResults = $(& $Cmd @CmdArgs) | Out-String | ConvertFrom-Json + +$TestResults +} + +Export-ModuleMember -Function @( + 'Invoke-Rego' +) diff --git a/PowerShell/ScubaGear/ScubaGear.psd1 b/PowerShell/ScubaGear/ScubaGear.psd1 new file mode 100644 index 0000000000..4738ea2dbc Binary files /dev/null and b/PowerShell/ScubaGear/ScubaGear.psd1 differ diff --git a/README.md b/README.md index f76a5519fe..8351f8e6d4 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,236 @@ -# scuba -Secure Cloud Business Applications baselines and automation +# SCuBA M365 Security Baseline Assessment Tool +Developed by CISA, this assessment tool verifies that an M365 tenant’s configuration conforms to the policies described in the SCuBA Minimum Viable Secure Configuration Baseline documents. + +> **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 E3 or G3 and E5 or G5 M365 license bundle. It may still function for tenants that do not have one of these bundles but it was not specifically tested that way. Also, some of the specific policy checks in the baseline rely on the following specific security licenses which are included by default in E5 and G5. If your tenant does not have the security licenses listed below, the report will display a non-compliant output for those respective policy checks. +- Azure AD Premium Plan 2 +- Microsoft Defender for Office 365 Plan 1 + +## 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. + +## Installation +### Installing the required PowerShell Modules +To install the required modules, execute the Powershell script `.\SetUp.ps1` (in the project's root folder) as in the example below. +**Warning**: Note that -AllowClobber -Force flags are currently included in that script. These flags will install the latest available version of all the required Powershell modules. If you need to keep earlier versions of the modules for other software that you use, modify the script file SetUp.ps1 and remove those two flags. + +``` +.\Setup.ps1 +``` + +## Usage +To run the assessment tool, customize and then execute the Powershell script `.\RunSCuBA.ps1` (in the project's root folder). + +In `RunSCuBA.ps1` there are execution variables that you must customize by entering values that match your specific M365 tenant. The variables are described below, along with execution examples. + +### Variable Definitions + +- **$LogIn \-** is a `$true` or `$false` variable that if set to `$true` will prompt you to provide credentials if you want 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. If you want to run another verification in the same PowerShell session simply set this variable to be `$false` to bypass the reauthenticating in the same session. + +- **$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** + - MS Power Platform: **powerplatform** + - SharePoint Online: **sharepoint** + - MS Teams: **teams** + +- **$Endpoint** is a variable used to authenticate to Power Platform. This variable is only mandatory if **powerplatform** is included in **$ProductNames**. Valid values include "dod", "prod","preview","tip1", "tip2", "usgov", or "usgovhigh". For M365 tenants with E3/E5 licenses enter the value **"prod"**. For M365 tenants with G3/G5 licenses enter the value **"usgov"**. + +- **$OPAPath** is a variable that refers to the folder location of the OPA Rego executable file. By default the OPA Rego executable embedded with this project is located in the project's root folder `"./"` and for most cases you won't need to modify this variable value. If you want to execute the tool using a version of OPA Rego located in another folder, then customize the variable value with the full path to the alternative OPA Rego exe file. + +- **$OutPath** is a variable that refers to the folder path where both the output JSON and the HTML report will be created. By default the Reports folder is created in the same directory where the script is executed so you only need to modify this if you want the reports to be placed in an alternative location. The folder will be created if it does not exist. + +### Example Runs + +The example edited variable $ProductNames below in `RunSCuBA.ps1` will run the tool against the MS Teams security baseline +``` +$LogIn = $true +$ProductNames = @("teams") +$Endpoint = "" +$OPAPath = "./" +$OutPath = "./Reports" +``` + +After these values are in place and `RunSCuBA.ps1` is saved. In a PowerShell terminal in the same directory as `RunSCuBA.ps1` enter +``` +.\RunSCuBA.ps1 +``` + +The example edited variables $ProductNames and $OutPath below in `RunSCuBA.ps1` will run the tool against the Azure Active Directory and SharePoint Online baselines in an M365 tenant with the primary domain contoso.onmicrosoft.com. Notice that the output path for the reports has been customized. + +``` +$LogIn = $true +$ProductNames = @("aad", "sharepoint") +$Endpoint = "" +$OPAPath = "./" +$OutPath = "C:\Users\mgibson\documents\Reports" +``` + +The example edited variables $ProductNames and $Endpoint below in `RunSCuBA.ps1` will run the tool against all possible product security baselines in an M365 G5 tenant with the primary domain example.onmicrosoft.com. + +``` +$LogIn = $true +$ProductNames = @("aad", "defender", "exo", "onedrive", "powerplatform", "sharepoint", "teams") +$Endpoint = "usgov" +$OPAPath = "./" +$OutPath = "./Reports" +``` +### Viewing the Report +The html report should open automatically 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 User Permissions to Execute the tool +The tool has two types of permissions that are required to execute: User Permissions (which are associated with Azure AD roles assigned to a user) and Application Permissions (which are assigned to the MS Graph Powershell application in Azure AD). The minimum user roles needed to execute the tool against each of the products in M365 are described in the bullets below. The application permissions needed to execute against the Azure AD baseline and the process to setup those permissions are described in the section named "MS Graph Powershell permissions". If you run against the Power Platform product you will also need to have a "Power Apps for Office 365" license assigned to you. Before running the tool, check with your M365 administrator to ensure you have the required Azure AD roles. + +Note: If you are running the tool as a user with the Global Administrator role, you have the necessary user permissions and no additional roles should be necessary. No license assignments are needed either. The role requirements listed below are only applicable when running the tool with a user that is NOT Global Administrator. + +- Azure Active Directory: User must have the Global Reader role. +- MS Teams: User must have the Teams Administrator role. +- Exchange Online: User must have the Exchange Administrator role. +- Defender for Office 365: User must have the Exchange Administrator role. +- MS Power Platform: User must have the Power Platform Administrator role. User must also have the "Power Apps for Office 365" license. +- Sharepoint Online: User must have the SharePoint Administrator role. +- OneDrive: User must have the SharePoint Administrator role. + +## MS Graph Powershell permissions to run Azure AD Baseline +When executing the tool against the Azure AD baseline for the first time, the script will attempt to configure the required API permissions needed by the MS Graph Powershell module if they have not already been configured in your tenant. The process to configure the API permissions is sometimes referred to as the "application consent process" because an administrator must "consent" for the MS 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 you and how the application consent settings are configured in your tenant, the process may vary slightly. This section describes various first time usage scenarios that may occur depending on the roles assigned to you and how to respond for each scenario. + +For reference, the following API permissions are used by the tool via the MS Graph Powershell application: + +- Directory.Read.All +- GroupMember.Read.All +- Organization.Read.All +- Policy.Read.All +- Policy.ReadWrite.AuthenticationMethod +- RoleManagement.Read.Directory +- User.Read.All +- UserAuthenticationMethod.Read.All + +### Scenario 1 - You need to request approval via a workflow process +When executing for the first time, if you see a screen named Approval Required that has a Request Approval button on it, enter a justification and click Request Approval. + +![Request approval screen 1](images/regularuserrequestapproval.PNG) +![Request approval screen 2](images/regularuserrequestapproval2.PNG) + +The script will abort and you will see a permissions error. Next notify your administrator that your request was submitted. The administrator should go to the Azure AD > Enterprise Applications > Activity > Admin Consent Requests page. On that page the administrator should see the request for Microsoft Graph Powershell, click on it to open up the Details page, then click Review Permissions and Consent. The administrator should then review the list of permissions requested and click the Accept button if the permissions are acceptable per the organization's security policies. + +![Admin application consent screen 1](images/adminconsentapproval1.PNG) +![Admin application consent screen 2](images/adminconsentapproval2.PNG) +![Admin application consent screen 3](images/adminconsentapproval3.PNG) + +To verify that Microsoft Graph Powershell was properly configured, the administrator can navigate to the Azure AD > Enterprise Applications > Manage > All Applications > Microsoft Graph Powershell > Security > Permissions page and it should look similar to the screenshot below. + +![Admin application consent screen 4](images/adminconsentapproval4.PNG) + +Once the administrator has completed the consent, you can re-execute the tool and it should successfully complete without any permissions errors. + +### Scenario 2 - You need Admin approval but the tenant does not have a workflow process configured +When executing for the first time, you may see a screen named Need Admin Approval. + +![Admin approval but no workflow screen 1](images/regularuserneedadminapproval.PNG) + +If that screen appears, perform the following steps. +Ask your administrator to download the tool and then execute it against the Azure AD baseline. + +``` +$LogIn = $true +$ProductNames = @("aad") +$Endpoint = "" +$OPAPath = "./" +$OutPath = "./Reports" + +--- +.\RunSCuBA.ps1 +``` +When the administrator runs the tool, they will be prompted to perform the consent process. The administrator should see a screen named Permissions Requested. The administrator should select "Consent on behalf of your organization" and click the Accept button. + +![Admin approve permissions for user screen 1](images/adminconsentworkflownotconfigured.PNG) + +Once the administrator has completed these steps, you can re-execute the tool and it should successfully complete without any permissions errors. + +### Scenario 3 - A user with Global Administrator privileges runs the automation tool +When executing for the first time, if your user account has the Global Administrator role, the consent process is abbreviated compared to scenarios 1 and 2. You should see a screen named Permissions Requested similar to the one below. + +![Global admin runs tool screen 1](images/globaladminuserrunningscript.PNG) + +On the Permissions Requested screen you have two options: +- Option 1: Configure the permissions for your user account only - if you want to do this, do NOT click the "Consent on behalf of your organization" checkbox. Simply click the Accept button. +- Option 2: Configure the permissions so that other users in the tenant can also execute the tool - if you want to do this, click the "Consent on behalf of your organization" checkbox and then the Accept button. + +Then the script should execute without any permissions errors. + +### Scenario 4 - A user with Cloud Application Administrator privileges runs the automation tool +When executing for the first time, if your user account has the Cloud Application Administrator role, the consent process is abbreviated compared to scenarios 1 and 2. You can only configure the tool to run with your user account. You should see a screen named Permissions Requested similar to the one below. + +![Cloud App admin runs tool screen 1](images/cloudadminuserrunningscript.PNG) + +On the Permissions Requested screen click the Accept button. Then the script should execute without any permissions errors. + +## Design +The tool employs a three-step process: +1. **Export**. In this step, we utilize the various PowerShell modules authored by Microsoft to export and serialize all the relevant settings into json. +2. **Parse**. Compare the exported settings from the previous step with the configuration prescribed in the baselines. We do this 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 executable and version v0.41.0 is already included in this repo. The code for our tool was tested against the included version of OPA. If you desire to use a later version of Rego, 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. **Report**. Package the data output by Rego into a human-friendly html report. + +## File Organization +- The PowerShell folder contains the code used to export the configuration settings from the M365 tenant and orchestrate the entire process from export to parse to report. The main PowerShell module manifest `SCuBA.psd1` is located in the PowerShell folder. The RunSCuBA.ps1 script located in the root folder uses that module to execute the tool. +- The Rego folder holds the .rego files. Each rego file essentially audits against the "desired state" for each product, per the SCuBA M365 secure configuration baseline documents. +- The Reporter folder contains code and supporting files used to create and format the output report. +- The Testing folder contains code that is used during the development flow to unit test the Rego policies. + +## Troubleshooting + +### 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. + +``` +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: +``` +Disconnect-ExchangeOnline +``` + +### 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) + +### Connect-MgGraph : Key not valid for use in specified state. + +If when running the tool you get the 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. diff --git a/Rego/AADConfig.rego b/Rego/AADConfig.rego new file mode 100644 index 0000000000..c892f44967 --- /dev/null +++ b/Rego/AADConfig.rego @@ -0,0 +1,884 @@ +package aad +import future.keywords + +################ +# The report formatting functions below are generic and used throughout the policies # +################ +Format(Array) = format_int(count(Array), 10) + +Description(String1, String2, String3) = trim(concat(" ", [String1, String2, String3]), " ") + +ReportDetailsBoolean(Status) = "Requirement met" if {Status == true} + +ReportDetailsBoolean(Status) = "Requirement not met" if {Status == false} + +ReportDetailsArray(Array, String) = Description(Format(Array), String, "") + +# Set to the maximum number of array items to be +# printed in the report details section +ReportArrayMaxCount := 20 + +ReportFullDetailsArray(Array, String) = Details { + count(Array) == 0 + Details := ReportDetailsArray(Array, String) +} + +ReportFullDetailsArray(Array, String) = Details { + count(Array) > 0 + count(Array) <= ReportArrayMaxCount + Details := Description(Format(Array), concat(":
", [String, concat(", ", Array)]), "") +} + +ReportFullDetailsArray(Array, String) = Details { + count(Array) > ReportArrayMaxCount + List := [ x | x := Array[_] ] + + TruncationWarning := "...
Note: The list of matching items has been truncated. Full details are available in the JSON results." + TruncatedList := concat(", ", array.slice(List, 0, ReportArrayMaxCount)) + Details := Description(Format(Array), concat(":
", [String, TruncatedList]), TruncationWarning) +} + +################ +# The report formatting functions below are for policies that check the required Azure AD Premium P2 license # +################ +Aad2P2Licenses[ServicePlan.ServicePlanId] { + ServicePlan = input.service_plans[_] + ServicePlan.ServicePlanName == "AAD_PREMIUM_P2" +} + +P2WarningString := "**NOTE: Your tenant does not have an Azure AD Premium P2 license, which is required for this feature**" + +ReportDetailsArrayLicenseWarning(Array, String) = Description if { + count(Aad2P2Licenses) > 0 + Description := ReportFullDetailsArray(Array, String) +} + +ReportDetailsArrayLicenseWarning(Array, String) = Description if { + count(Aad2P2Licenses) == 0 + Description := P2WarningString +} + +ReportDetailsBooleanLicenseWarning(Status) = Description if { + count(Aad2P2Licenses) > 0 + Status == true + Description := "Requirement met" +} + +ReportDetailsBooleanLicenseWarning(Status) = Description if { + count(Aad2P2Licenses) > 0 + Status == false + Description := "Requirement not met" +} + +ReportDetailsBooleanLicenseWarning(Status) = Description if { + count(Aad2P2Licenses) == 0 + Description := P2WarningString +} + +################ +# Baseline 2.1 # +################ + +# +# Baseline 2.1: Policy 1 +#-- +Policies2_1[Cap.DisplayName] { + Cap := input.conditional_access_policies[_] + # Filter: only include policies that meet all the requirements + "All" in Cap.Conditions.Users.IncludeUsers + "All" in Cap.Conditions.Applications.IncludeApplications + "other" in Cap.Conditions.ClientAppTypes + "exchangeActiveSync" in Cap.Conditions.ClientAppTypes + "block" in Cap.GrantControls.BuiltInControls + Cap.State == "enabled" +} + +tests[{ + "Requirement" : "Legacy authentication SHALL be blocked", + "Control" : "AAD 2.1", + "Criticality" : "Shall", + "Commandlet" : "Get-MgIdentityConditionalAccessPolicy", + "ActualValue" : Policies2_1, + "ReportDetails" : ReportFullDetailsArray(Policies2_1, DescriptionString), + "RequirementMet" : count(Policies2_1) > 0 +}] { + DescriptionString := "conditional access policy(s) found that meet(s) all requirements" + true +} +#-- + + +################ +# Baseline 2.2 # +################ + +# +# Baseline 2.2: Policy 1 +#-- +Policies2_2[Cap.DisplayName] { + Cap := input.conditional_access_policies[_] + # Filter: only include policies that meet all the requirements + "All" in Cap.Conditions.Users.IncludeUsers + "All" in Cap.Conditions.Applications.IncludeApplications + "high" in Cap.Conditions.UserRiskLevels + "block" in Cap.GrantControls.BuiltInControls + Cap.State == "enabled" +} + +tests[{ + "Requirement" : "Users detected as high risk SHALL be blocked", + "Control" : "AAD 2.2", + "Criticality" : "Shall", + "Commandlet" : "Get-MgIdentityConditionalAccessPolicy", + "ActualValue" : Policies2_2, + "ReportDetails" : ReportDetailsArrayLicenseWarning(Policies2_2, DescriptionString), + "RequirementMet" : Status +}] { + DescriptionString := "conditional access policy(s) found that meet(s) all requirements" + Status := count(Policies2_2) > 0 +} +#-- + +# +# Baseline 2.2: Policy 2 +#-- +# At this time we are unable to test for X because of Y +tests[{ + "Requirement" : "A notification SHOULD be sent to the administrator when high-risk users are detected", + "Control" : "AAD 2.2", + "Criticality" : "Should/Not-Implemented", + "Commandlet" : "", + "ActualValue" : [], + "ReportDetails" : "Currently cannot be checked automatically. See Azure Active Directory Secure Configuration Baseline policy 2.2 for instructions on manual check", + "RequirementMet" : false +}] { + true +} +#-- + + +################ +# Baseline 2.3 # +################ + +# +# Baseline 2.3: Policy 1 +#-- +Policies2_3[Cap.DisplayName] { + Cap := input.conditional_access_policies[_] + # Filter: only include policies that meet all the requirements + "All" in Cap.Conditions.Users.IncludeUsers + "All" in Cap.Conditions.Applications.IncludeApplications + "high" in Cap.Conditions.SignInRiskLevels + "block" in Cap.GrantControls.BuiltInControls + Cap.State == "enabled" +} + +tests[{ + "Requirement" : "Sign-ins detected as high risk SHALL be blocked", + "Control" : "AAD 2.3", + "Criticality" : "Shall", + "Commandlet" : "Get-MgIdentityConditionalAccessPolicy", + "ActualValue" : Policies2_3, + "ReportDetails" : ReportDetailsArrayLicenseWarning(Policies2_3, DescriptionString), + "RequirementMet" : Status +}] { + DescriptionString := "conditional access policy(s) found that meet(s) all requirements" + Status := count(Policies2_3) > 0 +} +#-- + + +################ +# Baseline 2.4 # +################ + +# +# Baseline 2.4: Policy 1 +#-- +Policies2_4_1[Cap.DisplayName] { + Cap := input.conditional_access_policies[_] + # Filter: only include policies that meet all the requirements + "All" in Cap.Conditions.Users.IncludeUsers + "All" in Cap.Conditions.Applications.IncludeApplications + "mfa" in Cap.GrantControls.BuiltInControls + Cap.State == "enabled" +} + +tests[{ + "Requirement" : "MFA SHALL be required for all users", + "Control" : "AAD 2.4", + "Criticality" : "Shall", + "Commandlet" : "Get-MgIdentityConditionalAccessPolicy", + "ActualValue" : Policies2_4_1, + "ReportDetails" : ReportDetailsArray(Policies2_4_1, DescriptionString), + "RequirementMet" : count(Policies2_4_1) > 0 +}]{ + DescriptionString := "conditional access policy(s) found that meet(s) all requirements.
Note: Policy exclusions and additional policy conditions may still 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." + true +} +#-- + +# +# Baseline 2.4: Policy 2 +#-- +# At this time we are unable to fully test for MFA due to conflicting and multiple ways to configure authentication methods +# Awaiting API changes and feature updates from Microsoft for automated checking +tests[{ + "Requirement" : "Phishing-resistant MFA SHALL be used for all users", + "Control" : "AAD 2.4", + "Criticality" : "Shall/Not-Implemented", + "Commandlet" : "", + "ActualValue" : [], + "ReportDetails" : "Currently cannot be checked automatically. See Azure Active Directory Secure Configuration Baseline policy 2.4 for instructions on manual check", + "RequirementMet" : false +}] { + true +} +#-- + +# +# Baseline 2.4: Policy 3 +#-- +# At this time we are unable to test for all users due to conflicting and multiple ways to configure authentication methods +# Awaiting API changes and feature updates from Microsoft for automated checking +tests[{ + "Requirement" : "If phishing-resistant MFA cannot be used, an MFA method from the list [see AAD baseline 2.4] SHALL be used in the interim", + "Control" : "AAD 2.4", + "Criticality" : "Shall/Not-Implemented", + "Commandlet" : "", + "ActualValue" : [], + "ReportDetails" : "Currently cannot be checked automatically. See Azure Active Directory Secure Configuration Baseline policy 2.4 for instructions on manual check", + "RequirementMet" : false +}] { + true +} +#-- + +# +# Baseline 2.4: Policy 4 +#-- +# At this time we are unable to test for SMS/Voice settings due to lack of API to validate +# Awaiting API changes and feature updates from Microsoft for automated checking +tests[{ + "Requirement" : "SMS or Voice as the MFA method SHALL NOT be used", + "Control" : "AAD 2.4", + "Criticality" : "Shall/Not-Implemented", + "Commandlet" : "", + "ActualValue" : [], + "ReportDetails" : "Currently cannot be checked automatically. See Azure Active Directory Secure Configuration Baseline policy 2.4 for instructions on manual check", + "RequirementMet" : false +}] { + true +} +#-- + + +################ +# Baseline 2.5 # +################ + +# +# Baseline 2.5: Policy 1 +#-- +# At this time we are unable to test for log collection until we integrate Azure Powershell capabilities +tests[{ + "Requirement" : "The following critical logs SHALL be sent at a minimum: AuditLogs, SignInLogs, RiskyUsers, UserRiskEvents, NonInteractiveUserSignInLogs, ServicePrincipalSignInLogs, ADFSSignInLogs, RiskyServicePrincipals, ServicePrincipalRiskEvents", + "Control" : "AAD 2.5", + "Criticality" : "Shall/Not-Implemented", + "Commandlet" : "", + "ActualValue" : [], + "ReportDetails" : "Currently cannot be checked automatically. See Azure Active Directory Secure Configuration Baseline policy 2.5 for instructions on manual check", + "RequirementMet" : false +}] { + true +} +#-- + +# +# Baseline 2.5: Policy 2 +#-- +# At this time we are unable to test for X because of Y +tests[{ + "Requirement" : "The logs SHALL be sent to the agency's SOC for monitoring", + "Control" : "AAD 2.5", + "Criticality" : "Shall/Not-Implemented", + "Commandlet" : "", + "ActualValue" : [], + "ReportDetails" : "Currently cannot be checked automatically. See Azure Active Directory Secure Configuration Baseline policy 2.5 for instructions on manual check", + "RequirementMet" : false +}] { + true +} +#-- + + +################ +# Baseline 2.6 # +################ + +# +# Baseline 2.6: Policy 1 +#-- +tests[{ + "Requirement" : "Only administrators SHALL be allowed to register third-party applications", + "Control" : "AAD 2.6", + "Criticality" : "Shall", + "Commandlet" : "Get-MgPolicyAuthorizationPolicy", + "ActualValue" : AllowedCreate, + "ReportDetails" : ReportDetailsBoolean(Status), + "RequirementMet" : Status +}] { + AllowedCreate := input.authorization_policies.DefaultUserRolePermissions.AllowedToCreateApps + Status := AllowedCreate == false +} +#-- + + +################ +# Baseline 2.7 # +################ + +# +# Baseline 2.7: Policy 1 +#-- +tests[{ + "Requirement" : "Only administrators SHALL be allowed to consent to third-party applications", + "Control" : "AAD 2.7", + "Criticality" : "Shall", + "Commandlet" : "Get-MgPolicyAuthorizationPolicy", + "ActualValue" : ListValues, + "ReportDetails" : ReportDetailsBoolean(Status), + "RequirementMet" : Status +}] { + UserRolePermissionGrantList := input.authorization_policies.PermissionGrantPolicyIdsAssignedToDefaultUserRole + ListValues := concat("", ["[", concat(",", UserRolePermissionGrantList), "]"]) + Status := count(UserRolePermissionGrantList) == 0 +} +#-- + +# +# Baseline 2.7: Policy 2 +#-- +tests[{ + "Requirement" : "An admin consent workflow SHALL be configured", + "Control" : "AAD 2.7", + "Criticality" : "Shall", + "Commandlet" : "Get-MgPolicyAdminConsentRequestPolicy", + "ActualValue" : Enabled, + "ReportDetails" : ReportDetailsBoolean(Status), + "RequirementMet" : Status +}] { + Enabled := input.admin_consent_policies.IsEnabled + Status := Enabled == true +} +#-- + +# +# Baseline 2.7: Policy 3 +#-- +tests[{ + "Requirement" : "Group owners SHALL NOT be allowed to consent to third-party applications", + "Control" : "AAD 2.7", + "Criticality" : "Shall", + "Commandlet" : "Get-MgDirectorySetting", + "ActualValue" : Setting.Value, + "ReportDetails" : ReportDetailsBoolean(Status), + "RequirementMet" : Status +}] { + Setting := input.directory_settings[_].Values[_] + Setting.Name == "EnableGroupSpecificConsent" + Status := Setting.Value == "false" +} +#-- + + +################ +# Baseline 2.8 # +################ + +# +# Baseline 2.8: Policy 1 +#-- +# At this time we are unable to test for X because of Y +tests[{ + "Requirement" : "User passwords SHALL NOT expire", + "Control" : "AAD 2.8", + "Criticality" : "Shall/Not-Implemented", + "Commandlet" : "", + "ActualValue" : [], + "ReportDetails" : "Currently cannot be checked automatically. See Azure Active Directory Secure Configuration Baseline policy 2.8 for instructions on manual check", + "RequirementMet" : false +}] { + true +} +#-- + + +################ +# Baseline 2.9 # +################ + +# +# Baseline 2.9: Policy 1 +#-- +Policies2_9[Cap.DisplayName] { + Cap := input.conditional_access_policies[_] + # Filter: only include policies that meet all the requirements + "All" in Cap.Conditions.Users.IncludeUsers + "All" in Cap.Conditions.Applications.IncludeApplications + Cap.SessionControls.SignInFrequency.IsEnabled == true + Cap.SessionControls.SignInFrequency.Type == "hours" + Cap.SessionControls.SignInFrequency.Value == 12 + Cap.State == "enabled" +} + +tests[{ + "Requirement" : "Sign-in frequency SHALL be configured to 12 hours", + "Control" : "AAD 2.9", + "Criticality" : "Shall", + "Commandlet" : "Get-MgIdentityConditionalAccessPolicy", + "ActualValue" : Policies2_9, + "ReportDetails" : ReportFullDetailsArray(Policies2_9, DescriptionString), + "RequirementMet" : count(Policies2_9) > 0 +}] { + DescriptionString := "conditional access policy(s) found that meet(s) all requirements" + true +} +#-- + + +################# +# Baseline 2.10 # +################# + +# +# Baseline 2.10: Policy 1 +#-- +Policies2_10[Cap.DisplayName] { + Cap := input.conditional_access_policies[_] + # Filter: only include policies that meet all the requirements + "All" in Cap.Conditions.Users.IncludeUsers + "All" in Cap.Conditions.Applications.IncludeApplications + Cap.SessionControls.PersistentBrowser.IsEnabled == true + Cap.SessionControls.PersistentBrowser.Mode == "never" + Cap.State == "enabled" +} + +tests[{ + "Requirement" : "Browser sessions SHALL not be persistent", + "Control" : "AAD 2.10", + "Criticality" : "Shall", + "Commandlet" : "Get-MgIdentityConditionalAccessPolicy", + "ActualValue" : Policies2_10, + "ReportDetails" : ReportFullDetailsArray(Policies2_10, DescriptionString), + "RequirementMet" : count(Policies2_10) > 0 +}] { + DescriptionString := "conditional access policy(s) found that meet(s) all requirements" + true +} +#-- + + +################# +# Baseline 2.11 # +################# + +# +# Baseline 2.11: Policy 1 +#-- +GlobalAdmins[User.DisplayName] { + some id + User := input.privileged_users[id] + "Global Administrator" in User.roles +} + +tests[{ + "Requirement" : "A minimum of two users and a maximum of four users SHALL be provisioned with the Global Administrator role", + "Control" : "AAD 2.11", + "Criticality" : "Shall", + "Commandlet" : "Get-MgDirectoryRoleMember", + "ActualValue" : GlobalAdmins, + "ReportDetails" : ReportFullDetailsArray(GlobalAdmins, DescriptionString), + "RequirementMet" : Status +}] { + DescriptionString := "global admin(s) found" + Conditions := [count(GlobalAdmins) < 5, count(GlobalAdmins) >= 2] + Status := count([Condition | Condition = Conditions[_]; Condition == false]) == 0 +} +#-- + + +################# +# Baseline 2.12 # +################# + +# +# Baseline 2.12: Policy 1 +#-- +FederatedAdmins[User.DisplayName] { + some id + User := input.privileged_users[id] + not is_null(User.OnPremisesImmutableId) +} + +tests[{ + "Requirement" : "Users that need to be assigned to highly privileged Azure AD roles SHALL be provisioned cloud-only accounts that are separate from the on-premises directory or other federated identity providers", + "Control" : "AAD 2.12", + "Criticality" : "Shall", + "Commandlet" : "Get-MgDirectoryRoleMember", + "ActualValue" : AdminNames, + "ReportDetails" : ReportFullDetailsArray(FederatedAdmins, DescriptionString), + "RequirementMet" : Status +}] { + DescriptionString := "admin(s) that are not cloud-only found" + Status := count(FederatedAdmins) == 0 + AdminNames := concat(", ", FederatedAdmins) +} +#-- + +################# +# Baseline 2.13 # +################# + +# +# Baseline 2.13: Policy 1 +#-- +Policies2_13[Cap.DisplayName] { + Cap := input.conditional_access_policies[_] + PrivRolesSet := { Role.RoleTemplateId | Role = input.privileged_roles[_] } + CondIncludedRolesSet := { Y | Y = Cap.Conditions.Users.IncludeRoles[_] } + MissingRoles := PrivRolesSet - CondIncludedRolesSet + # Filter: only include policies that meet all the requirements + count(MissingRoles) == 0 + "All" in Cap.Conditions.Applications.IncludeApplications + "mfa" in Cap.GrantControls.BuiltInControls + Cap.State == "enabled" +} + +tests[{ + "Requirement" : "MFA SHALL be required for user access to highly privileged roles", + "Control" : "AAD 2.13", + "Criticality" : "Shall", + "Commandlet" : "Get-MgIdentityConditionalAccessPolicy", + "ActualValue" : Policies2_13, + "ReportDetails" : ReportFullDetailsArray(Policies2_13, DescriptionString), + "RequirementMet" : count(Policies2_13) > 0 +}] { + DescriptionString := "conditional access policy(s) found that meet(s) all requirements" + RoleNames := concat(", ", { Role.DisplayName | Role = input.privileged_roles[_] }) +} +#-- + + +################# +# Helper functions for policies 2.14, 2.15, 2.16 +################# + +# DoPIMRoleRulesExist will return true when the JSON privileged_roles.Rules element exists and false when it does not. +# This was created to add special logic for the scenario where the Azure AD premium P2 license is missing and therefore +# the JSON Rules element will not exist in that case because there is no PIM service. +# This is necessary to avoid false negatives when a policy checks for zero instances of a specific condition. +# For example, if a policy checks for count(RolesWithoutLimitedExpirationPeriod) == 0 and that normally means compliant, when a +# tenant does not have the license, a count of 0 does not mean compliant because 0 is the result of not having the Rules element +# in the JSON. +DoPIMRoleRulesExist { + _ = input.privileged_roles[_]["Rules"] +} + +default check_if_role_rules_exist := false +check_if_role_rules_exist := DoPIMRoleRulesExist + +# DoPIMRoleAssignmentsExist will return true when the JSON privileged_roles.Assignments element exists and false when it does not. +DoPIMRoleAssignmentsExist { + _ = input.privileged_roles[_]["Assignments"] +} + +default check_if_role_assignments_exist := false +check_if_role_assignments_exist := DoPIMRoleAssignmentsExist + +################# +# Baseline 2.14 # +################# + +# +# Baseline 2.14: Policy 1 +#-- +RolesWithoutLimitedExpirationPeriod[Role.DisplayName] { + Role := input.privileged_roles[_] + Rule := Role.Rules[_] + RuleMatch := Rule.Id == "Expiration_Admin_Assignment" + ExpirationNotRequired := Rule.AdditionalProperties.isExpirationRequired == false + MaximumDurationCorrect := Rule.AdditionalProperties.maximumDuration == "P15D" + + # Role policy does not require assignment expiration + Conditions1 := [RuleMatch == true, ExpirationNotRequired == true] + Case1 := count([Condition | Condition = Conditions1[_]; Condition == false]) == 0 + + # Role policy requires assignment expiration, but maximum duration is not 15 days + Conditions2 := [RuleMatch == true, ExpirationNotRequired == false, MaximumDurationCorrect == false] + Case2 := count([Condition | Condition = Conditions2[_]; Condition == false]) == 0 + + # Filter: only include rules that meet one of the two cases + Conditions := [Case1, Case2] + count([Condition | Condition = Conditions[_]; Condition == true]) > 0 +} + +tests[{ + "Requirement" : "Permanent active role assignments SHALL NOT be allowed for highly privileged roles. Active assignments SHALL have an expiration period.", + "Control" : "AAD 2.14", + "Criticality" : "Shall", + "Commandlet" : "Get-MgRoleManagementDirectoryRoleAssignmentScheduleInstance", + "ActualValue" : RolesWithoutLimitedExpirationPeriod, + "ReportDetails" : ReportDetailsArrayLicenseWarning(RolesWithoutLimitedExpirationPeriod, DescriptionString), + "RequirementMet" : Status +}] { + DescriptionString := "role(s) configured to allow permanent active assignment or expiration period too long" + Conditions := [count(RolesWithoutLimitedExpirationPeriod) == 0, check_if_role_rules_exist] + Status := count([Condition | Condition = Conditions[_]; Condition == false]) == 0 +} +#-- + +# +# Baseline 2.14: Policy 2 +#-- +RolesAssignedOutsidePim[Role.DisplayName] { + Role := input.privileged_roles[_] + NoStartAssignments := { is_null(X.StartDateTime) | X = Role.Assignments[_] } + + count([Condition | Condition = NoStartAssignments[_]; Condition == true]) > 0 +} + +tests[{ + "Requirement" : "Provisioning of users to highly privileged roles SHALL NOT occur outside of a PAM system, such as the Azure AD PIM service, because this bypasses the controls the PAM system provides", + "Control" : "AAD 2.14", + "Criticality" : "Shall", + "Commandlet" : "Get-MgRoleManagementDirectoryRoleAssignmentScheduleInstance", + "ActualValue" : RolesAssignedOutsidePim, + "ReportDetails" : ReportDetailsArrayLicenseWarning(RolesAssignedOutsidePim, DescriptionString), + "RequirementMet" : Status +}] { + DescriptionString := "role(s) assigned to users outside of PIM" + Conditions := [count(RolesAssignedOutsidePim) == 0, check_if_role_rules_exist] + Status := count([Condition | Condition = Conditions[_]; Condition == false]) == 0 +} +#-- + + +################# +# Baseline 2.15 # +################# + +# +# Baseline 2.15: Policy 1 +#-- +RolesWithoutApprovalRequired[RoleName] { + Role := input.privileged_roles[_] + RoleName := Role.DisplayName + Rule := Role.Rules[_] + # Filter: only include policies that meet all the requirements + Rule.Id == "Approval_EndUser_Assignment" + Rule.AdditionalProperties.setting.isApprovalRequired == false +} + +tests[{ + "Requirement" : "Activation of highly privileged roles SHOULD require approval", + "Control" : "AAD 2.15", + "Criticality" : "Should", + "Commandlet" : "Get-MgRoleManagementDirectoryRoleAssignmentScheduleInstance", + "ActualValue" : RolesWithoutApprovalRequired, + "ReportDetails" : ReportDetailsArrayLicenseWarning(RolesWithoutApprovalRequired, DescriptionString), + "RequirementMet" : Status +}] { + DescriptionString := "role(s) that do not require approval to activate found" + Conditions := [count(RolesWithoutApprovalRequired) == 0, check_if_role_rules_exist] + Status := count([Condition | Condition = Conditions[_]; Condition == false]) == 0 +} +#-- + + +################# +# Baseline 2.16 # +################# + +# +# Baseline 2.16: Policy 1 +#-- +RolesWithoutActiveAssignmentAlerts[RoleName] { + Role := input.privileged_roles[_] + RoleName := Role.DisplayName + Rule := Role.Rules[_] + # Filter: only include policies that meet all the requirements + Rule.Id == "Notification_Admin_Admin_Assignment" + count(Rule.AdditionalProperties.notificationRecipients) == 0 +} + +RolesWithoutEligibleAssignmentAlerts[RoleName] { + Role := input.privileged_roles[_] + RoleName := Role.DisplayName + Rule := Role.Rules[_] + # Filter: only include policies that meet all the requirements + Rule.Id == "Notification_Admin_Admin_Eligibility" + count(Rule.AdditionalProperties.notificationRecipients) == 0 +} + +tests[{ + "Requirement" : "Eligible and Active highly privileged role assignments SHALL trigger an alert", + "Control" : "AAD 2.16", + "Criticality" : "Shall", + "Commandlet" : "Get-MgRoleManagementDirectoryRoleAssignmentScheduleInstance", + "ActualValue" : RolesWithoutAssignmentAlerts, + "ReportDetails" : ReportDetailsArrayLicenseWarning(RolesWithoutAssignmentAlerts, DescriptionString), + "RequirementMet" : Status +}] { + DescriptionString := "role(s) without notification e-mail configured for role assignments found" + RolesWithoutAssignmentAlerts := RolesWithoutActiveAssignmentAlerts | RolesWithoutEligibleAssignmentAlerts + Conditions := [count(RolesWithoutAssignmentAlerts) == 0, check_if_role_rules_exist] + Status := count([Condition | Condition = Conditions[_]; Condition == false]) == 0 +} +#-- + +# +# Baseline 2.16: Policy 2 +#-- +AdminsWithoutActivationAlert[RoleName] { + Role := input.privileged_roles[_] + RoleName := Role.DisplayName + Rule := Role.Rules[_] + # Filter: only include policies that meet all the requirements + Rule.Id == "Notification_Admin_EndUser_Assignment" + Rule.AdditionalProperties.notificationType == "Email" + count(Rule.AdditionalProperties.notificationRecipients) == 0 +} + +tests[{ + "Requirement" : "User activation of the Global Administrator role SHALL trigger an alert", + "Control" : "AAD 2.16", + "Criticality" : "Shall", + "Commandlet" : "Get-MgRoleManagementDirectoryRoleAssignmentScheduleInstance", + "ActualValue" : AdminsWithoutActivationAlert, + "ReportDetails" : ReportDetailsBooleanLicenseWarning(Status), + "RequirementMet" : Status +}] { + GlobalAdminNotMonitored := "Global Administrator" in AdminsWithoutActivationAlert + Conditions := [GlobalAdminNotMonitored == false, check_if_role_rules_exist] + Status := count([Condition | Condition = Conditions[_]; Condition == false]) == 0 +} +#-- + +# +# Baseline 2.16: Policy 3 +#-- +tests[{ + "Requirement" : "User activation of other highly privileged roles SHOULD trigger an alert", + "Control" : "AAD 2.16", + "Criticality" : "Should", + "Commandlet" : "Get-MgRoleManagementDirectoryRoleAssignmentScheduleInstance", + "ActualValue" : NonGlobalAdminsWithoutActivationAlert, + "ReportDetails" : ReportDetailsArrayLicenseWarning(NonGlobalAdminsWithoutActivationAlert, DescriptionString), + "RequirementMet" : Status +}] { + DescriptionString := "role(s) without notification e-mail configured for role activations found" + NonGlobalAdminsWithoutActivationAlert = AdminsWithoutActivationAlert - {"Global Administrator"} + Conditions := [count(NonGlobalAdminsWithoutActivationAlert) == 0, check_if_role_rules_exist] + Status := count([Condition | Condition = Conditions[_]; Condition == false]) == 0 +} +#-- + + +################# +# Baseline 2.17 # +################# + +# +# Baseline 2.17: Policy 1 +#-- +Policies2_17[Cap.DisplayName] { + Cap := input.conditional_access_policies[_] + CompliantDevice := "compliantDevice" in Cap.GrantControls.BuiltInControls + HybridJoin := "domainJoinedDevice" in Cap.GrantControls.BuiltInControls + Conditions := [CompliantDevice, HybridJoin] + # Filter: only include policies that meet all the requirements + "All" in Cap.Conditions.Users.IncludeUsers + "All" in Cap.Conditions.Applications.IncludeApplications + count([Condition | Condition = Conditions[_]; Condition == true]) > 0 + Cap.State == "enabled" +} + +tests[{ + "Requirement" : "Managed devices SHOULD be required for authentication", + "Control" : "AAD 2.17", + "Criticality" : "Should", + "Commandlet" : "Get-MgIdentityConditionalAccessPolicy", + "ActualValue" : Policies2_17, + "ReportDetails" : ReportFullDetailsArray(Policies2_17, DescriptionString), + "RequirementMet" : count(Policies2_17) > 0 +}] { + DescriptionString := "conditional access policy(s) found that meet(s) all requirements" + true +} +#-- + + +################# +# Baseline 2.18 # +################# + +# +# Baseline 2.18: Policy 1 +#-- +tests[{ + "Requirement" : "Only users with the Guest Inviter role SHOULD be able to invite guest users", + "Control" : "AAD 2.18", + "Criticality" : "Should", + "Commandlet" : "Get-MgPolicyAuthorizationPolicy", + "ActualValue" : AllowedCreate, + "ReportDetails" : ReportDetailsBoolean(Status), + "RequirementMet" : Status +}] { + AllowedCreate := input.authorization_policies.AllowInvitesFrom + Status := AllowedCreate == "adminsAndGuestInviters" +} +#-- + +# +# Baseline 2.18: Policy 2 +#-- +# At this time we are unable to test for X because of Y +tests[{ + "Requirement" : "Guest invites SHOULD only be allowed to specific external domains that have been authorized by the agency for legitimate business purposes", + "Control" : "AAD 2.18", + "Criticality" : "Should/Not-Implemented", + "Commandlet" : "", + "ActualValue" : [], + "ReportDetails" : "Currently cannot be checked automatically. See Azure Active Directory Secure Configuration Baseline policy 2.18 for instructions on manual check", + "RequirementMet" : false +}] { + true +} +#-- + +# +# Baseline 2.18: Policy 3 +#-- +LevelAsString(Id) := "Restricted access" if {Id == "2af84b1e-32c8-42b7-82bc-daa82404023b"} +LevelAsString(Id) := "Limited access" if {Id == "10dae51f-b6af-4016-8d66-8c2a99b929b3"} +LevelAsString(Id) := "Same as member users" if {Id == "a0b1b346-4d3e-4e8b-98f8-753987be4970"} +LevelAsString(Id) := "Unknown" if {not Id in ["2af84b1e-32c8-42b7-82bc-daa82404023b", "10dae51f-b6af-4016-8d66-8c2a99b929b3", "a0b1b346-4d3e-4e8b-98f8-753987be4970"]} + +# must hardcode the ID. See +# https://docs.microsoft.com/en-us/azure/active-directory/enterprise-users/users-restrict-guest-permissions +tests[{ + "Requirement" : "Guest users SHOULD have limited access to Azure AD directory objects", + "Control" : "AAD 2.18", + "Criticality" : "Should", + "Commandlet" : "Get-MgPolicyAuthorizationPolicy", + "ActualValue" : concat(" : ", ["Role ID", ExtractedRoleId ]), + "ReportDetails" : ReportDetail, + "RequirementMet" : Status +}] { + ExtractedRoleId := input.authorization_policies.GuestUserRoleId + ReportDetail := concat("", ["Permission level set to \"", LevelAsString(ExtractedRoleId), "\""]) + Status := ExtractedRoleId in ["10dae51f-b6af-4016-8d66-8c2a99b929b3", "2af84b1e-32c8-42b7-82bc-daa82404023b"] +} +#-- \ No newline at end of file diff --git a/Rego/DefenderConfig.rego b/Rego/DefenderConfig.rego new file mode 100644 index 0000000000..57c0fefe5d --- /dev/null +++ b/Rego/DefenderConfig.rego @@ -0,0 +1,1944 @@ +package defender +import future.keywords + +## Report details menu +# +# If you simply want a boolean "Requirement met" / "Requirement not met" +# just call ReportDetails(Status) and leave it at that. +# +# If you want to customize the error message, wrap the ReportDetails call +# inside CustomizeError, like so: +# CustomizeError(ReportDetails(Status), "Custom error message") +# +# If you want to customize the error message with details about an array, +# generate the custom error message using GenerateArrayString, for example: +# CustomizeError(ReportDetails(Status), GenerateArrayString(BadPolicies, "bad policies found:")) +# +# If the setting in question requires a defender license, +# wrap the details string inside ApplyLicenseWarning, like so: +# ApplyLicenseWarning(ReportDetails(Status)) +# +# These functions can be nested. For example: +# ApplyLicenseWarning(CustomizeError(ReportDetails(Status), "Custom error message")) +# +## +ReportDetails(Status) := "Requirement met" if { + Status == true +} + +ReportDetails(Status) := "Requirement not met" if { + Status == false +} + +GenerateArrayString(Array, CustomString) := Output if { + # Example usage and output: + # GenerateArrayString([1,2], "numbers found:") -> + # 2 numbers found: 1, 2 + Length := format_int(count(Array), 10) + ArrayString := concat(", ", Array) + Output := trim(concat(" ", [Length, concat(" ", [CustomString, ArrayString])]), " ") +} + +CustomizeError(Message, CustomString) := Message if { + # If the message reports success, don't apply the custom + # error message + Message == ReportDetails(true) +} + +CustomizeError(Message, CustomString) := CustomString if { + # If the message does not report success, apply the custom + # error message + Message != ReportDetails(true) +} + +ApplyLicenseWarning(Message) := Message if { + # If a defender license is present, don't apply the warning + # and leave the message unchanged + input.defender_license == true +} + +ApplyLicenseWarning(Message) := concat("", [ReportDetails(false), LicenseWarning]) if { + # If a defender license is not present, assume failure and + # replace the message with the warning + input.defender_license == false + LicenseWarning := " **NOTE: Your tenant appears to not have a license for Defender for Microsoft Defender for Office 365, which is required for this feature.**" +} + +################ +# Baseline 2.1 # +################ + +# +# Baseline 2.1: Policy 1 +#-- +StandardPresetSecurityPolicy[Rules.State] { + Rules := input.protection_policy_rules[_] + Rules.Identity == "Standard Preset Security Policy" +} + +tests[{ + "Requirement" : "Standard Preset security profiles SHOULD NOT be used", + "Control" : "Defender 2.1", + "Criticality" : "Should", + "Commandlet" : "Get-EOPProtectionPolicyRule", + "ActualValue" : Policy, + "ReportDetails" : CustomizeError(ReportDetails(Status), ErrorMessage), + "RequirementMet" : Status +}] { + Policy := StandardPresetSecurityPolicy + ErrorMessage := "The Standard Preset Security Policy is present and not disabled" + Conditions := [count(Policy) == 0, "Disabled" in Policy] + Status := count([Condition | Condition = Conditions[_]; Condition == true]) > 0 +} +#-- + +# +# Baseline 2.1: Policy 2 +#-- +StrictPresetSecurityPolicy[Rules.State] { + Rules := input.protection_policy_rules[_] + Rules.Identity == "Strict Preset Security Policy" +} + +tests[{ + "Requirement" : "Strict Preset security profiles SHOULD NOT be used", + "Control" : "Defender 2.1", + "Criticality" : "Should", + "Commandlet" : "Get-EOPProtectionPolicyRule", + "ActualValue" : Policy, + "ReportDetails" : CustomizeError(ReportDetails(Status), ErrorMessage), + "RequirementMet" : Status +}] { + Policy := StrictPresetSecurityPolicy + ErrorMessage := "The Strict Preset Security Policy is present and not disabled" + Conditions := [count(Policy) == 0, "Disabled" in Policy] + Status := count([Condition | Condition = Conditions[_]; Condition == true]) > 0 +} +#-- + + +################ +# Baseline 2.2 # +################ + +# Determine the set of rules that pertain to SSNs, ITINs, or credit card numbers. +# Used in multiple bullet points below +SensitiveRules[{ + "Name" : Rules.Name, + "ParentPolicyName" : Rules.ParentPolicyName, + "BlockAccess" : Rules.BlockAccess, + "BlockAccessScope" : Rules.BlockAccessScope, + "NotifyUser" : Rules.NotifyUser, + "NotifyUserType" : Rules.NotifyUserType, + "ContentNames" : ContentNames +}] { + Rules := input.dlp_compliance_rules[_] + Rules.Disabled == false + ContentNames := [Content.name | Content = Rules.ContentContainsSensitiveInformation[_]] + Conditions := [ "U.S. Social Security Number (SSN)" in ContentNames, + "U.S. Individual Taxpayer Identification Number (ITIN)" in ContentNames, + "Credit Card Number" in ContentNames] + count([Condition | Condition = Conditions[_]; Condition == true]) > 0 +} + +# +# Baseline 2.2: Policy 1 +#-- +# Step 1: Ensure that there is coverage for SSNs, ITINs, and credit cards +SSNRules[Rule.Name] { + Rule := SensitiveRules[_] + "U.S. Social Security Number (SSN)" in Rule.ContentNames +} + +ITINRules[Rule.Name] { + Rule := SensitiveRules[_] + "U.S. Individual Taxpayer Identification Number (ITIN)" in Rule.ContentNames +} + +CardRules[ Rule.Name] { + Rule := SensitiveRules[_] + "Credit Card Number" in Rule.ContentNames +} + +tests[{ + "Requirement" : "A custom policy SHALL be configured to protect PII and sensitive information, as defined by the agency: U.S. Social Security Number (SSN)", + "Control" : "Defender 2.2", + "Criticality" : "Shall", + "Commandlet" : "Get-DLPComplianceRule", + "ActualValue" : Rules, + "ReportDetails" : CustomizeError(ReportDetails(Status), ErrorMessage), + "RequirementMet" : Status +}] { + Rules := SSNRules + ErrorMessage := "No matching rule found for U.S. Social Security Number (SSN)" + Status := count(Rules) > 0 +} + +tests[{ + "Requirement" : "A custom policy SHALL be configured to protect PII and sensitive information, as defined by the agency: U.S. Individual Taxpayer Identification Number (ITIN)", + "Control" : "Defender 2.2", + "Criticality" : "Shall", + "Commandlet" : "Get-DLPComplianceRule", + "ActualValue" : Rules, + "ReportDetails" : CustomizeError(ReportDetails(Status), ErrorMessage), + "RequirementMet" : Status +}] { + Rules := ITINRules + ErrorMessage := "No matching rule found for U.S. Individual Taxpayer Identification Number (ITIN)" + Status := count(Rules) > 0 +} + +tests[{ + "Requirement" : "A custom policy SHALL be configured to protect PII and sensitive information, as defined by the agency: Credit Card Number", + "Control" : "Defender 2.2", + "Criticality" : "Shall", + "Commandlet" : "Get-DLPComplianceRule", + "ActualValue" : Rules, + "ReportDetails" : CustomizeError(ReportDetails(Status), ErrorMessage), + "RequirementMet" : Status +}] { + Rules := CardRules + ErrorMessage := "No matching rule found for Credit Card Number" + Status := count(Rules) > 0 +} +#-- + +# +# Baseline 2.2: Policy 2 +#-- +# Step 2: determine the set of sensitive policies that apply to EXO, Teams, etc. +ExchangePolicies[{ + "Name" : Policy.Name, + "Locations" : Policy.ExchangeLocation, + "Workload" : Policy.Workload +}] { + SensitivePolicies := {Rule.ParentPolicyName | Rule = SensitiveRules[_]} + Policy := input.dlp_compliance_policies[_] + Policy.Name in SensitivePolicies + "All" in Policy.ExchangeLocation + contains(Policy.Workload, "Exchange") +} + +SharePointPolicies[{ + "Name" : Policy.Name, + "Locations" : Policy.SharePointLocation, + "Workload" : Policy.Workload +}] { + SensitivePolicies := {Rule.ParentPolicyName | Rule = SensitiveRules[_]} + Policy := input.dlp_compliance_policies[_] + Policy.Name in SensitivePolicies + "All" in Policy.SharePointLocation + contains(Policy.Workload, "SharePoint") +} + +OneDrivePolicies[{ + "Name" : Policy.Name, + "Locations" : Policy.OneDriveLocation, + "Workload" : Policy.Workload +}] { + SensitivePolicies := {Rule.ParentPolicyName | Rule = SensitiveRules[_]} + Policy := input.dlp_compliance_policies[_] + Policy.Name in SensitivePolicies + "All" in Policy.OneDriveLocation + contains(Policy.Workload, "OneDrivePoint") # Is this supposed to be OneDrivePoint or OneDrive? +} + +TeamsPolicies[{ + "Name" : Policy.Name, + "Locations" : Policy.TeamsLocation, + "Workload" : Policy.Workload + }] { + SensitivePolicies := {Rule.ParentPolicyName | Rule = SensitiveRules[_]} + Policy := input.dlp_compliance_policies[_] + Policy.Name in SensitivePolicies + "All" in Policy.TeamsLocation + contains(Policy.Workload, "Teams") +} + +tests[{ + "Requirement" : "The custom policy SHOULD be applied in Exchange", + "Control" : "Defender 2.2", + "Criticality" : "Should", + "Commandlet" : "Get-DLPCompliancePolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), ErrorMessage), + "RequirementMet" : Status +}] { + Policies := ExchangePolicies + ErrorMessage := "No policy found that applies to Exchange." + Status := count(Policies) > 0 +} + +tests[{ + "Requirement" : "The custom policy SHOULD be applied in SharePoint", + "Control" : "Defender 2.2", + "Criticality" : "Should", + "Commandlet" : "Get-DLPCompliancePolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), ErrorMessage), + "RequirementMet" : Status +}] { + Policies := SharePointPolicies + ErrorMessage := "No policy found that applies to SharePoint." + Status := count(Policies) > 0 +} + +tests[{ + "Requirement" : "The custom policy SHOULD be applied in OneDrive", + "Control" : "Defender 2.2", + "Criticality" : "Should", + "Commandlet" : "Get-DLPCompliancePolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), ErrorMessage), + "RequirementMet" : Status +}] { + Policies := OneDrivePolicies + ErrorMessage := "No policy found that applies to OneDrive." + Status := count(Policies) > 0 +} + +tests[{ + "Requirement" : "The custom policy SHOULD be applied in Teams", + "Control" : "Defender 2.2", + "Criticality" : "Should", + "Commandlet" : "Get-DLPCompliancePolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), ErrorMessage), + "RequirementMet" : Status +}] { + Policies := TeamsPolicies + ErrorMessage := "No policy found that applies to Teams." + Status := count(Policies) > 0 +} +#-- + +# +# Baseline 2.2: Policy 3 +#-- +# Step 3: Ensure that the action for the rules is set to block +SensitiveRulesNotBlocking[Rule.Name] { + Rule := SensitiveRules[_] + not Rule.BlockAccess +} + +tests[{ + "Requirement" : "The action for the DLP policy SHOULD be set to block sharing sensitive information with everyone when DLP conditions are met", + "Control" : "Defender 2.2", + "Criticality" : "Should", + "Commandlet" : "get-DLPComplianceRule", + "ActualValue" : Rules, + "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Rules, ErrorMessage)), + "RequirementMet" : Status +}] { + Rules := SensitiveRulesNotBlocking + ErrorMessage := "rule(s) found that do(es) not block access:" + Status := count(Rules) == 0 +} +#-- + +# +# Baseline 2.2: Policy 4 +#-- +# Step 4: ensure that some user is notified in the event of a DLP violation +SensitiveRulesNotNotifying[Rule.Name] { + Rule := SensitiveRules[_] + count(Rule.NotifyUser) == 0 +} + +tests[{ + "Requirement" : "Notifications to inform users and help educate them on the proper use of sensitive information SHOULD be enabled", + "Control" : "Defender 2.2", + "Criticality" : "Should", + "Commandlet" : "get-DLPComplianceRule", + "ActualValue" : Rules, + "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Rules, ErrorMessage)), + "RequirementMet" : Status +}] { + Rules := SensitiveRulesNotNotifying + ErrorMessage := "rule(s) found that do(es) not notify at least one user:" + Status := count(Rules) == 0 +} +#-- + +# +# Baseline 2.2: Policy 5 +#-- +# At this time we are unable to test for X because of Y +tests[{ + "Requirement" : "A list of apps that are not allowed to access files protected by DLP policy SHOULD be defined", + "Control" : "Defender 2.2", + "Criticality" : "Should/Not-Implemented", + "Commandlet" : "", + "ActualValue" : [], + "ReportDetails" : "Currently cannot be checked automatically. See Defender Secure Configuration Baseline policy 2.2 for instructions on manual check", + "RequirementMet" : false +}] { + true +} +#-- + +# +# Baseline 2.2: Policy 6 +#-- +# At this time we are unable to test for X because of Y +tests[{ + "Requirement" : "A list of browsers that are not allowed to access files protected by DLP policy SHOULD be defined", + "Control" : "Defender 2.2", + "Criticality" : "Should/Not-Implemented", + "Commandlet" : "", + "ActualValue" : [], + "ReportDetails" : "Currently cannot be checked automatically. See Defender Secure Configuration Baseline policy 2.2 for instructions on manual check", + "RequirementMet" : false +}] { + true +} +#-- + + +################ +# Baseline 2.3 # +################ + +# +# Baseline 2.3: Policy 1 +#-- +MalwarePoliciesWithoutFileFilter[Policy.Name] { + Policy := input.malware_filter_policies[_] + not Policy.EnableFileFilter +} + +tests[{ + "Requirement" : "The common attachments filter SHALL be enabled in the default anti-malware policy and in all existing policies", + "Control" : "Defender 2.3", + "Criticality" : "Shall", + "Commandlet" : "Get-MalwareFilterPolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), + "RequirementMet" : Status +}] { + Policies := MalwarePoliciesWithoutFileFilter + ErrorMessage := "malware policy(ies) found that do(es) not have the common attachments filter enabled:" + Status := count(Policies) == 0 +} +#-- + +# +# Baseline 2.3: Policy 2 +#-- +# exe +MalwarePoliciesBlockingEXE[Policy.Name] { + Policy := input.malware_filter_policies[_] + Policy.EnableFileFilter + "exe" in Policy.FileTypes +} + +tests[{ + "Requirement" : "Disallowed file types SHALL be determined and set. At a minimum, click-to-run files SHOULD be blocked: exe files", + "Control" : "Defender 2.3", + "Criticality" : "Should", + "Commandlet" : "Get-MalwareFilterPolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), ErrorMessage), + "RequirementMet" : Status +}] { + Policies := MalwarePoliciesBlockingEXE + Status := count(Policies) > 0 + ErrorMessage := "No malware policies found that block .exe files." +} + +# cmd +MalwarePoliciesBlockingCMD[Policy.Name] { + Policy := input.malware_filter_policies[_] + Policy.EnableFileFilter + "cmd" in Policy.FileTypes +} + +tests[{ + "Requirement" : "Disallowed file types SHALL be determined and set. At a minimum, click-to-run files SHOULD be blocked: cmd files", + "Control" : "Defender 2.3", + "Criticality" : "Should", + "Commandlet" : "Get-MalwareFilterPolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), ErrorMessage), + "RequirementMet" : Status +}] { + Policies := MalwarePoliciesBlockingCMD + Status := count(Policies) > 0 + ErrorMessage := "No malware policies found that block .cmd files." +} + +# vbe +MalwarePoliciesBlockingVBE[Policy.Name] { + Policy := input.malware_filter_policies[_] + Policy.EnableFileFilter + "vbe" in Policy.FileTypes +} + +tests[{ + "Requirement" : "Disallowed file types SHALL be determined and set. At a minimum, click-to-run files SHOULD be blocked: vbe files", + "Control" : "Defender 2.3", + "Criticality" : "Should", + "Commandlet" : "Get-MalwareFilterPolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), ErrorMessage), + "RequirementMet" : Status +}] { + Policies := MalwarePoliciesBlockingVBE + Status := count(Policies) > 0 + ErrorMessage := "No malware policies found that block .vbe files." +} +#-- + + +################ +# Baseline 2.4 # +################ + +# +# Baseline 2.4: Policy 1 +#-- +MalwarePoliciesWithoutZAP[Policy.Name] { + Policy := input.malware_filter_policies[_] + not Policy.ZapEnabled +} + +tests[{ + "Requirement" : "Zero-hour Auto Purge (ZAP) for malware SHOULD be enabled in the default anti-malware policy and in all existing custom policies", + "Control" : "Defender 2.4", + "Criticality" : "Should", + "Commandlet" : "Get-MalwareFilterPolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), + "RequirementMet" : Status +}] { + Policies := MalwarePoliciesWithoutZAP + Status := count(Policies) == 0 + ErrorMessage := "malware policy(ies) found without ZAP for malware enabled:" +} +#-- + + +################ +# Baseline 2.5 # +################ + +# +# Baseline 2.5: Policy 1 +#-- +ProtectedUsersPolicies[{ + "Name" : Policy.Name, + "Users" : Policy.TargetedUsersToProtect, + "Action" : Policy.TargetedUserProtectionAction +}] { + Policy := input.anti_phish_policies[_] + Policy.Enabled # filter out the disabled policies + Policy.EnableTargetedUserProtection # filter out the policies that have impersonation protections disabled + count(Policy.TargetedUsersToProtect) > 0 # filter out the policies that don't list any protected users +} + +# assert that at least one of the enabled policies includes protected users +tests[{ + "Requirement" : "User impersonation protection SHOULD be enabled for key agency leaders", + "Control" : "Defender 2.5", + "Criticality" : "Should", + "Commandlet" : "Get-AntiPhishPolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), ErrorMessage), + "RequirementMet" : Status +}] { + Policies := ProtectedUsersPolicies + ErrorMessage := "No users are included for targeted user protection." + Status := count(Policies) > 0 +} +#-- + +# +# Baseline 2.5: Policy 2 +#-- +ProtectedOrgDomainsPolicies[{ + "Name" : Policy.Name, + "OrgDomains" : Policy.EnableOrganizationDomainsProtection, + "Action" : Policy.TargetedDomainProtectionAction +}] { + Policy := input.anti_phish_policies[_] + Policy.Enabled # filter out the disabled policies + Policy.EnableTargetedDomainsProtection # filter out the policies that don't have domain impersonation protection enabled + Policy.EnableOrganizationDomainsProtection # filter out the policies that don't protect org domains +} + +# assert that at least one of the enabled policies includes +# protection for the org's own domains +tests[{ + "Requirement" : "Domain impersonation protection SHOULD be enabled for domains owned by the agency", + "Control" : "Defender 2.5", + "Criticality" : "Should", + "Commandlet" : "Get-AntiPhishPolicy", + "ActualValue" : Policies, + "ReportDetails" : ReportDetails(Status), + "RequirementMet" : Status +}] { + Policies := ProtectedOrgDomainsPolicies + Status := count(Policies) > 0 +} +#-- + +# +# Baseline 2.5: Policy 3 +#-- +ProtectedCustomDomainsPolicies[{ + "Name" : Policy.Name, + "CustomDomains" : Policy.TargetedDomainsToProtect, + "Action" : Policy.TargetedDomainProtectionAction +}] { + Policy := input.anti_phish_policies[_] + Policy.Enabled # filter out the disabled policies + Policy.EnableTargetedDomainsProtection # filter out the policies that don't have domain impersonation protection enabled + count(Policy.TargetedDomainsToProtect) > 0 # filter out the policies that don't list any custom domains +} + +# assert that at least one of the enabled policies includes +# protection for custom domains +tests[{ + "Requirement" : "Domain impersonation protection SHOULD be added for frequent partners", + "Control" : "Defender 2.5", + "Criticality" : "Should", + "Commandlet" : "Get-AntiPhishPolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), ErrorMessage), + "RequirementMet" : Status +}] { + Policies := ProtectedCustomDomainsPolicies + ErrorMessage := "The Custom Domains protection policies: Enabled, EnableTargetedDomainsProtection, and TargetedDomainsToProtect are not set correctly" + Status := count(Policies) > 0 +} +#-- + +# +# Baseline 2.5: Policy 4 +#-- +IntelligenceProtectionPolicies[{ + "Name" : Policy.Name, + "IntelligenceProtection" : Policy.EnableMailboxIntelligenceProtection, + "Action" : Policy.MailboxIntelligenceProtectionAction +}] { + Policy := input.anti_phish_policies[_] + Policy.Enabled # filter out the disabled policies + Policy.EnableMailboxIntelligenceProtection # filter out the policies that don't have intelligence protection enabled +} + +# assert that at least one of the enabled policies includes +# intelligence protection +tests[{ + "Requirement" : "Intelligence for impersonation protection SHALL be enabled", + "Control" : "Defender 2.5", + "Criticality" : "Should", + "Commandlet" : "Get-AntiPhishPolicy", + "ActualValue" : Policies, + "ReportDetails" : ReportDetails(Status), + "RequirementMet" : Status +}] { + Policies := IntelligenceProtectionPolicies + Status := count(Policies) > 0 +} +#-- + +# +# Baseline 2.5: Policy 5 +#-- +# Step 1: Default (SHALL) +tests[{ + "Requirement" : "Message action SHALL be set to quarantine if the message is detected as impersonated: users default policy", + "Control" : "Defender 2.5", + "Criticality" : "Shall", + "Commandlet" : "Get-AntiPhishPolicy", + "ActualValue" : Policy.TargetedUserProtectionAction, + "ReportDetails" : ReportDetails(Status), + "RequirementMet" : Status +}] { + Policy := input.anti_phish_policies[_] + Policy.Identity == "Office365 AntiPhish Default" + Status := Policy.TargetedUserProtectionAction == "Quarantine" +} + +tests[{ + "Requirement" : "Message action SHALL be set to quarantine if the message is detected as impersonated: domains default policy", + "Control" : "Defender 2.5", + "Criticality" : "Shall", + "Commandlet" : "Get-AntiPhishPolicy", + "ActualValue" : Policy.TargetedDomainProtectionAction, + "ReportDetails" : ReportDetails(Status), + "RequirementMet" : Status +}] { + Policy := input.anti_phish_policies[_] + Policy.Identity == "Office365 AntiPhish Default" + Status := Policy.TargetedDomainProtectionAction == "Quarantine" +} + +tests[{ + "Requirement" : "Message action SHALL be set to quarantine if the message is detected as impersonated: mailbox default policy", + "Control" : "Defender 2.5", + "Criticality" : "Shall", + "Commandlet" : "Get-AntiPhishPolicy", + "ActualValue" : Policy.MailboxIntelligenceProtectionAction, + "ReportDetails" : ReportDetails(Status), + "RequirementMet" : Status +}] { + Policy := input.anti_phish_policies[_] + Policy.Identity == "Office365 AntiPhish Default" + Status := Policy.MailboxIntelligenceProtectionAction == "Quarantine" +} + + +# Step 2: non-default (SHOULD) +AntiPhishTargetedUserNotQuarantine[Policy.Identity] { + Policy := input.anti_phish_policies[_] + # Ignore the standard preset security policy because we can't change it in the tenant but it's always there. + not regex.match("Standard Preset Security Policy[0-9]+", Policy.Identity) + not Policy.Identity == "Office365 AntiPhish Default" + not Policy.TargetedUserProtectionAction == "Quarantine" +} + +tests[ { + "Requirement" : "Message action SHOULD be set to quarantine if the message is detected as impersonated: users non-default policies", + "Control" : "Defender 2.5", + "Criticality" : "Should", + "Commandlet" : "Get-AntiPhishPolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), + "RequirementMet" : Status +}] { + Policies := AntiPhishTargetedUserNotQuarantine + ErrorMessage := "non-default anti phish policy(ies) found where the action for messages detected as user impersonation is not quarantine:" + Status := count(Policies) == 0 +} + +AntiPhishTargetedDomainNotQuarantine[Policy.Identity] { + Policy := input.anti_phish_policies[_] + # Ignore the standard preset security policy because we can't change it in the tenant but it's always there. + not regex.match("Standard Preset Security Policy[0-9]+", Policy.Identity) + not Policy.Identity == "Office365 AntiPhish Default" + not Policy.TargetedDomainProtectionAction == "Quarantine" +} + +tests[ { + "Requirement" : "Message action SHOULD be set to quarantine if the message is detected as impersonated: domains non-default policies", + "Control" : "Defender 2.5", + "Criticality" : "Should", + "Commandlet" : "Get-AntiPhishPolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), + "RequirementMet" : Status +}] { + Policies := AntiPhishTargetedDomainNotQuarantine + ErrorMessage := "non-default anti phish policy(ies) found where the action for messages detected as domain impersonation is not quarantine:" + Status := count(Policies) == 0 +} + +AntiPhishMailIntNotQuarantine[Policy.Identity] { + Policy := input.anti_phish_policies[_] + # Ignore the standard preset security policy because we can't change it in the tenant but it's always there. + not regex.match("Standard Preset Security Policy[0-9]+", Policy.Identity) + not Policy.Identity == "Office365 AntiPhish Default" + not Policy.MailboxIntelligenceProtectionAction == "Quarantine" +} + +tests[ { + "Requirement" : "Message action SHOULD be set to quarantine if the message is detected as impersonated: mailbox non-default policies", + "Control" : "Defender 2.5", + "Criticality" : "Should", + "Commandlet" : "Get-AntiPhishPolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), + "RequirementMet" : Status +}] { + Policies := AntiPhishMailIntNotQuarantine + ErrorMessage := "non-default anti phish policy(ies) found where the action for messages flagged by mailbox intelligence is not quarantine:" + Status := count(Policies) == 0 +} + +# +# Baseline 2.5: Policy 6 +#-- +# Previous test divided into two rules. In the baseline we specify +# that default policies SHALL and custom policies SHOULD. To +# represent this, we have two tests checking the default and the +# nondefault. +tests[ { + "Requirement" : "Mail classified as spoofed SHALL be quarantined: default policy", + "Control" : "Defender 2.5", + "Criticality" : "Shall", + "Commandlet" : "Get-AntiPhishPolicy", + "ActualValue" : Policy.AuthenticationFailAction, + "ReportDetails" : ReportDetails(Status), + "RequirementMet" : Status +}] { + Policy := input.anti_phish_policies[_] + Policy.Identity == "Office365 AntiPhish Default" + Status := Policy.AuthenticationFailAction == "Quarantine" +} + +# Helper rule to get set of custom anti phishing policies where the +# AuthenticationFailAction is not set to quarantine +CustomAntiPhishSpoofNotQuarantine[Policy.Identity] { + Policy := input.anti_phish_policies[_] + # Ignore the standard preset security policy because we can't change it in the tenant but it's always there. + not regex.match("Standard Preset Security Policy[0-9]+", Policy.Identity) + Policy.Identity != "Office365 AntiPhish Default" + Policy.AuthenticationFailAction != "Quarantine" +} + +tests[ { + "Requirement" : "Mail classified as spoofed SHOULD be quarantined: non-default policies", + "Control" : "Defender 2.5", + "Criticality" : "Should", + "Commandlet" : "Get-AntiPhishPolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), + "RequirementMet" : Status +}] { + ErrorMessage := "custom anti phish policy(ies) found where the action for spoofed emails is not set to quarantine:" + Policies := CustomAntiPhishSpoofNotQuarantine + Status := count(Policies) == 0 +} +#-- + +# +# Baseline 2.5: Policy 7 +#-- +# First contact default policy +tests[ { + "Requirement" : "All safety tips SHALL be enabled: first contact default policy", + "Control" : "Defender 2.5", + "Criticality" : "Shall", + "Commandlet" : "Get-AntiPhishPolicy", + "ActualValue" : Policy.EnableFirstContactSafetyTips, + "ReportDetails" : ReportDetails(Status), + "RequirementMet" : Status +}] { + Policy := input.anti_phish_policies[_] + Policy.Identity == "Office365 AntiPhish Default" + Status := Policy.EnableFirstContactSafetyTips == true +} + +# First contact non-default policies +CustomAntiPhishNoSafetyTips[Policy.Identity] { + Policy := input.anti_phish_policies[_] + # Ignore the standard preset security policy because we can't change it in the tenant but it's always there. + not regex.match("Standard Preset Security Policy[0-9]+", Policy.Identity) + Policy.Identity != "Office365 AntiPhish Default" + not Policy.EnableFirstContactSafetyTips +} + +tests[{ + "Requirement" : "All safety tips SHOULD be enabled: first contact non-default policies", + "Control" : "Defender 2.5", + "Criticality" : "Should", + "Commandlet" : "Get-AntiPhishPolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), + "RequirementMet" : Status +}] { + ErrorMessage := "custom anti phish policy(ies) found where first contact safety tips are not enabled:" + Policies := CustomAntiPhishNoSafetyTips + Status := count(Policies) == 0 +} + +# Similar users default policy +tests[{ + "Requirement" : "All safety tips SHALL be enabled: user impersonation default policy", + "Control" : "Defender 2.5", + "Criticality" : "Shall", + "Commandlet" : "Get-AntiPhishPolicy", + "ActualValue" : Policy.EnableSimilarUsersSafetyTips, + "ReportDetails" : ReportDetails(Status), + "RequirementMet" : Status +}] { + Policy := input.anti_phish_policies[_] + Policy.Identity == "Office365 AntiPhish Default" + Status := Policy.EnableSimilarUsersSafetyTips == true +} + +# Similar users non-default policies +CustomAntiPhishNoSimilarUserTips[Policy.Identity] { + Policy := input.anti_phish_policies[_] + # Ignore the standard preset security policy because we can't change it in the tenant but it's always there. + not regex.match("Standard Preset Security Policy[0-9]+", Policy.Identity) + Policy.Identity != "Office365 AntiPhish Default" + not Policy.EnableSimilarUsersSafetyTips +} + +tests[{ + "Requirement" : "All safety tips SHOULD be enabled: user impersonation non-default policies", + "Control" : "Defender 2.5", + "Criticality" : "Should", + "Commandlet" : "Get-AntiPhishPolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), + "RequirementMet" : Status +}] { + ErrorMessage := "custom anti phish policy(ies) found where similar user safety tips are not enabled:" + Policies := CustomAntiPhishNoSimilarUserTips + Status := count(Policies) == 0 +} + +# Similar domains default policy +tests[{ + "Requirement" : "All safety tips SHALL be enabled: domain impersonation default policy", + "Control" : "Defender 2.5", + "Criticality" : "Shall", + "Commandlet" : "Get-AntiPhishPolicy", + "ActualValue" : Policy.EnableSimilarDomainsSafetyTips, + "ReportDetails" : ReportDetails(Status), + "RequirementMet" : Status +}] { + Policy := input.anti_phish_policies[_] + Policy.Identity == "Office365 AntiPhish Default" + Status := Policy.EnableSimilarDomainsSafetyTips == true +} + +# Similar domains non-default policies +CustomAntiPhishNoSimilarDomainTips[Policy.Identity] { + Policy := input.anti_phish_policies[_] + # Ignore the standard preset security policy because we can't change it in the tenant but it's always there. + not regex.match("Standard Preset Security Policy[0-9]+", Policy.Identity) + Policy.Identity != "Office365 AntiPhish Default" + not Policy.EnableSimilarDomainsSafetyTips +} + +tests[{ + "Requirement" : "All safety tips SHOULD be enabled: domain impersonation non-default policies", + "Control" : "Defender 2.5", + "Criticality" : "Should", + "Commandlet" : "Get-AntiPhishPolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), + "RequirementMet" : Status +}] { + ErrorMessage := "custom anti phish policy(ies) found where similar domains safety tips are not enabled:" + Policies := CustomAntiPhishNoSimilarDomainTips + Status := count(Policies) == 0 +} + +# Unusual characters default policy +tests[{ + "Requirement" : "All safety tips SHALL be enabled: user impersonation unusual characters default policy", + "Control" : "Defender 2.5", + "Criticality" : "Shall", + "Commandlet" : "Get-AntiPhishPolicy", + "ActualValue" : Policy.EnableUnusualCharactersSafetyTips, + "ReportDetails" : ReportDetails(Status), + "RequirementMet" : Status +}] { + Policy := input.anti_phish_policies[_] + Policy.Identity == "Office365 AntiPhish Default" + Status := Policy.EnableUnusualCharactersSafetyTips == true +} + +# Unusual characters non-default policies +CustomAntiPhishNoUnusualCharTips[Policy.Identity] { + Policy := input.anti_phish_policies[_] + # Ignore the standard preset security policy because we can't change it in the tenant but it's always there. + not regex.match("Standard Preset Security Policy[0-9]+", Policy.Identity) + Policy.Identity != "Office365 AntiPhish Default" + not Policy.EnableUnusualCharactersSafetyTips +} + +tests[{ + "Requirement" : "All safety tips SHOULD be enabled: user impersonation unusual characters non-default policies", + "Control" : "Defender 2.5", + "Criticality" : "Should", + "Commandlet" : "Get-AntiPhishPolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), + "RequirementMet" : Status +}] { + ErrorMessage := "custom anti phish policy(ies) found where unusual character safety tips are not enabled:" + Policies := CustomAntiPhishNoUnusualCharTips + Status := count(Policies) == 0 +} + +# Via tag default policy +tests[{ + "Requirement" : "All safety tips SHALL be enabled: \"via\" tag default policy", + "Control" : "Defender 2.5", + "Criticality" : "Shall", + "Commandlet" : "Get-AntiPhishPolicy", + "ActualValue" : Policy.EnableViaTag, + "ReportDetails" : ReportDetails(Status), + "RequirementMet" : Status +}] { + Policy := input.anti_phish_policies[_] + Policy.Identity == "Office365 AntiPhish Default" + Status := Policy.EnableViaTag == true +} + +# Via tag non-default policies +CustomAntiPhishNoViaTagTips[Policy.Identity] { + Policy := input.anti_phish_policies[_] + # Ignore the standard preset security policy because we can't change it in the tenant but it's always there. + not regex.match("Standard Preset Security Policy[0-9]+", Policy.Identity) + Policy.Identity != "Office365 AntiPhish Default" + not Policy.EnableViaTag +} + +tests[{ + "Requirement" : "All safety tips SHOULD be enabled: \"via\" tag non-default policies", + "Control" : "Defender 2.5", + "Criticality" : "Should", + "Commandlet" : "Get-AntiPhishPolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), + "RequirementMet" : Status +}] { + ErrorMessage := "custom anti phish policy(ies) found where via tag is not enabled:" + Policies := CustomAntiPhishNoViaTagTips + Status := count(Policies) == 0 +} + +# Unauthenticated sender default policy +tests[{ + "Requirement" : "All safety tips SHALL be enabled: \"?\" for unauthenticated senders for spoof default policy", + "Control" : "Defender 2.5", + "Criticality" : "Shall", + "Commandlet" : "Get-AntiPhishPolicy", + "ActualValue" : Policy.EnableUnauthenticatedSender, + "ReportDetails" : ReportDetails(Status), + "RequirementMet" : Status +}] { + Policy := input.anti_phish_policies[_] + Policy.Identity == "Office365 AntiPhish Default" + Status := Policy.EnableUnauthenticatedSender == true +} + +# Unauthenticated sender non-default policies +CustomAntiPhishNoUnauthSenderTips[Policy.Identity] { + Policy := input.anti_phish_policies[_] + # Ignore the standard preset security policy because we can't change it in the tenant but it's always there. + not regex.match("Standard Preset Security Policy[0-9]+", Policy.Identity) + Policy.Identity != "Office365 AntiPhish Default" + not Policy.EnableUnauthenticatedSender +} + +tests[{ + "Requirement" : "All safety tips SHOULD be enabled: \"?\" for unauthenticated senders for spoof non-default policies", + "Control" : "Defender 2.5", + "Criticality" : "Should", + "Commandlet" : "Get-AntiPhishPolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), + "RequirementMet" : Status +}] { + ErrorMessage := "custom anti phish policy(ies) found where '?' for unauthenticated sender is not enabled:" + Policies := CustomAntiPhishNoUnauthSenderTips + Status := count(Policies) == 0 +} +#-- + + + +################ +# Baseline 2.6 # +################ + +# +# Baseline 2.6: Policy 1 +#-- +tests[{ + "Requirement" : "The bulk complaint level (BCL) threshold SHOULD be set to six or lower: default policy", + "Control" : "Defender 2.6", + "Criticality" : "Should", + "Commandlet" : "Get-HostedContentFilterPolicy", + "ActualValue" : Policy.BulkThreshold, + "ReportDetails" : ReportDetails(Status), + "RequirementMet" : Status +}] { + Policy := input.hosted_content_filter_policies[_] # Refactor + Policy.Identity == "Default" + Status := Policy.BulkThreshold <= 6 +} + +CustomBulkThresholdWrong [Policy.Identity] { + Policy := input.hosted_content_filter_policies[_] + Policy.Identity != "Default" + Policy.BulkThreshold > 6 +} + +tests[{ + "Requirement" : "The bulk complaint level (BCL) threshold SHOULD be set to six or lower: non-default policies", + "Control" : "Defender 2.6", + "Criticality" : "Should", + "Commandlet" : "Get-HostedContentFilterPolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), + "RequirementMet" : Status +}] { + ErrorMessage := "custom anti-spam policy(ies) found where bulk complaint level threshold is set to 7 or more:" + Policies = CustomBulkThresholdWrong + Status := count(Policies) == 0 +} +#-- + +# +# Baseline 2.6: Policy 2 +#-- +# Step 1: The default policy (SHALL) +tests[{ + "Requirement" : "Spam SHALL be moved to either the junk email folder or the quarantine folder: default policy", + "Control" : "Defender 2.6", + "Criticality" : "Shall", + "Commandlet" : "Get-HostedContentFilterPolicy", + "ActualValue" : Policy.SpamAction, + "ReportDetails" : ReportDetails(Status), + "RequirementMet" : Status +}] { + Policy := input.hosted_content_filter_policies[_] + Policy.Identity == "Default" + Status := Policy.SpamAction in ["Quarantine", "MoveToJmf"] +} + +tests[{ + "Requirement" : "High confidence spam SHALL be moved to either the junk email folder or the quarantine folder: default policy", + "Control" : "Defender 2.6", + "Criticality" : "Shall", + "Commandlet" : "Get-HostedContentFilterPolicy", + "ActualValue" : Policy.HighConfidenceSpamAction, + "ReportDetails" : ReportDetails(Status), + "RequirementMet" : Status +}] { + Policy := input.hosted_content_filter_policies[_] + Policy.Identity == "Default" + Status := Policy.HighConfidenceSpamAction in ["Quarantine", "MoveToJmf"] +} + +# Step 2: The non-default policies (SHOULD) +CustomSpamActionWrong [Policy.Identity] { + Policy := input.hosted_content_filter_policies[_] + Policy.Identity != "Default" + not Policy.SpamAction in ["Quarantine", "MoveToJmf"] +} + +tests[{ + "Requirement" : "Spam SHOULD be moved to either the junk email folder or the quarantine folder: non-default policies", + "Control" : "Defender 2.6", + "Criticality" : "Should", + "Commandlet" : "Get-HostedContentFilterPolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), + "RequirementMet" : Status +}] { + ErrorMessage := "custom anti-spam policy(ies) found where spam is not being sent to the Quarantine folder or the Junk Mail Folder:" + Policies = CustomSpamActionWrong + Status := count(Policies) == 0 +} + +CustomHighConfidenceSpamActionWrong [Policy.Identity] { + Policy := input.hosted_content_filter_policies[_] + Policy.Identity != "Default" + not Policy.HighConfidenceSpamAction in ["Quarantine", "MoveToJmf"] +} + +tests[{ + "Requirement" : "High confidence spam SHOULD be moved to either the junk email folder or the quarantine folder: non-default policies", + "Control" : "Defender 2.6", + "Criticality" : "Should", + "Commandlet" : "Get-HostedContentFilterPolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), + "RequirementMet" : Status +}] { + ErrorMessage := "custom anti-spam policy(ies) found where high confidence spam is not being sent to the Quarantine folder or the Junk Mail Folder:" + Policies = CustomHighConfidenceSpamActionWrong + Status := count(Policies) == 0 +} +#-- + +# +# Baseline 2.6: Policy 3 +#-- +# Step 1: The default policy (SHALL) +tests[{ + "Requirement" : "Phishing SHALL be quarantined: default policy", + "Control" : "Defender 2.6", + "Criticality" : "Shall", + "Commandlet" : "Get-HostedContentFilterPolicy", + "ActualValue" : Policy.PhishSpamAction, + "ReportDetails" : ReportDetails(Status), + "RequirementMet" : Status +}] { + Policy := input.hosted_content_filter_policies[_] + Policy.Identity == "Default" + Status := Policy.PhishSpamAction == "Quarantine" +} + +tests[{ + "Requirement" : "High confidence phishing SHALL be quarantined: default policy", + "Control" : "Defender 2.6", + "Criticality" : "Shall", + "Commandlet" : "Get-HostedContentFilterPolicy", + "ActualValue" : Policy.HighConfidencePhishAction, + "ReportDetails" : ReportDetails(Status), + "RequirementMet" : Status +}] { + Policy := input.hosted_content_filter_policies[_] + Policy.Identity == "Default" + Status := Policy.HighConfidencePhishAction == "Quarantine" +} + +# Step 2: The non-default policies (SHOULD) + +CustomPhishSpamActionWrong [Policy.Identity] { + Policy := input.hosted_content_filter_policies[_] + Policy.Identity != "Default" + Policy.PhishSpamAction != "Quarantine" +} + +tests[{ + "Requirement" : "Phishing SHOULD be quarantined: non-default policies", + "Control" : "Defender 2.6", + "Criticality" : "Should", + "Commandlet" : "Get-HostedContentFilterPolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), + "RequirementMet" : Status +}] { + ErrorMessage := "custom anti-spam policy(ies) found where phishing isn't moved to the quarantine folder:" + Policies = CustomPhishSpamActionWrong + Status := count(Policies) == 0 +} + +CustomHighConfidencePhishActionWrong [Policy.Identity] { + Policy := input.hosted_content_filter_policies[_] + Policy.Identity != "Default" + Policy.HighConfidencePhishAction != "Quarantine" +} + +tests[{ + "Requirement" : "High confidence phishing SHOULD be quarantined: non-default policies", + "Control" : "Defender 2.6", + "Criticality" : "Should", + "Commandlet" : "Get-HostedContentFilterPolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), + "RequirementMet" : Status +}] { + ErrorMessage := "custom anti-spam policy(ies) found where high-confidence phishing isn't moved to quarantine folder:" + Policies = CustomHighConfidencePhishActionWrong + Status := count(Policies) == 0 +} + +# +# Baseline 2.6: Policy 4 +#-- +tests[{ + "Requirement" : "Bulk email SHOULD be moved to either the junk email folder or the quarantine folder: default policy", + "Control" : "Defender 2.6", + "Criticality" : "Should", + "Commandlet" : "Get-HostedContentFilterPolicy", + "ActualValue" : Policy.BulkSpamAction, + "ReportDetails" : ReportDetails(Status), + "RequirementMet" : Status +}] { + Policy := input.hosted_content_filter_policies[_] + Policy.Identity == "Default" + Status := Policy.BulkSpamAction in ["Quarantine", "MoveToJmf"] +} + +CustomBulkSpamActionWrong [Policy.Identity] { + Policy := input.hosted_content_filter_policies[_] + Policy.Identity != "Default" + not Policy.BulkSpamAction in ["Quarantine", "MoveToJmf"] +} + +tests[{ + "Requirement" : "Bulk email SHOULD be moved to either the junk email folder or the quarantine folder: non-default policies", + "Control" : "Defender 2.6", + "Criticality" : "Should", + "Commandlet" : "Get-HostedContentFilterPolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), + "RequirementMet" : Status +}] { + ErrorMessage := "custom anti-spam policy(ies) found where bulk spam action is not Quarantine or Move to Junk Email Folder" + Policies = CustomBulkSpamActionWrong + Status := count(Policies) == 0 +} +#-- + +# +# Baseline 2.6: Policy 5 +#-- +tests[{ + "Requirement" : "Spam in quarantine SHOULD be retained for at least 30 days: default policy", + "Control" : "Defender 2.6", + "Criticality" : "Should", + "Commandlet" : "Get-HostedContentFilterPolicy", + "ActualValue" : Policy.QuarantineRetentionPeriod, + "ReportDetails" : ReportDetails(Status), + "RequirementMet" : Status +}] { + Policy := input.hosted_content_filter_policies[_] + Policy.Identity == "Default" + Status := Policy.QuarantineRetentionPeriod == 30 +} + +CustomQuarantineRetentionPeriodWrong [Policy.Identity] { + Policy := input.hosted_content_filter_policies[_] + Policy.Identity != "Default" + Policy.QuarantineRetentionPeriod != 30 +} + +tests[{ + "Requirement" : "Spam in quarantine SHOULD be retained for at least 30 days: non-default policies", + "Control" : "Defender 2.6", + "Criticality" : "Should", + "Commandlet" : "Get-HostedContentFilterPolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), + "RequirementMet" : Status +}] { + ErrorMessage := "custom anti-spam policy(ies) found where spam in quarantine isn't retained for 30 days:" + Policies = CustomQuarantineRetentionPeriodWrong + Status := count(Policies) == 0 +} +#-- + +# +# Baseline 2.6: Policy 6 +#-- +tests[{ + "Requirement" : "Spam safety tips SHOULD be turned on: default policy", + "Control" : "Defender 2.6", + "Criticality" : "Should", + "Commandlet" : "Get-HostedContentFilterPolicy", + "ActualValue" : Policy.InlineSafetyTipsEnabled, + "ReportDetails" : ReportDetails(Status), + "RequirementMet" : Status +}] { + Policy := input.hosted_content_filter_policies[_] + Policy.Identity == "Default" + Status := Policy.InlineSafetyTipsEnabled +} + +CustomInlineSafetyTipsDisabled [Policy.Identity] { + Policy := input.hosted_content_filter_policies[_] + Policy.Identity != "Default" + not Policy.InlineSafetyTipsEnabled +} + +tests[{ + "Requirement" : "Spam safety tips SHOULD be turned on: non-default policies", + "Control" : "Defender 2.6", + "Criticality" : "Should", + "Commandlet" : "Get-HostedContentFilterPolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), + "RequirementMet" : Status +}] { + ErrorMessage := "custom anti-spam policy(ies) found where spam safety tips is disabled:" + Policies = CustomInlineSafetyTipsDisabled + Status := count(Policies) == 0 +} +#-- + +# +# Baseline 2.6: Policy 7 +#-- +# Step 1: The default policy (SHALL) +tests[{ + "Requirement" : "Zero-hour auto purge (ZAP) SHALL be enabled: default policy", + "Control" : "Defender 2.6", + "Criticality" : "Shall", + "Commandlet" : "Get-HostedContentFilterPolicy", + "ActualValue" : Policy.ZapEnabled, + "ReportDetails" : ReportDetails(Status), + "RequirementMet" : Status +}] { + Policy := input.hosted_content_filter_policies[_] + Policy.Identity == "Default" + Status := Policy.ZapEnabled +} + +tests[{ + "Requirement" : "Zero-hour auto purge (ZAP) SHALL be enabled for spam messages: default policy", + "Control" : "Defender 2.6", + "Criticality" : "Shall", + "Commandlet" : "Get-HostedContentFilterPolicy", + "ActualValue" : Policy.SpamZapEnabled, + "ReportDetails" : ReportDetails(Status), + "RequirementMet" : Status +}] { + Policy := input.hosted_content_filter_policies[_] + Policy.Identity == "Default" + Status := Policy.SpamZapEnabled +} + +tests[{ + "Requirement" : "Zero-hour auto purge (ZAP) SHALL be enabled for phishing: default policy", + "Control" : "Defender 2.6", + "Criticality" : "Shall", + "Commandlet" : "Get-HostedContentFilterPolicy", + "ActualValue" : Policy.PhishZapEnabled, + "ReportDetails" : ReportDetails(Status), + "RequirementMet" : Status +}] { + Policy := input.hosted_content_filter_policies[_] + Policy.Identity == "Default" + Status := Policy.PhishZapEnabled +} + +# Step 2: The non-default policies (SHOULD) +CustomZapDisabled [Policy.Identity] { + Policy := input.hosted_content_filter_policies[_] + Policy.Identity != "Default" + not Policy.ZapEnabled +} + +tests[{ + "Requirement" : "Zero-hour auto purge (ZAP) SHOULD be enabled: non-default", + "Control" : "Defender 2.6", + "Criticality" : "Should", + "Commandlet" : "Get-HostedContentFilterPolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), + "RequirementMet" : Status +}] { + ErrorMessage := "custom anti-spam policies found where Zero-hour auto purge is disabled:" + Policies = CustomZapDisabled + Status := count(Policies) == 0 +} + +CustomSpamZapDisabled [Policy.Identity] { + Policy := input.hosted_content_filter_policies[_] + Policy.Identity != "Default" + not Policy.SpamZapEnabled +} + +tests[{ + "Requirement" : "Zero-hour auto purge (ZAP) SHOULD be enabled for Spam: non-default", + "Control" : "Defender 2.6", + "Criticality" : "Should", + "Commandlet" : "Get-HostedContentFilterPolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), + "RequirementMet" : Status +}] { + ErrorMessage := "custom anti-spam policies found where Zero-hour auto purge for spam is disabled:" + Policies = CustomSpamZapDisabled + Status := count(Policies) == 0 +} + +CustomPhishZapDisabled [Policy.Identity] { + Policy := input.hosted_content_filter_policies[_] + Policy.Identity != "Default" + not Policy.PhishZapEnabled +} + +tests[{ + "Requirement" : "Zero-hour auto purge (ZAP) SHOULD be enabled for phishing: non-default", + "Control" : "Defender 2.6", + "Criticality" : "Should", + "Commandlet" : "Get-HostedContentFilterPolicy", + "ActualValue" : Policies, + "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(Policies, ErrorMessage)), + "RequirementMet" : Status +}] { + ErrorMessage := "custom anti-spam policy(ies) found where Zero-hour auto purge for phishing is disabled:" + Policies = CustomPhishZapDisabled + Status := count(Policies) == 0 +} +#-- + +# +# Baseline 2.6: Policy 8 +#-- +# At this time we are unable to test for X because of Y +tests[{ + "Requirement" : "Allowed senders MAY be added but allowed domains SHALL NOT be added", + "Control" : "Defender 2.6", + "Criticality" : "Shall/Not-Implemented", + "Commandlet" : "", + "ActualValue" : [], + "ReportDetails" : "Currently cannot be checked automatically. See Defender Secure Configuration Baseline policy 2.8 for instructions on manual check", + "RequirementMet" : false +}] { + true +} +#-- + + +################ +# Baseline 2.7 # +################ + +# +# Baseline 2.7: Policy 1 +#-- +AllDomainsSafeLinksPolicies[{ + "Identity" : Rule.Identity, + "RecipientDomains" : RecipientDomains}] { + Rule := input.safe_links_rules[_] + Rule.State == "Enabled" + DomainNames := {Name.DomainName | Name = input.all_domains[_]} + RecipientDomains := {Name | Name = Rule.RecipientDomainIs[_]} + Difference := DomainNames - RecipientDomains # set difference + count(Difference) == 0 +} + +tests[{ + "Requirement" : "The Safe Links Policy SHALL include all agency domains-and by extension-all users", + "Control" : "Defender 2.7", + "Criticality" : "Shall", + "Commandlet" : "Get-SafeLinksPolicy", + "ActualValue" : AllDomainsSafeLinksPolicies, + "ReportDetails" : ApplyLicenseWarning(CustomizeError(ReportDetails(Status), ErrorMessage)), + "RequirementMet" : Status +}] { + DomainNames := {Name.DomainName | Name = input.all_domains[_]} + ErrorMessage := concat("", ["No policy found that applies to all domains: ", concat(", ", DomainNames)]) + Status := count(AllDomainsSafeLinksPolicies) > 0 +} +#-- + +# +# Baseline 2.7: Policy 2 +#-- +EnableSafeLinksForEmailCorrect[Policy.Identity] { + Policy := input.safe_links_policies[_] + Policy.Identity != "Built-In Protection Policy" + Policy.EnableSafeLinksForEmail == true + Rule := input.safe_links_rules[_] + Rule.Identity == Policy.Identity + Rule.State == "Enabled" +} + +tests[{ + "Requirement" : "URL rewriting and malicious link click checking SHALL be enabled", + "Control" : "Defender 2.7", + "Criticality" : "Shall", + "Commandlet" : "Get-SafeLinksPolicy", + "ActualValue" : Policies, + "ReportDetails" : ApplyLicenseWarning(ReportDetails(Status)), + "RequirementMet" : Status +}] { + Policies := EnableSafeLinksForEmailCorrect + Status := count(Policies) >= 1 +} +#-- + +# +# Baseline 2.7: Policy 3 +#-- +EnableSafeLinksForTeamsCorrect[Policy.Identity] { + Policy := input.safe_links_policies[_] + Policy.Identity != "Built-In Protection Policy" + Policy.EnableSafeLinksForTeams == true + Rule := input.safe_links_rules[_] + Rule.Identity == Policy.Identity + Rule.State == "Enabled" +} + +tests[{ + "Requirement" : "Malicious link click checking SHALL be enabled with Microsoft Teams", + "Control" : "Defender 2.7", + "Criticality" : "Shall", + "Commandlet" : "Get-SafeLinksPolicy", + "ActualValue" : Policies, + "ReportDetails" : ApplyLicenseWarning(ReportDetails(Status)), + "RequirementMet" : Status +}] { + Policies := EnableSafeLinksForTeamsCorrect + Status := count(Policies) >= 1 +} +#-- + +# +# Baseline 2.7: Policy 4 +#-- +ScanUrlsCorrect[Policy.Identity] { + Policy := input.safe_links_policies[_] + Policy.Identity != "Built-In Protection Policy" + Policy.ScanUrls == true + Rule := input.safe_links_rules[_] + Rule.Identity == Policy.Identity + Rule.State == "Enabled" +} + +tests[{ + "Requirement" : "Real-time suspicious URL and file-link scanning SHALL be enabled", + "Control" : "Defender 2.7", + "Criticality" : "Shall", + "Commandlet" : "Get-SafeLinksPolicy", + "ActualValue" : Policies, + "ReportDetails" : ApplyLicenseWarning(ReportDetails(Status)), + "RequirementMet" : Status +}] { + Policies := ScanUrlsCorrect + Status := count(Policies) >= 1 +} +#-- + +# +# Baseline 2.7: Policy 5 +#-- +DeliverMessageAfterScanCorrect[Policy.Identity] { + Policy := input.safe_links_policies[_] + Policy.Identity != "Built-In Protection Policy" + Policy.DeliverMessageAfterScan == true + Rule := input.safe_links_rules[_] + Rule.Identity == Policy.Identity + Rule.State == "Enabled" +} + +tests[{ + "Requirement" : "URLs SHALL be scanned completely before message delivery", + "Control" : "Defender 2.7", + "Criticality" : "Shall", + "Commandlet" : "Get-SafeLinksPolicy", + "ActualValue" : Policies, + "ReportDetails" : ApplyLicenseWarning(ReportDetails(Status)), + "RequirementMet" : Status +}] { + Policies := DeliverMessageAfterScanCorrect + Status := count(Policies) >= 1 +} +#-- + +# +# Baseline 2.7: Policy 6 +#-- +EnableForInternalSendersCorrect[Policy.Identity] { + Policy := input.safe_links_policies[_] + Policy.Identity != "Built-In Protection Policy" + Policy.EnableForInternalSenders == true + Rule := input.safe_links_rules[_] + Rule.Identity == Policy.Identity + Rule.State == "Enabled" +} + +tests[{ + "Requirement" : "Internal agency email messages SHALL have safe links enabled", + "Control" : "Defender 2.7", + "Criticality" : "Shall", + "Commandlet" : "Get-SafeLinksPolicy", + "ActualValue" : Policies, + "ReportDetails" : ApplyLicenseWarning(ReportDetails(Status)), + "RequirementMet" : Status +}] { + Policies := EnableForInternalSendersCorrect + Status := count(Policies) >= 1 +} +#-- + +# +# Baseline 2.7: Policy 7 +#-- +TrackClicksCorrect[Policy.Identity] { + Policy := input.safe_links_policies[_] + Policy.Identity != "Built-In Protection Policy" + Policy.TrackClicks == true + Rule := input.safe_links_rules[_] + Rule.Identity == Policy.Identity + Rule.State == "Enabled" +} + +tests[{ + "Requirement" : "User click tracking SHALL be enabled", + "Control" : "Defender 2.7", + "Criticality" : "Shall", + "Commandlet" : "Get-SafeLinksPolicy", + "ActualValue" : Policies, + "ReportDetails" : ApplyLicenseWarning(ReportDetails(Status)), + "RequirementMet" : Status +}] { + Policies := TrackClicksCorrect + Status := count(Policies) >= 1 +} +#-- + +# +# Baseline 2.7: Policy 8 +#-- +EnableSafeLinksForOfficeCorrect[Policy.Identity] { + Policy := input.safe_links_policies[_] + Policy.EnableSafeLinksForOffice == true + Rule := input.safe_links_rules[_] + Rule.Identity == Policy.Identity + Rule.State == "Enabled" +} + +tests[{ + "Requirement" : "Safe Links in Office 365 apps SHALL be turned on", + "Control" : "Defender 2.7", + "Criticality" : "Shall", + "Commandlet" : "Get-SafeLinksPolicy", + "ActualValue" : Policies, + "ReportDetails" : ApplyLicenseWarning(ReportDetails(Status)), + "RequirementMet" : Status +}] { + Policies := EnableSafeLinksForOfficeCorrect + Status := count(Policies) >= 1 +} +#-- + +# +# Baseline 2.7: Policy 9 +#-- +AllowClickThroughCorrect[Policy.Identity] { + Policy := input.safe_links_policies[_] + Policy.AllowClickThrough == false + Rule := input.safe_links_rules[_] + Rule.Identity == Policy.Identity + Rule.State == "Enabled" +} + +tests[{ + "Requirement" : "Users SHALL NOT be enabled to click through to the original URL", + "Control" : "Defender 2.7", + "Criticality" : "Shall", + "Commandlet" : "Get-SafeLinksPolicy", + "ActualValue" : Policies, + "ReportDetails" : ApplyLicenseWarning(ReportDetails(Status)), + "RequirementMet" : Status +}] { + Policies := AllowClickThroughCorrect + Status := count(Policies) >= 1 +} +#-- + + +################ +# Baseline 2.8 # +################ + +# +# Baseline 2.8: Policy 1 +#-- +# find the set of policies that are applied to all of the tenant's domains +AllDomainsSafeAttachmentRules[{ + "Identity" : Rule.Identity, + "RecipientDomains" : RecipientDomains}] { + Rule := input.safe_attachment_rules[_] + DomainNames = {Name.DomainName | Name = input.all_domains[_]} + RecipientDomains = {Name | Name = Rule.RecipientDomainIs[_]} + Difference := DomainNames - RecipientDomains # set difference + count(Difference) == 0 +} + +tests[{ + "Requirement" : "At least one Safe Attachments Policy SHALL include all agency domains-and by extension-all users", + "Control" : "Defender 2.8", + "Criticality" : "Shall", + "Commandlet" : "Get-SafeAttachmentRule", + "ActualValue" : AllDomainsSafeAttachmentRules, + "ReportDetails" : ApplyLicenseWarning(CustomizeError(ReportDetails(Status), ErrorMessage)), + "RequirementMet" : Status +}] { + DomainNames := {Name.DomainName | Name = input.all_domains[_]} + ErrorMessage := concat("", ["No policy found that applies to all domains: ", concat(", ", DomainNames)]) + Status := count(AllDomainsSafeAttachmentRules) > 0 +} +#-- + +# +# Baseline 2.8: Policy 2 +#-- +# Find the set of policies that: +# - have the action set to block +# - are enabled +# - and are one of the policies that apply to all domains +BlockMalwarePolicies[{ + "Identity" : SafeAttachmentPolicies.Identity, + "Action" : SafeAttachmentPolicies.Action, + "Enable" : SafeAttachmentPolicies.Enable, + "RedirectAddress" : SafeAttachmentPolicies.RedirectAddress}] { + SafeAttachmentPolicies := input.safe_attachment_policies[_] + SafeAttachmentPolicies.Action == "Block" + SafeAttachmentPolicies.Enable + AllDomainsPoliciesNames := {Rule.Identity | Rule = AllDomainsSafeAttachmentRules[_]} + SafeAttachmentPolicies.Identity in AllDomainsPoliciesNames +} + +tests[{ + "Requirement" : "The action for malware in email attachments SHALL be set to block", + "Control" : "Defender 2.8", + "Criticality" : "Shall", + "Commandlet" : "Get-SafeAttachmentPolicy", + "ActualValue" : Policies, + "ReportDetails" : ApplyLicenseWarning(CustomizeError(ReportDetails(Status), ErrorMessage)), + "RequirementMet" : Status +}] { + Policies := BlockMalwarePolicies + ErrorMessage := "No enabled policy found with action set to block that apply to all domains" + Status := count(Policies) > 0 +} +#-- + +# +# Baseline 2.8: Policy 3 +#-- +# Find the set of policies that are blocking malware and have a +# redirection address specified +RedirectionPolicies[{ + "Identity" : Policy.Identity, + "RedirectAddress" : Policy.RedirectAddress}] { + Policy := BlockMalwarePolicies[_] + Policy.RedirectAddress != "" +} + +tests[{ + "Requirement" : "Redirect emails with detected attachments to an agency-specified email SHOULD be enabled", + "Control" : "Defender 2.8", + "Criticality" : "Should", + "Commandlet" : "Get-SafeAttachmentPolicy", + "ActualValue" : Policies, + "ReportDetails" : ApplyLicenseWarning(CustomizeError(ReportDetails(Status), ErrorMessage)), + "RequirementMet" : Status +}] { + Policies := RedirectionPolicies + ErrorMessage := "No enabled policy found with action set to block and at least one contact specified" + Status := count(Policies) > 0 +} +#-- + +# +# Baseline 2.8: Policy 4 +#-- +# Find the set of policies that have EnableATPForSPOTeamsODB set to true +ATPPolicies[{ + "Identity" : Policy.Identity, + "EnableATPForSPOTeamsODB" : Policy.EnableATPForSPOTeamsODB}] { + Policy := input.atp_policy_for_o365[_] + Policy.EnableATPForSPOTeamsODB == true +} + +tests[{ + "Requirement" : "Safe attachments SHOULD be enabled for SharePoint, OneDrive, and Microsoft Teams", + "Control" : "Defender 2.8", + "Criticality" : "Should", + "Commandlet" : "Get-AtpPolicyForO365", + "ActualValue" : Policies, + "ReportDetails" : ApplyLicenseWarning(ReportDetails(Status)), + "RequirementMet" : Status +}] { + Policies := ATPPolicies + Status := count(Policies) > 0 +} +#-- + + +################# +# Baseline 2.9 # +################# + +# +# Baseline 2.9: Policy 1 +#-- +# At a minimum, the alerts required by the EXO baseline SHALL be enabled. +RequiredAlerts := { + "Suspicious email sending patterns detected", + "Unusual increase in email reported as phish", + "Suspicious Email Forwarding Activity", + "Messages have been delayed", + "Tenant restricted from sending unprovisioned email", + "User restricted from sending email", + "Malware campaign detected after delivery", + "A potentially malicious URL click was detected", + "Suspicious connector activity" +} + +EnabledAlerts[Alert.Name] { + Alert := input.protection_alerts[_] + Alert.Disabled == false +} + +tests[{ + "Requirement" : "At a minimum, the alerts required by the Exchange Online Minimum Viable Secure Configuration Baseline SHALL be enabled", + "Control" : "Defender 2.9", + "Criticality" : "Shall", + "Commandlet" : "Get-ProtectionAlert", + "ActualValue" : MissingAlerts, + "ReportDetails" : CustomizeError(ReportDetails(Status), GenerateArrayString(MissingAlerts, ErrorMessage)), + "RequirementMet" : Status +}] { + MissingAlerts := RequiredAlerts - EnabledAlerts + ErrorMessage := "disabled required alert(s) found:" + Status := count(MissingAlerts) == 0 +} +#-- + +# +# Baseline 2.9: Policy 2 +#-- +# SIEM incorporation cannot be checked programmatically +tests[{ + "Requirement" : "The alerts SHOULD be sent to a monitored address or incorporated into a SIEM", + "Control" : "Defender 2.9", + "Criticality" : "Should/Not-Implemented", + "Commandlet" : "", + "ActualValue" : [], + "ReportDetails" : "Currently cannot be checked automatically. See Defender Secure Configuration Baseline policy 2.9 for instructions on manual check", + "RequirementMet" : false +}] { + true +} +#-- + + +################# +# Baseline 2.10 # +################# + +# +# Baseline 2.10: Policy 1 +#-- +tests[{ + "Requirement" : "Unified audit logging SHALL be enabled", + "Control" : "Defender 2.10", + "Criticality" : "Shall", + "Commandlet" : "Get-AdminAuditLogConfig", + "ActualValue" : AuditLog.UnifiedAuditLogIngestionEnabled, + "ReportDetails" : ReportDetails(Status), + "RequirementMet" : Status +}] { + AuditLog := input.admin_audit_log_config + Status := AuditLog.UnifiedAuditLogIngestionEnabled == true +} +#-- + +# +# Baseline 2.10: Policy 2 +#-- +# Turns out audit logging is non-trivial to implement and test for. +# Would require looping through all users. See discussion in GitHub +# issue #200. +tests[{ + "Requirement" : "Advanced audit SHALL be enabled", + "Control" : "Defender 2.10", + "Criticality" : "Shall/Not-Implemented", + "Commandlet" : "", + "ActualValue" : [], + "ReportDetails" : "Currently cannot be checked automatically. See Defender Secure Configuration Baseline policy 2.10 for instructions on manual check", + "RequirementMet" : false +}] { + true +} +#-- + +# +# Baseline 2.10: Policy 3 +#-- +# Dictated by OMB M-21-31: 12 months in hot storage and 18 months in cold +# It is not required to maintain these logs in the M365 cloud environment; doing so would require an additional add-on SKU. +#This requirement can be met by offloading the logs out of the cloud environment. +tests[{ + "Requirement" : "Audit logs SHALL be maintained for at least the minimum duration dictated by OMB M-21-31", + "Control" : "Defender 2.10", + "Criticality" : "Shall/Not-Implemented", + "Commandlet" : "", + "ActualValue" : [], + "ReportDetails" : "Currently cannot be checked automatically. See Defender Secure Configuration Baseline policy 2.10 for instructions on manual check", + "RequirementMet" : false +}] { + true +} +#-- \ No newline at end of file diff --git a/Rego/EXOConfig.rego b/Rego/EXOConfig.rego new file mode 100644 index 0000000000..1d884a7bd1 --- /dev/null +++ b/Rego/EXOConfig.rego @@ -0,0 +1,802 @@ +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" : "Resolve-DnsName", + "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 + startswith(DkimRecord.rdata, "v=DKIM1;") +} + +tests[{ + "Requirement" : "DKIM SHOULD be enabled for any custom domain", + "Control" : "EXO 2.3", + "Criticality" : "Should", + "Commandlet" : "Get-DkimSigningConfig, Resolve-DnsName", + "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[_] + not startswith(DmarcRecord.rdata, "v=DMARC1;") +} + +tests[{ + "Requirement" : "A DMARC policy SHALL be published for every second-level domain", + "Control" : "EXO 2.4", + "Criticality" : "Shall", + "Commandlet" : "Resolve-DnsName", + "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[_] + not contains(DmarcRecord.rdata, "p=reject;") +} + +tests[{ + "Requirement" : "The DMARC message rejection option SHALL be \"p=reject\"", + "Control" : "EXO 2.4", + "Criticality" : "Shall", + "Commandlet" : "Resolve-DnsName", + "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[_] + not contains(DmarcRecord.rdata, "mailto:reports@dmarc.cyber.dhs.gov") +} + +tests[{ + "Requirement" : "The DMARC point of contact for aggregate reports SHALL include reports@dmarc.cyber.dhs.gov", + "Control" : "EXO 2.4", + "Criticality" : "Shall", + "Commandlet" : "Resolve-DnsName", + "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[_] + count(split(DmarcRecord.rdata, "@")) < 3 +} + +tests[{ + "Requirement" : "An agency point of contact SHOULD be included for aggregate and/or failure reports", + "Control" : "EXO 2.4", + "Criticality" : "Shall", + "Commandlet" : "Resolve-DnsName", + "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 +#-- +tests[{ + "Requirement" : "SMTP AUTH SHALL be disabled in Exchange Online", + "Control" : "EXO 2.5", + "Criticality" : "Shall", + "Commandlet" : "Get-TransportConfig", + "ActualValue" : TransportConfig.SmtpClientAuthenticationDisabled, + "ReportDetails" : ReportDetailsBoolean(Status), + "RequirementMet" : Status +}] { + TransportConfig := input.transport_config + Status := TransportConfig.SmtpClientAuthenticationDisabled == true +} +#-- + + +################ +# Baseline 2.6 # +################ + +# Are both the tests supposed to be the same? + +# +# Baseline 2.6: Policy 1 +#-- +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" : SharingPolicy.Domains, + "ReportDetails" : ReportDetailsString(Status, ErrorMessage), + "RequirementMet" : Status +}] { + SharingPolicy := input.sharing_policy + InList := "*" in SharingPolicy.Domains + ErrorMessage := "Wildcard domain (\"*\") in shared domains list, enabling sharing will all domains by default" + Status := InList == false +} +#-- + +# +# 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" : SharingPolicy.Domains, + "ReportDetails" : ReportDetailsString(Status, ErrorMessage), + "RequirementMet" : Status +}] { + SharingPolicy := input.sharing_policy + InList := "*" in SharingPolicy.Domains + ErrorMessage := "Wildcard domain (\"*\") in shared domains list, enabling sharing will all domains by default" + Status := InList == false +} +#-- + + +################ +# 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 with that applies to emails received from outside the organization" + Conditions := [IsCorrectScope | IsCorrectScope = Rules[_].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 +#-- +tests[{ + "Requirement" : "IP allow lists SHOULD NOT be created", + "Control" : "EXO 2.12", + "Criticality" : "Should", + "Commandlet" : "Get-HostedConnectionFilterPolicy", + "ActualValue" : AllowList, + "ReportDetails" : ReportDetailsString(Status, ErrorMessage), + "RequirementMet" : Status +}]{ + AllowList := input.conn_filter.IPAllowList + ErrorMessage := "Allow-list is in use" + Status := count(AllowList) == 0 +} +#-- + +# +# Baseline 2.12: Policy 2 +#-- +tests[{ + "Requirement" : "Safe lists SHOULD NOT be enabled", + "Control" : "EXO 2.12", + "Criticality" : "Should", + "Commandlet" : "Get-HostedConnectionFilterPolicy", + "ActualValue" : EnableSafeList, + "ReportDetails" : ReportDetailsBoolean(Status), + "RequirementMet" : Status +}]{ + EnableSafeList := input.conn_filter.EnableSafeList + Status := EnableSafeList == false +} +#-- + + +################# +# Baseline 2.13 # +################# + +# +# Baseline 2.13: Policy 1 +#-- +tests[{ + "Requirement" : "Mailbox auditing SHALL be enabled", + "Control" : "EXO 2.13", + "Criticality" : "Shall", + "Commandlet" : "Get-OrganizationConfig", + "ActualValue" : AuditDisabled, + "ReportDetails" : ReportDetailsBoolean(Status), + "RequirementMet" : Status +}] { + AuditDisabled := input.org_config.AuditDisabled + Status := AuditDisabled == false +} +#-- + + +################# +# 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 +} +#-- \ No newline at end of file diff --git a/Rego/OneDriveConfig.rego b/Rego/OneDriveConfig.rego new file mode 100644 index 0000000000..26a7e3bb34 --- /dev/null +++ b/Rego/OneDriveConfig.rego @@ -0,0 +1,178 @@ +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 +#-- +tests[{ + "Requirement" : "Anyone links SHOULD be disabled", + "Control" : "OneDrive 2.1", + "Criticality" : "Should", + "Commandlet" : "Get-SPOTenant", + "ActualValue" : TenantInfo.OneDriveLoopSharingCapability, + "ReportDetails" : ReportDetailsBoolean(Status), + "RequirementMet" : Status +}] { + TenantInfo := input.SPO_tenant_info + Status := TenantInfo.OneDriveLoopSharingCapability == 1 +} +#-- + + +################ +# Baseline 2.2 # +################ + +# +# Baseline 2.2: Policy 1 +#-- +tests[{ + "Requirement" : "An expiration date SHOULD be set for Anyone links", + "Control" : "OneDrive 2.2", + "Criticality" : "Should", + "Commandlet" : "Get-SPOTenant", + "ActualValue" : TenantInfo.ExternalUserExpirationRequired, + "ReportDetails" : ReportDetailsBoolean(Status), + "RequirementMet" : Status +}] { + TenantInfo := input.SPO_tenant_info + Status := TenantInfo.ExternalUserExpirationRequired== true +} +#-- + +# +# Baseline 2.2: Policy 2 +#-- +tests[{ + "Requirement" : "Expiration date SHOULD be set to thirty days", + "Control" : "OneDrive 2.2", + "Criticality" : "Should", + "Commandlet" : "Get-SPOTenant", + "ActualValue" : TenantInfo.ExternalUserExpireInDays, + "ReportDetails" : ReportDetailsBoolean(Status), + "RequirementMet" : Status +}] { + TenantInfo := input.SPO_tenant_info + Status := TenantInfo.ExternalUserExpireInDays == 30 +} +#-- + + +################ +# Baseline 2.3 # +################ + +# +# Baseline 2.3: Policy 1 +#-- +tests[{ + "Requirement" : "Anyone link permissions SHOULD be limited to View", + "Control" : "OneDrive 2.3", + "Criticality" : "Should", + "Commandlet" : "Get-SPOTenant", + "ActualValue" : TenantInfo.DefaultLinkPermission, + "ReportDetails" : ReportDetailsBoolean(Status), + "RequirementMet" : Status +}] { + TenantInfo := input.SPO_tenant_info + Status := TenantInfo.DefaultLinkPermission == 1 +} +#-- + + +################ +# Baseline 2.4 # +################ + +# +# Baseline 2.4: Policy 1 +#-- +tests[{ + "Requirement" : "OneDrive Client for Windows SHALL be restricted to agency-Defined Domain(s)", + "Control" : "OneDrive 2.4", + "Criticality" : "Shall", + "Commandlet" : "Get-SPOTenant", + "ActualValue" : ["Domain GUID: ", Domain], + "ReportDetails" : ReportDetailsBoolean(Status), + "RequirementMet" : Status +}] { + TenantSyncInfo := input.Tenant_sync_info + Domain := input.Expected_results.Owner + Status := Domain in TenantSyncInfo.AllowedDomainList +} +#-- + + +################ +# Baseline 2.5 # +################ + +# +# Baseline 2.5: Policy 1 +#-- +tests[{ + "Requirement" : "OneDrive Client Sync SHALL only be allowed only within the local domain", + "Control" : "OneDrive 2.5", + "Criticality" : "Shall", + "Commandlet" : "Get-SPOTenant", + "ActualValue" : TenantSyncInfo.BlockMacSync, + "ReportDetails" : ReportDetailsBoolean(Status), + "RequirementMet" : Status +}] { + TenantSyncInfo := input.Tenant_sync_info + Status := TenantSyncInfo.BlockMacSync == false +} +#-- + + +################ +# 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 +} +#-- \ No newline at end of file diff --git a/Rego/PowerPlatformConfig.rego b/Rego/PowerPlatformConfig.rego new file mode 100644 index 0000000000..abbfa82a9d --- /dev/null +++ b/Rego/PowerPlatformConfig.rego @@ -0,0 +1,247 @@ +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 additional 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.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 +}] { + ErrorMessage := "Subsequent environments without DLP policies:" + Status := count(EnvWithoutPolicies) == 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 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 +}] { + ErrorMessage := "Connectors are allowed that should be blocked:" + RogueConnectors := (ConnectorSet - AllowedInBaseline) + Status := count(RogueConnectors) == 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 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 +} +#-- \ No newline at end of file diff --git a/Rego/SharepointConfig.rego b/Rego/SharepointConfig.rego new file mode 100644 index 0000000000..8ea1005f84 --- /dev/null +++ b/Rego/SharepointConfig.rego @@ -0,0 +1,133 @@ +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", + "ActualValue" : SPOTenant.DefaultSharingLinkType, + "ReportDetails" : ReportDetailsBoolean(Status), + "RequirementMet" : Status +}] { + SPOTenant := input.SPO_tenant + Status := SPOTenant.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", + "ActualValue" : SPOTenant.SharingCapability, + "ReportDetails" : ReportDetailsBoolean(Status), + "RequirementMet" : Status +}] { + SPOTenant := input.SPO_tenant + Status := SPOTenant.SharingCapability == 1 +} +#-- + + +################ +# 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 +#-- +tests[{ + "Requirement" : "Expiration timers for 'guest access to a site or OneDrive' and 'people who use a verification code' SHOULD be set", + "Control" : "Sharepoint 2.4", + "Criticality" : "Should", + "Commandlet" : "Get-SPOTenant", + "ActualValue" : SPOTenant.ExternalUserExpirationRequired, + "ReportDetails" : ReportDetailsBoolean(Status), + "RequirementMet" : Status +}] { + SPOTenant := input.SPO_tenant + Status := SPOTenant.ExternalUserExpirationRequired == true +} +#-- + +# +# Baseline 2.4: Policy 2 +#-- +tests[{ + "Requirement" : "Expiration timers SHOULD be set to 30 days", + "Control" : "Sharepoint 2.4", + "Criticality" : "Should", + "Commandlet" : "Get-SPOTenant", + "ActualValue" : SPOTenant.ExternalUserExpireInDays, + "ReportDetails" : ReportDetailsBoolean(Status), + "RequirementMet" : Status +}] { + SPOTenant := input.SPO_tenant + Status := SPOTenant.ExternalUserExpireInDays == 30 +} +#-- + + +################ +# Baseline 2.5 # +################ + +# +# Baseline 2.5: Policy 1 +#-- +tests[{ + "Requirement" : "Users SHALL be prevented from running custom scripts", + "Control" : "Sharepoint 2.5", + "Criticality" : "Shall", + "Commandlet" : "Get-SPOSite -Identity", + "ActualValue" : SPOSite.DenyAddAndCustomizePages, + "ReportDetails" : ReportDetailsBoolean(Status), + "RequirementMet" : Status +}] { + SPOSite := input.SPO_site + Status := SPOSite.DenyAddAndCustomizePages == 1 +} +#-- \ No newline at end of file diff --git a/Rego/TeamsConfig.rego b/Rego/TeamsConfig.rego new file mode 100644 index 0000000000..e07d027e6d --- /dev/null +++ b/Rego/TeamsConfig.rego @@ -0,0 +1,660 @@ +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 +} +#-- + +# +# 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.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, ComfirmCorrectConfig, Policies) = Description if { + IsGCC == true + Description := "N/A: Feature is unavailable in GCC environments" +} + +ReportDetails2_7(IsGCC, ComfirmCorrectConfig, Policies) = Description if { + IsGCC == false + ComfirmCorrectConfig == true + Description := "Requirement met" +} + +ReportDetails2_7(IsGCC, ComfirmCorrectConfig, Policies) = Description if { + IsGCC == false + ComfirmCorrectConfig == false + Detail := "Requirement not met: Email integration is enabled across domain:" + Description := ReportDetailsArray(false, Policies, Detail) +} + +tests[{ + "Requirement" : "Teams email integration SHALL be disabled", + "Control" : "Teams 2.7", + "Criticality" : "Shall", + "Commandlet" : "Get-CsTeamsClientConfiguration", + "ActualValue" : [Policies, ServiceInstance], + "ReportDetails" : ReportDetails2_7(IsGCC, ComfirmCorrectConfig, Policies), + "RequirementMet" : Status +}] { + TenantConfig := input.teams_tenant_info[_] + ServiceInstance := TenantConfig.ServiceInstance + Policies := ConfigsAllowingEmail + ComfirmCorrectConfig := count(Policies) ==0 + IsGCC := indexof(ServiceInstance, "GOV") != -1 + Conditions := [ComfirmCorrectConfig, 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), + "ExpectedValue" : "false", + "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 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-CsTeamsMeetingPolicy", + "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.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 +} +#-- diff --git a/RegoCachedProviderTesting.ps1 b/RegoCachedProviderTesting.ps1 new file mode 100644 index 0000000000..49bbb4b0ce --- /dev/null +++ b/RegoCachedProviderTesting.ps1 @@ -0,0 +1,35 @@ +# +# 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 +$ExportProvider = $true + +$LogIn = $true # Set $true to authenticate yourself to a tenant or if you are already authenticated set to $false to avoid reauthentication +$ProductNames = @("aad", "defender", "exo", "onedrive", "powerplatform", "sharepoint", "teams") +$Endpoint = "usgov" # Mandatory parameter if running Power Platform +$OPAPath = "./" # Path to OPA Executable +$OutPath = "./output" # output directory + +$RunCachedParams = @{ + 'Login' = $Login; + 'ProductNames' = $ProductNames; + 'OPAPath' = $OPAPath; + 'ExportProvider' = $ExportProvider; + 'Endpoint' = $Endpoint; + 'OutPath' = $OutPath; +} + +$ManifestPath = Join-Path -Path "./PowerShell" -ChildPath "ScubaGear" +Remove-Module "SCuBA" -ErrorAction "SilentlyContinue" # For dev work +####### +Import-Module $ManifestPath -ErrorAction Stop +Invoke-RunCached @RunCachedParams diff --git a/RunSCuBA.ps1 b/RunSCuBA.ps1 new file mode 100644 index 0000000000..52d2cab390 --- /dev/null +++ b/RunSCuBA.ps1 @@ -0,0 +1,35 @@ +<# + # For end user modifications. + # See README for detailed instructions on which parameters to change. +#> + +param ( + [switch] + $Version +) + +$LogIn = $true # Set $true to authenticate yourself to a tenant or if you are already authenticated set to $false to avoid reauthentication +$ProductNames = @("aad", "defender", "exo", "onedrive", "sharepoint", "teams") # The specific products that you want the tool to assess. +$Endpoint = "usgov" # Mandatory parameter if running Power Platform. Valid options are "dod", "prod","preview","tip1", "tip2", "usgov", or "usgovhigh". +$OutPath = "./Reports" # Report output directory path. Leave as-is if you want the Reports folder to be created in the same directory where the script is executed. +$OPAPath = "./" # Path to the OPA Executable. Leave this as-is for most cases. + +$SCuBAParams = @{ + 'Login' = $Login; + 'ProductNames' = $ProductNames; + 'Endpoint' = $Endpoint; + 'OPAPath' = $OPAPath; + 'OutPath' = $OutPath; +} + +$ManifestPath = Join-Path -Path "./PowerShell" -ChildPath "ScubaGear" +####### +Import-Module -Name $ManifestPath -ErrorAction Stop +if ($Version) { + Invoke-SCuBA @SCuBAParams -Version +} +else { + Invoke-SCuBA @SCuBAParams +} +Remove-Module "ScubaGear" -ErrorAction "SilentlyContinue" +####### diff --git a/SetUp.ps1 b/SetUp.ps1 new file mode 100644 index 0000000000..630002958f --- /dev/null +++ b/SetUp.ps1 @@ -0,0 +1,29 @@ +# +# This script installs the required Powershell modules used by the assessment tool. +# + +$ModuleList = @( + "PowerShellGet", + "MicrosoftTeams", + "ExchangeOnlineManagement", # includes Defender + "Microsoft.Online.SharePoint.PowerShell", # includes OneDrive + "Microsoft.PowerApps.Administration.PowerShell", + "Microsoft.PowerApps.PowerShell", + "Microsoft.Graph.Applications", # starting here, modules for AAD + "Microsoft.Graph.Authentication", + "Microsoft.Graph.DeviceManagement", + "Microsoft.Graph.DeviceManagement.Administration", + "Microsoft.Graph.DeviceManagement.Enrolment", + "Microsoft.Graph.Devices.CorporateManagement", + "Microsoft.Graph.Groups", + "Microsoft.Graph.Identity.DirectoryManagement", + "Microsoft.Graph.Identity.Governance", + "Microsoft.Graph.Identity.SignIns", + "Microsoft.Graph.Planner", + "Microsoft.Graph.Teams", + "Microsoft.Graph.Users" + ) + +foreach($Module in $ModuleList) { + Install-Module -Name $Module -Force -AllowClobber -Scope CurrentUser -Verbose +} diff --git a/Testing/AAD/AADConfig2_01_test.rego b/Testing/AAD/AADConfig2_01_test.rego new file mode 100644 index 0000000000..8daddff03e --- /dev/null +++ b/Testing/AAD/AADConfig2_01_test.rego @@ -0,0 +1,198 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_Conditions_Correct if { + ControlNumber := "AAD 2.1" + Requirement := "Legacy authentication SHALL be blocked" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + }, + "ClientAppTypes": ["other", "exchangeActiveSync"] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "enabled", + "DisplayName": "Test block Legacy Authentication" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 conditional access policy(s) found that meet(s) all requirements:
Test block Legacy Authentication" +} + +test_IncludeApplications_Incorrect if { + ControlNumber := "AAD 2.1" + Requirement := "Legacy authentication SHALL be blocked" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["Office365"] + }, + "Users": { + "IncludeUsers": ["All"] + }, + "ClientAppTypes": ["other", "exchangeActiveSync"] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "enabled", + "DisplayName": "Test block Legacy Authentication" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_IncludeUsers_Incorrect if { + ControlNumber := "AAD 2.1" + Requirement := "Legacy authentication SHALL be blocked" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["49b4dcdf-1f90-41a5-9dd7-5e7c3609b423"] + }, + "ClientAppTypes": ["other", "exchangeActiveSync"] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "enabled", + "DisplayName": "Test block Legacy Authentication" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_ClientAppTypes_Incorrect if { + ControlNumber := "AAD 2.1" + Requirement := "Legacy authentication SHALL be blocked" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + }, + "ClientAppTypes": [""] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "enabled", + "DisplayName": "Test block Legacy Authentication" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_BuiltInControls_Incorrect if { + ControlNumber := "AAD 2.1" + Requirement := "Legacy authentication SHALL be blocked" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + }, + "ClientAppTypes": ["other", "exchangeActiveSync"] + }, + "GrantControls": { + "BuiltInControls": null + }, + "State": "enabled", + "DisplayName": "Test block Legacy Authentication" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_State_Incorrect if { + ControlNumber := "AAD 2.1" + Requirement := "Legacy authentication SHALL be blocked" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + }, + "ClientAppTypes": ["other", "exchangeActiveSync"] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "disabled", + "DisplayName": "Test block Legacy Authentication" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} \ No newline at end of file diff --git a/Testing/AAD/AADConfig2_02_test.rego b/Testing/AAD/AADConfig2_02_test.rego new file mode 100644 index 0000000000..6b44737dae --- /dev/null +++ b/Testing/AAD/AADConfig2_02_test.rego @@ -0,0 +1,274 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_Conditions_Correct if { + ControlNumber := "AAD 2.2" + Requirement := "Users detected as high risk SHALL be blocked" + + Output := tests with input as + {"conditional_access_policies": [ + { + "Conditions": { + "Applications": {"IncludeApplications": ["All"]}, + "Users": {"IncludeUsers": ["All"]}, + "UserRiskLevels": ["high"] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "enabled", + "DisplayName": "Test name" + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 conditional access policy(s) found that meet(s) all requirements:
Test name" +} + +test_IncludeApplications_Incorrect if { + ControlNumber := "AAD 2.2" + Requirement := "Users detected as high risk SHALL be blocked" + + Output := tests with input as + {"conditional_access_policies": [ + { + "Conditions": { + "Applications": {"IncludeApplications": ["Office365"]}, + "Users": {"IncludeUsers": ["All"]}, + "UserRiskLevels": ["high"] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "enabled", + "DisplayName": "Test name" + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_IncludeUsers_Incorrect if { + ControlNumber := "AAD 2.2" + Requirement := "Users detected as high risk SHALL be blocked" + + Output := tests with input as + {"conditional_access_policies": [ + { + "Conditions": { + "Applications": {"IncludeApplications": ["All"]}, + "Users": {"IncludeUsers": ["8bc7c6ee-39a2-42a5-a31b-f77fb51db652"]}, + "UserRiskLevels": ["high"] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "enabled", + "DisplayName": "Test name" + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_BuiltInControls_Incorrect if { + ControlNumber := "AAD 2.2" + Requirement := "Users detected as high risk SHALL be blocked" + + Output := tests with input as + {"conditional_access_policies": [ + { + "Conditions": { + "Applications": {"IncludeApplications": ["All"]}, + "Users": {"IncludeUsers": ["All"]}, + "UserRiskLevels": ["high"] + }, + "GrantControls": { + "BuiltInControls": [""] + }, + "State": "enabled", + "DisplayName": "Test name" + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_State_Incorrect if { + ControlNumber := "AAD 2.2" + Requirement := "Users detected as high risk SHALL be blocked" + + Output := tests with input as + {"conditional_access_policies": [ + { + "Conditions": { + "Applications": {"IncludeApplications": ["All"]}, + "Users": {"IncludeUsers": ["All"]}, + "UserRiskLevels": ["high"] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "disabled", + "DisplayName": "Test name" + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_UserRiskLevels_Incorrect if { + ControlNumber := "AAD 2.2" + Requirement := "Users detected as high risk SHALL be blocked" + + Output := tests with input as + {"conditional_access_policies": [ + { + "Conditions": { + "Applications": {"IncludeApplications": ["All"]}, + "Users": {"IncludeUsers": ["All"]}, + "UserRiskLevels": [""] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "enabled", + "DisplayName": "Test name" + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_ServicePlans_Incorrect if { + ControlNumber := "AAD 2.2" + Requirement := "Users detected as high risk SHALL be blocked" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + }, + "UserRiskLevels": [""] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "enabled", + "DisplayName": "Test name" + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "**NOTE: Your tenant does not have an Azure AD Premium P2 license, which is required for this feature**" +} + +# +# Policy 2 +#-- +test_NotImplemented_Correct if { + ControlNumber := "AAD 2.2" + Requirement := "A notification SHOULD be sent to the administrator when high-risk users are detected" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Azure Active Directory Secure Configuration Baseline policy 2.2 for instructions on manual check" +} \ No newline at end of file diff --git a/Testing/AAD/AADConfig2_03_test.rego b/Testing/AAD/AADConfig2_03_test.rego new file mode 100644 index 0000000000..bdf6f80674 --- /dev/null +++ b/Testing/AAD/AADConfig2_03_test.rego @@ -0,0 +1,222 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_Conditions_Correct if { + ControlNumber := "AAD 2.3" + Requirement := "Sign-ins detected as high risk SHALL be blocked" + + Output := tests with input as + {"conditional_access_policies": [ + { + "Conditions": { + "Applications": {"IncludeApplications": ["All"]}, + "Users": {"IncludeUsers": ["All"]}, + "SignInRiskLevels": ["high"] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "enabled", + "DisplayName": "Test name" + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 conditional access policy(s) found that meet(s) all requirements:
Test name" +} + +test_IncludeApplications_Incorrect if { + ControlNumber := "AAD 2.3" + Requirement := "Sign-ins detected as high risk SHALL be blocked" + + Output := tests with input as + {"conditional_access_policies": [ + { + "Conditions": { + "Applications": {"IncludeApplications": ["Office365"]}, + "Users": {"IncludeUsers": ["All"]}, + "SignInRiskLevels": ["high"] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "enabled", + "DisplayName": "Test name" + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_IncludeUsers_Incorrect if { + ControlNumber := "AAD 2.3" + Requirement := "Sign-ins detected as high risk SHALL be blocked" + + Output := tests with input as + {"conditional_access_policies": [ + { + "Conditions": { + "Applications": {"IncludeApplications": ["All"]}, + "Users": {"IncludeUsers": ["8bc7c6ee-39a2-42a5-a31b-f77fb51db652"]}, + "SignInRiskLevels": ["high"] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "enabled", + "DisplayName": "Test name" + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_SignInRiskLevels_Incorrect if { + ControlNumber := "AAD 2.3" + Requirement := "Sign-ins detected as high risk SHALL be blocked" + + Output := tests with input as + {"conditional_access_policies": [ + { + "Conditions": { + "Applications": {"IncludeApplications": ["All"]}, + "Users": {"IncludeUsers": ["All"]}, + "SignInRiskLevels": [""] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "enabled", + "DisplayName": "Test name" + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_BuiltInControls_Incorrect if { + ControlNumber := "AAD 2.3" + Requirement := "Sign-ins detected as high risk SHALL be blocked" + + Output := tests with input as + {"conditional_access_policies": [ + { + "Conditions": { + "Applications": {"IncludeApplications": ["All"]}, + "Users": {"IncludeUsers": ["All"]}, + "SignInRiskLevels": ["high"] + }, + "GrantControls": { + "BuiltInControls": [""] + }, + "State": "enabled", + "DisplayName": "Test name" + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_State_Incorrect if { + ControlNumber := "AAD 2.3" + Requirement := "Sign-ins detected as high risk SHALL be blocked" + + Output := tests with input as + {"conditional_access_policies": [ + { + "Conditions": { + "Applications": {"IncludeApplications": ["All"]}, + "Users": {"IncludeUsers": ["All"]}, + "SignInRiskLevels": ["high"] + }, + "GrantControls": { + "BuiltInControls": ["block"] + }, + "State": "disabled", + "DisplayName": "Test name" + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} \ No newline at end of file diff --git a/Testing/AAD/AADConfig2_04_test.rego b/Testing/AAD/AADConfig2_04_test.rego new file mode 100644 index 0000000000..5b901ed100 --- /dev/null +++ b/Testing/AAD/AADConfig2_04_test.rego @@ -0,0 +1,209 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_ConditionalAccessPolicies_Correct if { + ControlNumber := "AAD 2.4" + Requirement := "MFA SHALL be required for all users" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "GrantControls": { + "BuiltInControls": ["mfa"] + }, + "State": "enabled", + "DisplayName": "Test Policy require MFA for All Users" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput)>= 1 + RuleOutput[0].RequirementMet + endswith(RuleOutput[0].ReportDetails, "conditional access policy(s) found that meet(s) all requirements.
Note: Policy exclusions and additional policy conditions may still 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.") +} + +test_IncludeApplications_Incorrect if { + ControlNumber := "AAD 2.4" + Requirement := "MFA SHALL be required for all users" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["Office365"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "GrantControls": { + "BuiltInControls": ["mfa"] + }, + "State": "enabled", + "DisplayName": "Test Policy require MFA for All Users, but not all Apps" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements.
Note: Policy exclusions and additional policy conditions may still 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." +} + +test_IncludeUsers_Incorrect if { + ControlNumber := "AAD 2.4" + Requirement := "MFA SHALL be required for all users" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["8bc7c6ee-39a2-42a5-a31b-f77fb51db652"] + } + }, + "GrantControls": { + "BuiltInControls": ["mfa"] + }, + "State": "enabled", + "DisplayName": "Test Policy require MFA for All Apps, but not All Users" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements.
Note: Policy exclusions and additional policy conditions may still 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." +} + +test_BuiltInControls_Incorrect if { + ControlNumber := "AAD 2.4" + Requirement := "MFA SHALL be required for all users" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "GrantControls": { + "BuiltInControls": [""] + }, + "State": "enabled", + "DisplayName": "Test Policy does not require MFA" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements.
Note: Policy exclusions and additional policy conditions may still 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." +} + +test_State_Incorrect if { + ControlNumber := "AAD 2.4" + Requirement := "MFA SHALL be required for all users" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "GrantControls": { + "BuiltInControls": ["mfa"] + }, + "State": "disabled", + "DisplayName": "Test Policy is correct, but not enabled" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements.
Note: Policy exclusions and additional policy conditions may still 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." +} + +# +# Policy 2 +#-- +test_NotImplemented_Correct_V1 if { + ControlNumber := "AAD 2.4" + Requirement := "Phishing-resistant MFA SHALL be used for all users" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Azure Active Directory Secure Configuration Baseline policy 2.4 for instructions on manual check" +} + +# +# Policy 3 +#-- +test_NotImplemented_Correct_V2 if { + ControlNumber := "AAD 2.4" + Requirement := "If phishing-resistant MFA cannot be used, an MFA method from the list [see AAD baseline 2.4] SHALL be used in the interim" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Azure Active Directory Secure Configuration Baseline policy 2.4 for instructions on manual check" +} + +# +# Policy 4 +#-- +test_NotImplemented_Correct_V3 if { + ControlNumber := "AAD 2.4" + Requirement := "SMS or Voice as the MFA method SHALL NOT be used" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Azure Active Directory Secure Configuration Baseline policy 2.4 for instructions on manual check" +} \ No newline at end of file diff --git a/Testing/AAD/AADConfig2_05_test.rego b/Testing/AAD/AADConfig2_05_test.rego new file mode 100644 index 0000000000..529a8a0e9a --- /dev/null +++ b/Testing/AAD/AADConfig2_05_test.rego @@ -0,0 +1,35 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_NotImplemented_Correct_V1 if { + ControlNumber := "AAD 2.5" + Requirement := "The following critical logs SHALL be sent at a minimum: AuditLogs, SignInLogs, RiskyUsers, UserRiskEvents, NonInteractiveUserSignInLogs, ServicePrincipalSignInLogs, ADFSSignInLogs, RiskyServicePrincipals, ServicePrincipalRiskEvents" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Azure Active Directory Secure Configuration Baseline policy 2.5 for instructions on manual check" +} + +# +# Policy 2 +#-- +test_NotImplemented_Correct_V2 if { + ControlNumber := "AAD 2.5" + Requirement := "The logs SHALL be sent to the agency's SOC for monitoring" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Azure Active Directory Secure Configuration Baseline policy 2.5 for instructions on manual check" +} \ No newline at end of file diff --git a/Testing/AAD/AADConfig2_06_test.rego b/Testing/AAD/AADConfig2_06_test.rego new file mode 100644 index 0000000000..428ef8c358 --- /dev/null +++ b/Testing/AAD/AADConfig2_06_test.rego @@ -0,0 +1,47 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_AllowedToCreateApps_Correct if { + ControlNumber := "AAD 2.6" + Requirement := "Only administrators SHALL be allowed to register third-party applications" + + Output := tests with input as { + "authorization_policies": { + "DefaultUserRolePermissions": { + "AllowedToCreateApps": false + } + } + } + + # filter for just the output produced by the specific rule by + # checking 1) the control number and 2) the requirement string + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + # Each rule should produce exactly 1 line of output in the report + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowedToCreateApps_Incorrect if { + ControlNumber := "AAD 2.6" + Requirement := "Only administrators SHALL be allowed to register third-party applications" + + Output := tests with input as { + "authorization_policies": { + "DefaultUserRolePermissions": { + "AllowedToCreateApps": true + } + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} \ No newline at end of file diff --git a/Testing/AAD/AADConfig2_07_test.rego b/Testing/AAD/AADConfig2_07_test.rego new file mode 100644 index 0000000000..df7c69169a --- /dev/null +++ b/Testing/AAD/AADConfig2_07_test.rego @@ -0,0 +1,130 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_PermissionGrantPolicyIdsAssignedToDefaultUserRole_Correct if { + ControlNumber := "AAD 2.7" + Requirement := "Only administrators SHALL be allowed to consent to third-party applications" + + Output := tests with input as { + "authorization_policies": { + "PermissionGrantPolicyIdsAssignedToDefaultUserRole": [] + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_PermissionGrantPolicyIdsAssignedToDefaultUserRole_Incorrect if { + ControlNumber := "AAD 2.7" + Requirement := "Only administrators SHALL be allowed to consent to third-party applications" + + Output := tests with input as { + "authorization_policies": { + "PermissionGrantPolicyIdsAssignedToDefaultUserRole": [ + "Test User" + ] + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +# +# Policy 2 +#-- +test_IsEnabled_Correct if { + ControlNumber := "AAD 2.7" + Requirement := "An admin consent workflow SHALL be configured" + + Output := tests with input as { + "admin_consent_policies": { + "IsEnabled" : true + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_IsEnabled_Incorrect if { + ControlNumber := "AAD 2.7" + Requirement := "An admin consent workflow SHALL be configured" + + Output := tests with input as { + "admin_consent_policies": { + "IsEnabled" : false + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +# +# Policy 3 +#-- +test_Value_Correct if { + ControlNumber := "AAD 2.7" + Requirement := "Group owners SHALL NOT be allowed to consent to third-party applications" + + Output := tests with input as { + "directory_settings": [ + { + "Values" : [ + { + "Name" : "EnableGroupSpecificConsent", + "Value" : "false" + } + ] + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Value_Incorrect if { + ControlNumber := "AAD 2.7" + Requirement := "Group owners SHALL NOT be allowed to consent to third-party applications" + + Output := tests with input as { + "directory_settings": [ + { + "Values" : [ + { + "Name" : "EnableGroupSpecificConsent", + "Value" : "true" + } + ] + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} \ No newline at end of file diff --git a/Testing/AAD/AADConfig2_08_test.rego b/Testing/AAD/AADConfig2_08_test.rego new file mode 100644 index 0000000000..009b21f2b1 --- /dev/null +++ b/Testing/AAD/AADConfig2_08_test.rego @@ -0,0 +1,19 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_NotImplemented_Correct if { + ControlNumber := "AAD 2.8" + Requirement := "User passwords SHALL NOT expire" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Azure Active Directory Secure Configuration Baseline policy 2.8 for instructions on manual check" +} \ No newline at end of file diff --git a/Testing/AAD/AADConfig2_09_test.rego b/Testing/AAD/AADConfig2_09_test.rego new file mode 100644 index 0000000000..cdeca30fe9 --- /dev/null +++ b/Testing/AAD/AADConfig2_09_test.rego @@ -0,0 +1,250 @@ +package aad +import future.keywords + +# +# Policy 1 +#-- +test_ConditionalAccessPolicies_Correct if { + ControlNumber := "AAD 2.9" + Requirement := "Sign-in frequency SHALL be configured to 12 hours" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "SessionControls": { + "SignInFrequency": { + "IsEnabled" : true, + "Type" : "hours", + "Value" : 12 + } + }, + "State": "enabled", + "DisplayName" : "Test Name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 conditional access policy(s) found that meet(s) all requirements:
Test Name" +} + +test_IncludeApplications_Incorrect if { + ControlNumber := "AAD 2.9" + Requirement := "Sign-in frequency SHALL be configured to 12 hours" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": [] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "SessionControls": { + "SignInFrequency": { + "IsEnabled" : true, + "Type" : "hours", + "Value" : 12 + } + }, + "State": "enabled", + "DisplayName" : "Test Name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_IncludeUsers_Incorrect if { + ControlNumber := "AAD 2.9" + Requirement := "Sign-in frequency SHALL be configured to 12 hours" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": [] + } + }, + "SessionControls": { + "SignInFrequency": { + "IsEnabled" : true, + "Type" : "hours", + "Value" : 12 + } + }, + "State": "enabled", + "DisplayName" : "Test Name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_IsEnabled_Incorrect if { + ControlNumber := "AAD 2.9" + Requirement := "Sign-in frequency SHALL be configured to 12 hours" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "SessionControls": { + "SignInFrequency": { + "IsEnabled" : false, + "Type" : "hours", + "Value" : 12 + } + }, + "State": "enabled", + "DisplayName" : "Test Name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_Type_Incorrect if { + ControlNumber := "AAD 2.9" + Requirement := "Sign-in frequency SHALL be configured to 12 hours" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "SessionControls": { + "SignInFrequency": { + "IsEnabled" : true, + "Type" : "Hello World", + "Value" : 12 + } + }, + "State": "enabled", + "DisplayName" : "Test Name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_Value_Incorrect if { + ControlNumber := "AAD 2.9" + Requirement := "Sign-in frequency SHALL be configured to 12 hours" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "SessionControls": { + "SignInFrequency": { + "IsEnabled" : true, + "Type" : "hours", + "Value" : 24 + } + }, + "State": "enabled", + "DisplayName" : "Test Name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_State_Incorrect if { + ControlNumber := "AAD 2.9" + Requirement := "Sign-in frequency SHALL be configured to 12 hours" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "SessionControls": { + "SignInFrequency": { + "IsEnabled" : true, + "Type" : "hours", + "Value" : 12 + } + }, + "State": "disabled", + "DisplayName" : "Test Name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} \ No newline at end of file diff --git a/Testing/AAD/AADConfig2_10_test.rego b/Testing/AAD/AADConfig2_10_test.rego new file mode 100644 index 0000000000..1ccdb28cb2 --- /dev/null +++ b/Testing/AAD/AADConfig2_10_test.rego @@ -0,0 +1,210 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_ConditionalAccessPolicies_Correct if { + ControlNumber := "AAD 2.10" + Requirement := "Browser sessions SHALL not be persistent" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "SessionControls": { + "PersistentBrowser": { + "IsEnabled" : true, + "Mode" : "never" + } + }, + "State": "enabled", + "DisplayName" : "Test Name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 conditional access policy(s) found that meet(s) all requirements:
Test Name" +} + +test_IncludeApplications_Incorrect if { + ControlNumber := "AAD 2.10" + Requirement := "Browser sessions SHALL not be persistent" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": [] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "SessionControls": { + "PersistentBrowser": { + "IsEnabled" : true, + "Mode" : "never" + } + }, + "State": "enabled", + "DisplayName" : "Test Name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_IncludeUsers_Incorrect if { + ControlNumber := "AAD 2.10" + Requirement := "Browser sessions SHALL not be persistent" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": [] + } + }, + "SessionControls": { + "PersistentBrowser": { + "IsEnabled" : true, + "Mode" : "never" + } + }, + "State": "enabled", + "DisplayName" : "Test Name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_IsEnabled_Incorrect if { + ControlNumber := "AAD 2.10" + Requirement := "Browser sessions SHALL not be persistent" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "SessionControls": { + "PersistentBrowser": { + "IsEnabled" : false, + "Mode" : "never" + } + }, + "State": "enabled", + "DisplayName" : "Test Name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_Mode_Incorrect if { + ControlNumber := "AAD 2.10" + Requirement := "Browser sessions SHALL not be persistent" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "SessionControls": { + "PersistentBrowser": { + "IsEnabled" : true, + "Mode" : "always" + } + }, + "State": "enabled", + "DisplayName" : "Test Name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_State_Incorrect if { + ControlNumber := "AAD 2.10" + Requirement := "Browser sessions SHALL not be persistent" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "SessionControls": { + "PersistentBrowser": { + "IsEnabled" : true, + "Mode" : "never" + } + }, + "State": "disabled", + "DisplayName" : "Test Name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} \ No newline at end of file diff --git a/Testing/AAD/AADConfig2_11_test.rego b/Testing/AAD/AADConfig2_11_test.rego new file mode 100644 index 0000000000..829d5cd5e1 --- /dev/null +++ b/Testing/AAD/AADConfig2_11_test.rego @@ -0,0 +1,86 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_PrivilegedUsers_Correct if { + ControlNumber := "AAD 2.11" + Requirement := "A minimum of two users and a maximum of four users SHALL be provisioned with the Global Administrator role" + + Output := tests with input as { + "privileged_users" : { + "User1": { + "DisplayName": "Test Name1", + "roles": ["Privileged Role Administrator", "Global Administrator"] + }, + "User2": { + "DisplayName": "Test Name2", + "roles": ["Global Administrator"] + } + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "2 global admin(s) found:
Test Name1, Test Name2" +} + +test_PrivilegedUsers_Incorrect_V1 if { + ControlNumber := "AAD 2.11" + Requirement := "A minimum of two users and a maximum of four users SHALL be provisioned with the Global Administrator role" + + Output := tests with input as { + "privileged_users" : { + "User1": { + "DisplayName": "Test Name1", + "roles": ["Privileged Role Administrator", "Global Administrator"] + } + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 global admin(s) found:
Test Name1" +} + +test_PrivilegedUsers_Incorrect_V2 if { + ControlNumber := "AAD 2.11" + Requirement := "A minimum of two users and a maximum of four users SHALL be provisioned with the Global Administrator role" + + Output := tests with input as { + "privileged_users" : { + "User1": { + "DisplayName": "Test Name1", + "roles": ["Privileged Role Administrator", "Global Administrator"] + }, + "User2": { + "DisplayName": "Test Name2", + "roles": ["Global Administrator"] + }, + "User3": { + "DisplayName": "Test Name3", + "roles": ["Global Administrator"] + }, + "User4": { + "DisplayName": "Test Name4", + "roles": ["Global Administrator"] + }, + "User5": { + "DisplayName": "Test Name5", + "roles": ["Global Administrator"] + } + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "5 global admin(s) found:
Test Name1, Test Name2, Test Name3, Test Name4, Test Name5" +} \ No newline at end of file diff --git a/Testing/AAD/AADConfig2_12_test.rego b/Testing/AAD/AADConfig2_12_test.rego new file mode 100644 index 0000000000..9a774d1136 --- /dev/null +++ b/Testing/AAD/AADConfig2_12_test.rego @@ -0,0 +1,78 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_OnPremisesImmutableId_Correct if { + ControlNumber := "AAD 2.12" + Requirement := "Users that need to be assigned to highly privileged Azure AD roles SHALL be provisioned cloud-only accounts that are separate from the on-premises directory or other federated identity providers" + + Output := tests with input as { + "privileged_users": { + "User1": { + "DisplayName": "Alice", + "OnPremisesImmutableId": null, + "roles": ["Privileged Role Administrator", "Global Administrator"] + }, + "User2": { + "DisplayName": "Bob", + "OnPremisesImmutableId": null, + "roles": ["Global Administrator"] + } + } + } + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 admin(s) that are not cloud-only found" +} + +test_OnPremisesImmutableId_Incorrect_V1 if { + ControlNumber := "AAD 2.12" + Requirement := "Users that need to be assigned to highly privileged Azure AD roles SHALL be provisioned cloud-only accounts that are separate from the on-premises directory or other federated identity providers" + + Output := tests with input as { + "privileged_users": { + "User1": { + "DisplayName": "Alice", + "OnPremisesImmutableId": "HelloWorld", + "roles": ["Privileged Role Administrator", "Global Administrator"] + } + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 admin(s) that are not cloud-only found:
Alice" +} + +test_OnPremisesImmutableId_Incorrect_V2 if { + ControlNumber := "AAD 2.12" + Requirement := "Users that need to be assigned to highly privileged Azure AD roles SHALL be provisioned cloud-only accounts that are separate from the on-premises directory or other federated identity providers" + + Output := tests with input as { + "privileged_users": { + "User1": { + "DisplayName": "Alice", + "OnPremisesImmutableId": "HelloWorld", + "roles": ["Privileged Role Administrator", "Global Administrator"] + }, + "User2": { + "DisplayName": "Bob", + "OnPremisesImmutableId": null, + "roles": ["Global Administrator"] + } + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 admin(s) that are not cloud-only found:
Alice" +} \ No newline at end of file diff --git a/Testing/AAD/AADConfig2_13_test.rego b/Testing/AAD/AADConfig2_13_test.rego new file mode 100644 index 0000000000..2aab8a1b2d --- /dev/null +++ b/Testing/AAD/AADConfig2_13_test.rego @@ -0,0 +1,248 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_ConditionalAccessPolicies_Correct if { + ControlNumber := "AAD 2.13" + Requirement := "MFA SHALL be required for user access to highly privileged roles" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeRoles": ["Role1", "Role2" ] + } + }, + "GrantControls": { + "BuiltInControls": ["mfa"] + }, + "State": "enabled", + "DisplayName": "MFA required for all highly Privileged Roles Policy" + } + ], + "privileged_roles": [ + { + "RoleTemplateId": "Role1", + "DisplayName": "Global Administrator" + }, + { + "RoleTemplateId": "Role2", + "DisplayName": "Privileged Role Administrator" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 conditional access policy(s) found that meet(s) all requirements:
MFA required for all highly Privileged Roles Policy" +} + +test_IncludeApplications_Incorrect if { + ControlNumber := "AAD 2.13" + Requirement := "MFA SHALL be required for user access to highly privileged roles" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": [""] + }, + "Users": { + "IncludeRoles": ["Role1", "Role2" ] + } + }, + "GrantControls": { + "BuiltInControls": ["mfa"] + }, + "State": "enabled", + "DisplayName": {"MFA required for all highly Privileged Roles Policy"} + } + ], + "privileged_roles": [ + { + "RoleTemplateId": "Role1", + "DisplayName": "Global Administrator" + }, + { + "RoleTemplateId": "Role2", + "DisplayName": "Privileged Role Administrator" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_BuiltInControls_Incorrect if { + ControlNumber := "AAD 2.13" + Requirement := "MFA SHALL be required for user access to highly privileged roles" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeRoles": ["Role1", "Role2" ] + } + }, + "GrantControls": { + "BuiltInControls": [""] + }, + "State": "enabled", + "DisplayName": {"MFA required for all highly Privileged Roles Policy"} + } + ], + "privileged_roles": [ + { + "RoleTemplateId": "Role1", + "DisplayName": "Global Administrator" + }, + { + "RoleTemplateId": "Role2", + "DisplayName": "Privileged Role Administrator" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_State_Incorrect if { + ControlNumber := "AAD 2.13" + Requirement := "MFA SHALL be required for user access to highly privileged roles" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeRoles": ["Role1", "Role2" ] + } + }, + "GrantControls": { + "BuiltInControls": ["mfa"] + }, + "State": "disabled", + "DisplayName": {"MFA required for all highly Privileged Roles Policy"} + } + ], + "privileged_roles": [ + { + "RoleTemplateId": "Role1", + "DisplayName": "Global Administrator" + }, + { + "RoleTemplateId": "Role2", + "DisplayName": "Privileged Role Administrator" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_IncludeRoles_Incorrect_V1 if { + ControlNumber := "AAD 2.13" + Requirement := "MFA SHALL be required for user access to highly privileged roles" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeRoles": ["Role1"] + } + }, + "GrantControls": { + "BuiltInControls": ["mfa"] + }, + "State": "enabled", + "DisplayName": {"MFA required for all highly Privileged Roles Policy"} + } + ], + "privileged_roles": [ + { + "RoleTemplateId": "Role1", + "DisplayName": "Global Administrator" + }, + { + "RoleTemplateId": "Role2", + "DisplayName": "Privileged Role Administrator" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_IncludeRoles_Incorrect_V2 if { + ControlNumber := "AAD 2.13" + Requirement := "MFA SHALL be required for user access to highly privileged roles" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeRoles": ["Role2" ] + } + }, + "GrantControls": { + "BuiltInControls": ["mfa"] + }, + "State": "enabled", + "DisplayName": {"MFA required for all highly Privileged Roles Policy"} + } + ], + "privileged_roles": [ + { + "RoleTemplateId": "Role1", + "DisplayName": "Global Administrator" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} \ No newline at end of file diff --git a/Testing/AAD/AADConfig2_14_test.rego b/Testing/AAD/AADConfig2_14_test.rego new file mode 100644 index 0000000000..5e8df833a3 --- /dev/null +++ b/Testing/AAD/AADConfig2_14_test.rego @@ -0,0 +1,199 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_AdditionalProperties_Correct if { + ControlNumber := "AAD 2.14" + Requirement := "Permanent active role assignments SHALL NOT be allowed for highly privileged roles. Active assignments SHALL have an expiration period." + + Output := tests with input as { + "privileged_roles": [ + { + "DisplayName": "Global Administrator", + "Rules": [ + { + "Id": "Expiration_Admin_Assignment", + "AdditionalProperties": { + "isExpirationRequired": true, + "maximumDuration": "P15D" + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 role(s) configured to allow permanent active assignment or expiration period too long" +} + +test_AdditionalProperties_Incorrect_V1 if { + ControlNumber := "AAD 2.14" + Requirement := "Permanent active role assignments SHALL NOT be allowed for highly privileged roles. Active assignments SHALL have an expiration period." + + Output := tests with input as { + "privileged_roles": [ + { + "DisplayName": "Global Administrator", + "Rules": [ + { + "Id": "Expiration_Admin_Assignment", + "AdditionalProperties": { + "isExpirationRequired": false, + "maximumDuration": "P30D" + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 role(s) configured to allow permanent active assignment or expiration period too long:
Global Administrator" +} + +test_AdditionalProperties_Incorrect_V2 if { + ControlNumber := "AAD 2.14" + Requirement := "Permanent active role assignments SHALL NOT be allowed for highly privileged roles. Active assignments SHALL have an expiration period." + + Output := tests with input as { + "privileged_roles": [ + { + "DisplayName": "Global Administrator", + "Rules": [ + { + "Id": "Expiration_Admin_Assignment", + "AdditionalProperties": { + "isExpirationRequired": true, + "maximumDuration": "P30D" + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 role(s) configured to allow permanent active assignment or expiration period too long:
Global Administrator" +} + +# +# Policy 2 +#-- +test_Assignments_Correct if { + ControlNumber := "AAD 2.14" + Requirement := "Provisioning of users to highly privileged roles SHALL NOT occur outside of a PAM system, such as the Azure AD PIM service, because this bypasses the controls the PAM system provides" + + Output := tests with input as { + "privileged_roles": [ + { + "DisplayName": "Global Administrator", + "Assignments": [ + { + "StartDateTime": "/Date(1660328610000)/" + } + ], + "Rules": [ + { + "Id": "Expiration_Admin_Assignment", + "AdditionalProperties": { + "isExpirationRequired": true, + "maximumDuration": "P30D" + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 role(s) assigned to users outside of PIM" +} + +test_Assignments_Incorrect if { + ControlNumber := "AAD 2.14" + Requirement := "Provisioning of users to highly privileged roles SHALL NOT occur outside of a PAM system, such as the Azure AD PIM service, because this bypasses the controls the PAM system provides" + + Output := tests with input as { + "privileged_roles": [ + { + "DisplayName": "Global Administrator", + "Assignments": [ + { + "StartDateTime": null + } + ], + "Rules": [ + { + "Id": "Expiration_Admin_Assignment", + "AdditionalProperties": { + "isExpirationRequired": true, + "maximumDuration": "P30D" + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 role(s) assigned to users outside of PIM:
Global Administrator" +} \ No newline at end of file diff --git a/Testing/AAD/AADConfig2_15_test.rego b/Testing/AAD/AADConfig2_15_test.rego new file mode 100644 index 0000000000..ad64ed79ea --- /dev/null +++ b/Testing/AAD/AADConfig2_15_test.rego @@ -0,0 +1,80 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_AdditionalProperties_Correct if { + ControlNumber := "AAD 2.15" + Requirement := "Activation of highly privileged roles SHOULD require approval" + + Output := tests with input as { + "privileged_roles": [ + { + "DisplayName": "Global Administrator", + "Rules": [ + { + "Id": "Approval_EndUser_Assignment", + "AdditionalProperties": { + "setting": { + "isApprovalRequired" : true + } + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 role(s) that do not require approval to activate found" +} + +test_AdditionalProperties_Incorrect if { + ControlNumber := "AAD 2.15" + Requirement := "Activation of highly privileged roles SHOULD require approval" + + Output := tests with input as { + "privileged_roles": [ + { + "DisplayName": "Global Administrator", + "Rules": [ + { + "Id": "Approval_EndUser_Assignment", + "AdditionalProperties": { + "setting": { + "isApprovalRequired" : false + } + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 role(s) that do not require approval to activate found:
Global Administrator" +} \ No newline at end of file diff --git a/Testing/AAD/AADConfig2_16_test.rego b/Testing/AAD/AADConfig2_16_test.rego new file mode 100644 index 0000000000..9f39cb47a5 --- /dev/null +++ b/Testing/AAD/AADConfig2_16_test.rego @@ -0,0 +1,359 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_notificationRecipients_Correct if { + ControlNumber := "AAD 2.16" + Requirement := "Eligible and Active highly privileged role assignments SHALL trigger an alert" + + Output := tests with input as { + "privileged_roles": [ + { + "RoleTemplateId": "1D2EE3F0-90D3-4764-8AF8-BE81FE9D4D71", + "DisplayName": "Global Administrator", + "Rules": [ + { + "Id": "Notification_Admin_Admin_Assignment", + "AdditionalProperties": { + "notificationRecipients": ["test@example.com"] + } + }, + { + "Id": "Notification_Admin_Admin_Eligibility", + "AdditionalProperties": { + "notificationRecipients": ["test@example.com"] + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 role(s) without notification e-mail configured for role assignments found" +} + +test_notificationRecipients_Incorrect_V1 if { + ControlNumber := "AAD 2.16" + Requirement := "Eligible and Active highly privileged role assignments SHALL trigger an alert" + + Output := tests with input as { + "privileged_roles": [ + { + "RoleTemplateId": "1D2EE3F0-90D3-4764-8AF8-BE81FE9D4D71", + "DisplayName": "Global Administrator", + "Rules": [ + { + "Id": "Notification_Admin_Admin_Assignment", + "AdditionalProperties": { + "notificationRecipients": [] + } + }, + { + "Id": "Notification_Admin_Admin_Eligibility", + "AdditionalProperties": { + "notificationRecipients": ["test@example.com"] + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 role(s) without notification e-mail configured for role assignments found:
Global Administrator" +} + +test_notificationRecipients_Incorrect_V2 if { + ControlNumber := "AAD 2.16" + Requirement := "Eligible and Active highly privileged role assignments SHALL trigger an alert" + + Output := tests with input as { + "privileged_roles": [ + { + "RoleTemplateId": "1D2EE3F0-90D3-4764-8AF8-BE81FE9D4D71", + "DisplayName": "Global Administrator", + "Rules": [ + { + "Id": "Notification_Admin_Admin_Assignment", + "AdditionalProperties": { + "notificationRecipients": ["test@example.com"] + } + }, + { + "Id": "Notification_Admin_Admin_Eligibility", + "AdditionalProperties": { + "notificationRecipients": [] + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 role(s) without notification e-mail configured for role assignments found:
Global Administrator" +} + +test_notificationRecipients_Incorrect_V3 if { + ControlNumber := "AAD 2.16" + Requirement := "Eligible and Active highly privileged role assignments SHALL trigger an alert" + + Output := tests with input as { + "privileged_roles": [ + { + "RoleTemplateId": "1D2EE3F0-90D3-4764-8AF8-BE81FE9D4D71", + "DisplayName": "Global Administrator", + "Rules": [ + { + "Id": "Notification_Admin_Admin_Assignment", + "AdditionalProperties": { + "notificationRecipients": [] + } + }, + { + "Id": "Notification_Admin_Admin_Eligibility", + "AdditionalProperties": { + "notificationRecipients": [] + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 role(s) without notification e-mail configured for role assignments found:
Global Administrator" +} + +test_Id_Correct_V1 if { + ControlNumber := "AAD 2.16" + Requirement := "User activation of the Global Administrator role SHALL trigger an alert" + + Output := tests with input as { + "privileged_roles": [ + { + "RoleTemplateId": "1D2EE3F0-90D3-4764-8AF8-BE81FE9D4D71", + "DisplayName": "Global Administrator", + "Rules": [ + { + "Id": "Notification_Admin_EndUser_Assignment", + "AdditionalProperties": { + "notificationType": "Email", + "notificationRecipients": ["test@example.com"] + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Id_Correct_V2 if { + ControlNumber := "AAD 2.16" + Requirement := "User activation of the Global Administrator role SHALL trigger an alert" + + Output := tests with input as { + "privileged_roles": [ + { + "RoleTemplateId": "1D2EE3F0-90D3-4764-8AF8-BE81FE9D4D71", + "DisplayName": "Global Administrator", + "Rules": [ + { + "Id": "Notification_Admin_EndUser_Assignment", + "AdditionalProperties": { + "notificationType": "", + "notificationRecipients": ["test@example.com"] + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Id_Incorrect if { + ControlNumber := "AAD 2.16" + Requirement := "User activation of the Global Administrator role SHALL trigger an alert" + + Output := tests with input as { + "privileged_roles": [ + { + "RoleTemplateId": "1D2EE3F0-90D3-4764-8AF8-BE81FE9D4D71", + "DisplayName": "Global Administrator", + "Rules": [ + { + "Id": "Notification_Admin_EndUser_Assignment", + "AdditionalProperties": { + "notificationType": "Email", + "notificationRecipients": [] + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_DisplayName_Correct if { + ControlNumber := "AAD 2.16" + Requirement := "User activation of other highly privileged roles SHOULD trigger an alert" + + Output := tests with input as { + "privileged_roles": [ + { + "RoleTemplateId": "1D2EE3F0-90D3-4764-8AF8-BE81FE9D4D71", + "DisplayName": "Global Administrator", + "Rules": [ + { + "Id": "Notification_Admin_EndUser_Assignment", + "AdditionalProperties": { + "notificationType": "Email", + "notificationRecipients": [] + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 role(s) without notification e-mail configured for role activations found" +} + +test_DisplayName_Incorrect if { + ControlNumber := "AAD 2.16" + Requirement := "User activation of other highly privileged roles SHOULD trigger an alert" + + Output := tests with input as { + "privileged_roles": [ + { + "RoleTemplateId": "1D2EE3F0-90D3-4764-8AF8-BE81FE9D4D71", + "DisplayName": "Cloud Administrator", + "Rules": [ + { + "Id": "Notification_Admin_EndUser_Assignment", + "AdditionalProperties": { + "notificationType": "Email", + "notificationRecipients": [] + } + } + ] + } + ], + "service_plans": [ + { "ServicePlanName": "EXCHANGE_S_FOUNDATION", + "ServicePlanId": "31a0d5b2-13d0-494f-8e42-1e9c550a1b24" + }, + { "ServicePlanName": "AAD_PREMIUM_P2", + "ServicePlanId": "c7d91867-e1ce-4402-8d4f-22188b44b6c2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 role(s) without notification e-mail configured for role activations found:
Cloud Administrator" +} \ No newline at end of file diff --git a/Testing/AAD/AADConfig2_17_test.rego b/Testing/AAD/AADConfig2_17_test.rego new file mode 100644 index 0000000000..363855eb48 --- /dev/null +++ b/Testing/AAD/AADConfig2_17_test.rego @@ -0,0 +1,192 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_ConditionalAccessPolicies_Correct if { + ControlNumber := "AAD 2.17" + Requirement := "Managed devices SHOULD be required for authentication" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "GrantControls": { + "BuiltInControls": ["domainJoinedDevice"] + }, + "State": "enabled", + "DisplayName": "AD Joined Device Authentication Policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 conditional access policy(s) found that meet(s) all requirements:
AD Joined Device Authentication Policy" +} + +test_BuiltInControls_Correct if { + ControlNumber := "AAD 2.17" + Requirement := "Managed devices SHOULD be required for authentication" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "GrantControls": { + "BuiltInControls": ["compliantDevice"] + }, + "State": "enabled", + "DisplayName": "AD Joined Device Authentication Policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 conditional access policy(s) found that meet(s) all requirements:
AD Joined Device Authentication Policy" +} + +test_IncludeApplications_Incorrect if { + ControlNumber := "AAD 2.17" + Requirement := "Managed devices SHOULD be required for authentication" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": [""] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "GrantControls": { + "BuiltInControls": ["compliantDevice"] + }, + "State": "enabled", + "DisplayName": "AD Joined Device Authentication Policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_IncludeUsers_Incorrect if { + ControlNumber := "AAD 2.17" + Requirement := "Managed devices SHOULD be required for authentication" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": [""] + } + }, + "GrantControls": { + "BuiltInControls": ["compliantDevice"] + }, + "State": "enabled", + "DisplayName": "AD Joined Device Authentication Policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_BuiltInControls_Incorrect if { + ControlNumber := "AAD 2.17" + Requirement := "Managed devices SHOULD be required for authentication" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "GrantControls": { + "BuiltInControls": [""] + }, + "State": "enabled", + "DisplayName": "AD Joined Device Authentication Policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} + +test_State_Incorrect if { + ControlNumber := "AAD 2.17" + Requirement := "Managed devices SHOULD be required for authentication" + + Output := tests with input as { + "conditional_access_policies": [ + { + "Conditions": { + "Applications": { + "IncludeApplications": ["All"] + }, + "Users": { + "IncludeUsers": ["All"] + } + }, + "GrantControls": { + "BuiltInControls": ["compliantDevice"] + }, + "State": "disabled", + "DisplayName": "AD Joined Device Authentication Policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "0 conditional access policy(s) found that meet(s) all requirements" +} \ No newline at end of file diff --git a/Testing/AAD/AADConfig2_18_test.rego b/Testing/AAD/AADConfig2_18_test.rego new file mode 100644 index 0000000000..c745245a46 --- /dev/null +++ b/Testing/AAD/AADConfig2_18_test.rego @@ -0,0 +1,133 @@ +package aad +import future.keywords + + +# +# Policy 1 +#-- +test_AllowInvitesFrom_Correct if { + ControlNumber := "AAD 2.18" + Requirement := "Only users with the Guest Inviter role SHOULD be able to invite guest users" + + Output := tests with input as { + "authorization_policies": + { + "AllowInvitesFrom": "adminsAndGuestInviters" + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowInvitesFrom_Incorrect if { + ControlNumber := "AAD 2.18" + Requirement := "Only users with the Guest Inviter role SHOULD be able to invite guest users" + + Output := tests with input as { + "authorization_policies": + { + "AllowInvitesFrom": "" + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +# +# Policy 2 +#-- +test_NotImplemented_Correct if { + ControlNumber := "AAD 2.18" + Requirement := "Guest invites SHOULD only be allowed to specific external domains that have been authorized by the agency for legitimate business purposes" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Azure Active Directory Secure Configuration Baseline policy 2.18 for instructions on manual check" +} + +# +# Policy 3 +#-- +test_GuestUserRoleId_Correct_V1 if { + ControlNumber := "AAD 2.18" + Requirement := "Guest users SHOULD have limited access to Azure AD directory objects" + + Output := tests with input as { + "authorization_policies": + { + "GuestUserRoleId" : "2af84b1e-32c8-42b7-82bc-daa82404023b" + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Permission level set to \"Restricted access\"" +} + +test_GuestUserRoleId_Correct_V2 if { + ControlNumber := "AAD 2.18" + Requirement := "Guest users SHOULD have limited access to Azure AD directory objects" + + Output := tests with input as { + "authorization_policies": + { + "GuestUserRoleId" : "10dae51f-b6af-4016-8d66-8c2a99b929b3" + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Permission level set to \"Limited access\"" +} + +test_GuestUserRoleId_Incorrect_V1 if { + ControlNumber := "AAD 2.18" + Requirement := "Guest users SHOULD have limited access to Azure AD directory objects" + + Output := tests with input as { + "authorization_policies": + { + "GuestUserRoleId" : "a0b1b346-4d3e-4e8b-98f8-753987be4970" + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Permission level set to \"Same as member users\"" +} + +test_GuestUserRoleId_Incorrect_V2 if { + ControlNumber := "AAD 2.18" + Requirement := "Guest users SHOULD have limited access to Azure AD directory objects" + + Output := tests with input as { + "authorization_policies": + { + "GuestUserRoleId" : "Hello World" + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Permission level set to \"Unknown\"" +} \ No newline at end of file diff --git a/Testing/Defender/DefenderConfig2_01_test.rego b/Testing/Defender/DefenderConfig2_01_test.rego new file mode 100644 index 0000000000..c3e9dbcacf --- /dev/null +++ b/Testing/Defender/DefenderConfig2_01_test.rego @@ -0,0 +1,119 @@ +package defender +import future.keywords + + +# +# Policy 1 +#-- +test_Identity_Correct_V1 if { + ControlNumber := "Defender 2.1" + Requirement := "Standard Preset security profiles SHOULD NOT be used" + + Output := tests with input as { + "protection_policy_rules" : [] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Identity_Correct_V2 if { + ControlNumber := "Defender 2.1" + Requirement := "Standard Preset security profiles SHOULD NOT be used" + + Output := tests with input as { + "protection_policy_rules" : [ + { + "Identity" : "Standard Preset Security Policy", + "State" : "Disabled" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Identity_Incorrect_V1 if { + ControlNumber := "Defender 2.1" + Requirement := "Standard Preset security profiles SHOULD NOT be used" + + Output := tests with input as { + "protection_policy_rules" : [ + { + "Identity" : "Standard Preset Security Policy", + "State" : "Enabled" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "The Standard Preset Security Policy is present and not disabled" +} + +# +# Policy 2 +#-- +test_Identity_Correct_V1 if { + ControlNumber := "Defender 2.1" + Requirement := "Strict Preset security profiles SHOULD NOT be used" + + Output := tests with input as { + "protection_policy_rules" : [] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Identity_Correct_V2 if { + ControlNumber := "Defender 2.1" + Requirement := "Strict Preset security profiles SHOULD NOT be used" + + Output := tests with input as { + "protection_policy_rules" : [ + { + "Identity" : "Strict Preset Security Policy", + "State" : "Disabled" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Identity_Incorrect_V2 if { + ControlNumber := "Defender 2.1" + Requirement := "Strict Preset security profiles SHOULD NOT be used" + + Output := tests with input as { + "protection_policy_rules" : [ + { + "Identity" : "Strict Preset Security Policy", + "State" : "Enabled" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "The Strict Preset Security Policy is present and not disabled" +} \ No newline at end of file diff --git a/Testing/Defender/DefenderConfig2_02_test.rego b/Testing/Defender/DefenderConfig2_02_test.rego new file mode 100644 index 0000000000..8f2b1b8706 --- /dev/null +++ b/Testing/Defender/DefenderConfig2_02_test.rego @@ -0,0 +1,888 @@ +package defender +import future.keywords + + +# +# Policy 1 +#-- +test_ContentContainsSensitiveInformation_Correct_V1 if { + ControlNumber := "Defender 2.2" + Requirement := "A custom policy SHALL be configured to protect PII and sensitive information, as defined by the agency: U.S. Social Security Number (SSN)" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Social Security Number (SSN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_ContentContainsSensitiveInformation_Incorrect_V1 if { + ControlNumber := "Defender 2.2" + Requirement := "A custom policy SHALL be configured to protect PII and sensitive information, as defined by the agency: U.S. Social Security Number (SSN)" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"}, + {"name": "Credit Card Number"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No matching rule found for U.S. Social Security Number (SSN)" +} + +test_ContentContainsSensitiveInformation_Correct_V2 if { + ControlNumber := "Defender 2.2" + Requirement := "A custom policy SHALL be configured to protect PII and sensitive information, as defined by the agency: U.S. Individual Taxpayer Identification Number (ITIN)" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_ContentContainsSensitiveInformation_Incorrect_V2 if { + ControlNumber := "Defender 2.2" + Requirement := "A custom policy SHALL be configured to protect PII and sensitive information, as defined by the agency: U.S. Individual Taxpayer Identification Number (ITIN)" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "Credit Card Number"}, + {"name": "U.S. Social Security Number (SSN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No matching rule found for U.S. Individual Taxpayer Identification Number (ITIN)" +} + +test_ContentContainsSensitiveInformation_Correct_V3 if { + ControlNumber := "Defender 2.2" + Requirement := "A custom policy SHALL be configured to protect PII and sensitive information, as defined by the agency: Credit Card Number" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "Credit Card Number"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_ContentContainsSensitiveInformation_Incorrect_V3 if { + ControlNumber := "Defender 2.2" + Requirement := "A custom policy SHALL be configured to protect PII and sensitive information, as defined by the agency: Credit Card Number" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"}, + {"name": "U.S. Social Security Number (SSN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No matching rule found for Credit Card Number" +} + +# +# Policy 2 +#-- +test_Exchange_Correct if { + ControlNumber := "Defender 2.2" + Requirement := "The custom policy SHOULD be applied in Exchange" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"}, + {"name": "Credit Card Number"}, + {"name": "U.S. Social Security Number (SSN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ], + "dlp_compliance_policies": [ + { + "ExchangeLocation": ["All"], + "Workload": "Exchange", + "Name": "Default Office 365 DLP policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_ExchangeLocation_Incorrect if { + ControlNumber := "Defender 2.2" + Requirement := "The custom policy SHOULD be applied in Exchange" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"}, + {"name": "Credit Card Number"}, + {"name": "U.S. Social Security Number (SSN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ], + "dlp_compliance_policies": [ + { + "ExchangeLocation": [""], + "Workload": "Exchange", + "Name": "Default Office 365 DLP policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No policy found that applies to Exchange." +} + +test_Workload_Incorrect_V1 if { + ControlNumber := "Defender 2.2" + Requirement := "The custom policy SHOULD be applied in Exchange" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"}, + {"name": "Credit Card Number"}, + {"name": "U.S. Social Security Number (SSN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ], + "dlp_compliance_policies": [ + { + "ExchangeLocation": ["All"], + "Workload": "", + "Name": "Default Office 365 DLP policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No policy found that applies to Exchange." +} + +test_SharePoint_Correct if { + ControlNumber := "Defender 2.2" + Requirement := "The custom policy SHOULD be applied in SharePoint" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"}, + {"name": "Credit Card Number"}, + {"name": "U.S. Social Security Number (SSN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ], + "dlp_compliance_policies": [ + { + "SharePointLocation": ["All"], + "Workload": "SharePoint", + "Name": "Default Office 365 DLP policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_SharePointLocation_Incorrect if { + ControlNumber := "Defender 2.2" + Requirement := "The custom policy SHOULD be applied in SharePoint" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"}, + {"name": "Credit Card Number"}, + {"name": "U.S. Social Security Number (SSN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ], + "dlp_compliance_policies": [ + { + "SharePointLocation": [""], + "Workload": "SharePoint", + "Name": "Default Office 365 DLP policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No policy found that applies to SharePoint." +} + +test_Workload_Incorrect_V2 if { + ControlNumber := "Defender 2.2" + Requirement := "The custom policy SHOULD be applied in SharePoint" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"}, + {"name": "Credit Card Number"}, + {"name": "U.S. Social Security Number (SSN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ], + "dlp_compliance_policies": [ + { + "SharePointLocation": ["All"], + "Workload": "", + "Name": "Default Office 365 DLP policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No policy found that applies to SharePoint." +} + +test_OneDrive_Correct if { + ControlNumber := "Defender 2.2" + Requirement := "The custom policy SHOULD be applied in OneDrive" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"}, + {"name": "Credit Card Number"}, + {"name": "U.S. Social Security Number (SSN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ], + "dlp_compliance_policies": [ + { + "OneDriveLocation": ["All"], + "Workload": "OneDrivePoint", + "Name": "Default Office 365 DLP policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_OneDriveLocation_Incorrect if { + ControlNumber := "Defender 2.2" + Requirement := "The custom policy SHOULD be applied in OneDrive" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"}, + {"name": "Credit Card Number"}, + {"name": "U.S. Social Security Number (SSN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ], + "dlp_compliance_policies": [ + { + "OneDriveLocation": [""], + "Workload": "OneDrivePoint", + "Name": "Default Office 365 DLP policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No policy found that applies to OneDrive." +} + +test_Workload_Incorrect_V3 if { + ControlNumber := "Defender 2.2" + Requirement := "The custom policy SHOULD be applied in OneDrive" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"}, + {"name": "Credit Card Number"}, + {"name": "U.S. Social Security Number (SSN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ], + "dlp_compliance_policies": [ + { + "OneDriveLocation": ["All"], + "Workload": "", + "Name": "Default Office 365 DLP policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No policy found that applies to OneDrive." +} + +test_Teams_Correct if { + ControlNumber := "Defender 2.2" + Requirement := "The custom policy SHOULD be applied in Teams" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"}, + {"name": "Credit Card Number"}, + {"name": "U.S. Social Security Number (SSN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ], + "dlp_compliance_policies": [ + { + "TeamsLocation": ["All"], + "Workload": "Teams", + "Name": "Default Office 365 DLP policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_TeamsLocation_Incorrect if { + ControlNumber := "Defender 2.2" + Requirement := "The custom policy SHOULD be applied in Teams" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"}, + {"name": "Credit Card Number"}, + {"name": "U.S. Social Security Number (SSN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ], + "dlp_compliance_policies": [ + { + "TeamsLocation": [""], + "Workload": "Teams", + "Name": "Default Office 365 DLP policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No policy found that applies to Teams." +} + +test_Workload_Incorrect_V4 if { + ControlNumber := "Defender 2.2" + Requirement := "The custom policy SHOULD be applied in Teams" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"}, + {"name": "Credit Card Number"}, + {"name": "U.S. Social Security Number (SSN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ], + "dlp_compliance_policies": [ + { + "TeamsLocation": ["All"], + "Workload": "", + "Name": "Default Office 365 DLP policy" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No policy found that applies to Teams." +} + +# +# Policy 3 +#-- +test_BlockAccess_Correct if { + ControlNumber := "Defender 2.2" + Requirement := "The action for the DLP policy SHOULD be set to block sharing sensitive information with everyone when DLP conditions are met" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_BlockAccess_Incorrect if { + ControlNumber := "Defender 2.2" + Requirement := "The action for the DLP policy SHOULD be set to block sharing sensitive information with everyone when DLP conditions are met" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": false, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 rule(s) found that do(es) not block access: Baseline Rule" +} + +# +# Policy 4 +#-- +test_NotifyUser_Correct_V1 if { + ControlNumber := "Defender 2.2" + Requirement := "Notifications to inform users and help educate them on the proper use of sensitive information SHOULD be enabled" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin" + ], + "NotifyUserType": "NotSet" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_NotifyUser_Correct_V2 if { + ControlNumber := "Defender 2.2" + Requirement := "Notifications to inform users and help educate them on the proper use of sensitive information SHOULD be enabled" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ + "SiteAdmin", + "LastModifier", + "Owner" + ], + "NotifyUserType": "NotSet" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_NotifyUser_Incorrect if { + ControlNumber := "Defender 2.2" + Requirement := "Notifications to inform users and help educate them on the proper use of sensitive information SHOULD be enabled" + + Output := tests with input as { + "dlp_compliance_rules": [ + { + "ContentContainsSensitiveInformation": [ + {"name": "U.S. Individual Taxpayer Identification Number (ITIN)"} + ], + "Name": "Baseline Rule", + "Disabled" : false, + "ParentPolicyName": "Default Office 365 DLP policy", + "BlockAccess": true, + "BlockAccessScope": "All", + "NotifyUser": [ ], + "NotifyUserType": "NotSet" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 rule(s) found that do(es) not notify at least one user: Baseline Rule" +} + +# +# Policy 5 +#-- +test_NotImplemented_Correct_V1 if { + ControlNumber := "Defender 2.2" + Requirement := "A list of apps that are not allowed to access files protected by DLP policy SHOULD be defined" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Defender Secure Configuration Baseline policy 2.2 for instructions on manual check" +} + +# +# Policy 6 +#-- +test_NotImplemented_Correct_V2 if { + ControlNumber := "Defender 2.2" + Requirement := "A list of browsers that are not allowed to access files protected by DLP policy SHOULD be defined" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Defender Secure Configuration Baseline policy 2.2 for instructions on manual check" +} \ No newline at end of file diff --git a/Testing/Defender/DefenderConfig2_03_test.rego b/Testing/Defender/DefenderConfig2_03_test.rego new file mode 100644 index 0000000000..ad1b82b303 --- /dev/null +++ b/Testing/Defender/DefenderConfig2_03_test.rego @@ -0,0 +1,198 @@ +package defender +import future.keywords + + +# +# Policy 1 +#-- +test_EnableFileFilter_Correct if { + ControlNumber := "Defender 2.3" + Requirement := "The common attachments filter SHALL be enabled in the default anti-malware policy and in all existing policies" + + Output := tests with input as { + "malware_filter_policies": [ + { + "EnableFileFilter" : true, + "Name": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableFileFilter_Incorrect if { + ControlNumber := "Defender 2.3" + Requirement := "The common attachments filter SHALL be enabled in the default anti-malware policy and in all existing policies" + + Output := tests with input as { + "malware_filter_policies": [ + { + "EnableFileFilter" : false, + "Name": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 malware policy(ies) found that do(es) not have the common attachments filter enabled: Default" +} + +test_EnableFileFilterMultiple_Incorrect if { + ControlNumber := "Defender 2.3" + Requirement := "The common attachments filter SHALL be enabled in the default anti-malware policy and in all existing policies" + + Output := tests with input as { + "malware_filter_policies": [ + { + "EnableFileFilter" : true, + "Name": "Default" + }, + { + "EnableFileFilter" : false, + "Name": "Custom 1" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 malware policy(ies) found that do(es) not have the common attachments filter enabled: Custom 1" +} + +# +# Policy 2 +#-- +test_FileTypes_Correct_V1 if { + ControlNumber := "Defender 2.3" + Requirement := "Disallowed file types SHALL be determined and set. At a minimum, click-to-run files SHOULD be blocked: exe files" + + Output := tests with input as { + "malware_filter_policies": [ + { + "FileTypes" : ["exe"], + "EnableFileFilter" : true, + "Name": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_FileTypes_Incorrect_V1 if { + ControlNumber := "Defender 2.3" + Requirement := "Disallowed file types SHALL be determined and set. At a minimum, click-to-run files SHOULD be blocked: exe files" + + Output := tests with input as { + "malware_filter_policies": [ + { + "FileTypes" : ["cmd", "vbe"], + "EnableFileFilter" : true, + "Name": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No malware policies found that block .exe files." +} + +test_FileTypes_Correct_V2 if { + ControlNumber := "Defender 2.3" + Requirement := "Disallowed file types SHALL be determined and set. At a minimum, click-to-run files SHOULD be blocked: cmd files" + + Output := tests with input as { + "malware_filter_policies": [ + { + "FileTypes" : ["cmd"], + "EnableFileFilter" : true, + "Name": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_FileTypes_Incorrect_V2 if { + ControlNumber := "Defender 2.3" + Requirement := "Disallowed file types SHALL be determined and set. At a minimum, click-to-run files SHOULD be blocked: cmd files" + + Output := tests with input as { + "malware_filter_policies": [ + { + "FileTypes" : ["exe", "vbe"], + "EnableFileFilter" : true, + "Name": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No malware policies found that block .cmd files." +} + +test_FileTypes_Correct_V3 if { + ControlNumber := "Defender 2.3" + Requirement := "Disallowed file types SHALL be determined and set. At a minimum, click-to-run files SHOULD be blocked: vbe files" + + Output := tests with input as { + "malware_filter_policies": [ + { + "FileTypes" : ["vbe"], + "EnableFileFilter" : true, + "Name": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_FileTypes_Incorrect_V3 if { + ControlNumber := "Defender 2.3" + Requirement := "Disallowed file types SHALL be determined and set. At a minimum, click-to-run files SHOULD be blocked: vbe files" + + Output := tests with input as { + "malware_filter_policies": [ + { + "FileTypes" : ["exe", "cmd"], + "EnableFileFilter" : true, + "Name": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No malware policies found that block .vbe files." +} \ No newline at end of file diff --git a/Testing/Defender/DefenderConfig2_04_test.rego b/Testing/Defender/DefenderConfig2_04_test.rego new file mode 100644 index 0000000000..5bc280fd0e --- /dev/null +++ b/Testing/Defender/DefenderConfig2_04_test.rego @@ -0,0 +1,70 @@ +package defender +import future.keywords + + +# +# Policy 1 +#-- +test_ZapEnabled_Correct if { + ControlNumber := "Defender 2.4" + Requirement := "Zero-hour Auto Purge (ZAP) for malware SHOULD be enabled in the default anti-malware policy and in all existing custom policies" + + Output := tests with input as { + "malware_filter_policies": [ + { + "ZapEnabled" : true, + "Name": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_ZapEnabled_Incorrect if { + ControlNumber := "Defender 2.4" + Requirement := "Zero-hour Auto Purge (ZAP) for malware SHOULD be enabled in the default anti-malware policy and in all existing custom policies" + + Output := tests with input as { + "malware_filter_policies": [ + { + "ZapEnabled" : false, + "Name": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 malware policy(ies) found without ZAP for malware enabled: Default" +} + +test_ZapEnabledMultiple_Incorrect if { + ControlNumber := "Defender 2.4" + Requirement := "Zero-hour Auto Purge (ZAP) for malware SHOULD be enabled in the default anti-malware policy and in all existing custom policies" + + Output := tests with input as { + "malware_filter_policies": [ + { + "ZapEnabled" : true, + "Name": "Default" + }, + { + "ZapEnabled" : false, + "Name": "Custom 1" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 malware policy(ies) found without ZAP for malware enabled: Custom 1" +} \ No newline at end of file diff --git a/Testing/Defender/DefenderConfig2_05_test.rego b/Testing/Defender/DefenderConfig2_05_test.rego new file mode 100644 index 0000000000..b24ed52f0a --- /dev/null +++ b/Testing/Defender/DefenderConfig2_05_test.rego @@ -0,0 +1,1377 @@ +package defender +import future.keywords + + +# +# Policy 1 +#-- +test_TargetedUsers_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "User impersonation protection SHOULD be enabled for key agency leaders" + + Output := tests with input as { + "anti_phish_policies": [ + { + "Name" : "Standard Preset Security Policy1659535429826", + "Enabled" : true, + "EnableTargetedUserProtection" : true, + "TargetedUsersToProtect" : [ + "john doe;jdoe@someemail.com", + "jane doe;jadoe@someemail.com" + ], + "TargetedUserProtectionAction": "Quarantine" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Enabled_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "User impersonation protection SHOULD be enabled for key agency leaders" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "Name" : "Standard Preset Security Policy1659535429826", + "Enabled" : false, + "EnableTargetedUserProtection" : true, + "TargetedUsersToProtect" : [ + "john doe;jdoe@someemail.com", + "jane doe;jadoe@someemail.com" + ], + "TargetedUserProtectionAction" : "Quarantine" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No users are included for targeted user protection." +} + +test_EnableTargetedUserProtection_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "User impersonation protection SHOULD be enabled for key agency leaders" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "Name" : "Standard Preset Security Policy1659535429826", + "Enabled" : true, + "EnableTargetedUserProtection" : false, + "TargetedUsersToProtect" : [ + "john doe;jdoe@someemail.com", + "jane doe;jadoe@someemail.com" + ], + "TargetedUserProtectionAction" : "Quarantine" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No users are included for targeted user protection." +} + +test_TargetedUsersToProtect_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "User impersonation protection SHOULD be enabled for key agency leaders" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "Name" : "Standard Preset Security Policy1659535429826", + "Enabled" : true, + "EnableTargetedUserProtection" : true, + "TargetedUsersToProtect" : [ ], + "TargetedUserProtectionAction" : "Quarantine" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No users are included for targeted user protection." +} + +# +# Policy 2 +#-- +test_OrganizationDomain_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "Domain impersonation protection SHOULD be enabled for domains owned by the agency" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "Name" : "Standard Preset Security Policy1659535429826", + "Enabled" : true, + "EnableOrganizationDomainsProtection" : true, + "EnableTargetedDomainsProtection" : true, + "TargetedDomainProtectionAction" : "Quarantine", + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Enabled_Incorrect_V2 if { + ControlNumber := "Defender 2.5" + Requirement := "Domain impersonation protection SHOULD be enabled for domains owned by the agency" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "Name" : "Standard Preset Security Policy1659535429826", + "Enabled" : false, + "EnableOrganizationDomainsProtection" : true, + "EnableTargetedDomainsProtection" : true, + "TargetedDomainProtectionAction" : "Quarantine", + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableOrganizationDomainsProtection_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "Domain impersonation protection SHOULD be enabled for domains owned by the agency" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "Name" : "Standard Preset Security Policy1659535429826", + "Enabled" : true, + "EnableOrganizationDomainsProtection" : false, + "EnableTargetedDomainsProtection" : true, + "TargetedDomainProtectionAction" : "Quarantine", + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableTargetedDomainsProtection_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "Domain impersonation protection SHOULD be enabled for domains owned by the agency" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "Name" : "Standard Preset Security Policy1659535429826", + "Enabled" : true, + "EnableOrganizationDomainsProtection" : true, + "EnableTargetedDomainsProtection" : false, + "TargetedDomainProtectionAction" : "Quarantine", + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +# +# Policy 3 +#-- +test_CustomDomains_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "Domain impersonation protection SHOULD be added for frequent partners" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "Name" : "Standard Preset Security Policy1659535429826", + "Enabled" : true, + "EnableTargetedDomainsProtection" : true, + "TargetedDomainsToProtect" : [ "test domain" ], + "TargetedDomainProtectionAction" : "Quarantine" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Enabled_Incorrect_V3 if { + ControlNumber := "Defender 2.5" + Requirement := "Domain impersonation protection SHOULD be added for frequent partners" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "Name" : "Standard Preset Security Policy1659535429826", + "Enabled" : false, + "EnableTargetedDomainsProtection" : true, + "TargetedDomainsToProtect" : [ "test domain" ], + "TargetedDomainProtectionAction" : "Quarantine" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "The Custom Domains protection policies: Enabled, EnableTargetedDomainsProtection, and TargetedDomainsToProtect are not set correctly" +} + +test_EnableTargetedDomainsProtection_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "Domain impersonation protection SHOULD be added for frequent partners" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "Name" : "Standard Preset Security Policy1659535429826", + "Enabled" : true, + "EnableTargetedDomainsProtection" : false, + "TargetedDomainsToProtect" : [ "test domain" ], + "TargetedDomainProtectionAction" : "Quarantine" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "The Custom Domains protection policies: Enabled, EnableTargetedDomainsProtection, and TargetedDomainsToProtect are not set correctly" +} + +test_TargetedDomainsToProtect_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "Domain impersonation protection SHOULD be added for frequent partners" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "Name" : "Standard Preset Security Policy1659535429826", + "Enabled" : true, + "EnableTargetedDomainsProtection" : true, + "TargetedDomainsToProtect" : [ ], + "TargetedDomainProtectionAction" : "Quarantine" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "The Custom Domains protection policies: Enabled, EnableTargetedDomainsProtection, and TargetedDomainsToProtect are not set correctly" +} + +# +# Policy 4 +#-- +test_Email_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "Intelligence for impersonation protection SHALL be enabled" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "Name" : "Standard Preset Security Policy1659535429826", + "Enabled" : true, + "EnableMailboxIntelligenceProtection" : true, + "MailboxIntelligenceProtectionAction" : "Quarantine" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Enabled_Incorrect_V4 if { + ControlNumber := "Defender 2.5" + Requirement := "Intelligence for impersonation protection SHALL be enabled" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "Name" : "Standard Preset Security Policy1659535429826", + "Enabled" : false, + "EnableMailboxIntelligenceProtection" : true, + "MailboxIntelligenceProtectionAction" : "Quarantine" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableMailboxIntelligenceProtection_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "Intelligence for impersonation protection SHALL be enabled" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "Name" : "Standard Preset Security Policy1659535429826", + "Enabled" : true, + "EnableMailboxIntelligenceProtection" : false, + "MailboxIntelligenceProtectionAction" : "Quarantine" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +# +# Policy 5 +#-- +test_TargetedUserProtectionAction_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "Message action SHALL be set to quarantine if the message is detected as impersonated: users default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "TargetedUserProtectionAction" : "Quarantine", + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_TargetedUserProtectionAction_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "Message action SHALL be set to quarantine if the message is detected as impersonated: users default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "TargetedUserProtectionAction" : "Not Quarantine", + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_TargetedUserProtectionActionCustom_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "Message action SHOULD be set to quarantine if the message is detected as impersonated: users non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "TargetedUserProtectionAction" : "Quarantine", + "Identity" : "Custom 1" + }, + { + "TargetedUserProtectionAction" : "Not Quarantine", + "Identity" : "Office365 AntiPhish Default" # The default policy should be ignored here + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_TargetedUserProtectionActionCustom_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "Message action SHOULD be set to quarantine if the message is detected as impersonated: users non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "TargetedUserProtectionAction" : "Quarantine", + "Identity" : "Custom 1" + }, + { + "TargetedUserProtectionAction" : "Not Quarantine", + "Identity" : "Custom 2" + }, + { + "TargetedUserProtectionAction" : "Not Quarantine", + "Identity" : "Office365 AntiPhish Default" # The default policy should be ignored here + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 non-default anti phish policy(ies) found where the action for messages detected as user impersonation is not quarantine: Custom 2" +} + + +test_TargetedDomainProtectionAction_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "Message action SHALL be set to quarantine if the message is detected as impersonated: domains default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "TargetedDomainProtectionAction" : "Quarantine", + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_TargetedDomainProtectionAction_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "Message action SHALL be set to quarantine if the message is detected as impersonated: domains default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "TargetedDomainProtectionAction" : "Not Quarantine", + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_TargetedDomainProtectionActionCustom_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "Message action SHOULD be set to quarantine if the message is detected as impersonated: domains non-default policies" + Output := tests with input as { + "anti_phish_policies" : [ + { + "TargetedDomainProtectionAction" : "Quarantine", + "Identity" : "Custom 1" + }, + { + "TargetedDomainProtectionAction" : "Not Quarantine", + "Identity" : "Office365 AntiPhish Default" # The default policy should be ignored here + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_TargetedDomainProtectionActionCustom_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "Message action SHOULD be set to quarantine if the message is detected as impersonated: domains non-default policies" + Output := tests with input as { + "anti_phish_policies" : [ + { + "TargetedDomainProtectionAction" : "Quarantine", + "Identity" : "Custom 1" + }, + { + "TargetedDomainProtectionAction" : "Not Quarantine", + "Identity" : "Custom 2" + }, + { + "TargetedDomainProtectionAction" : "Not Quarantine", + "Identity" : "Custom 3" + }, + { + "TargetedDomainProtectionAction" : "Not Quarantine", + "Identity" : "Office365 AntiPhish Default" # The default policy should be ignored here + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + startswith(RuleOutput[0].ReportDetails, "2 non-default anti phish policy(ies) found where the action for messages detected as domain impersonation is not quarantine:") + # I don't think we can assume the order rego will Output these, hence the "includes" check instead of a simple == + contains(RuleOutput[0].ReportDetails, "Custom 2") + contains(RuleOutput[0].ReportDetails, "Custom 3") +} + +test_MailboxIntelligenceProtectionAction_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "Message action SHALL be set to quarantine if the message is detected as impersonated: mailbox default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "MailboxIntelligenceProtectionAction" : "Quarantine", + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_MailboxIntelligenceProtectionAction_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "Message action SHALL be set to quarantine if the message is detected as impersonated: mailbox default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "MailboxIntelligenceProtectionAction" : "Not Quarantine", + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_MailIntProtectionActionCustom_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "Message action SHOULD be set to quarantine if the message is detected as impersonated: mailbox non-default policies" + Output := tests with input as { + "anti_phish_policies" : [ + { + "MailboxIntelligenceProtectionAction" : "Quarantine", + "Identity" : "Custom 1" + }, + { + "MailboxIntelligenceProtectionAction" : "Quarantine", + "Identity" : "Custom 2" + }, + { + "MailboxIntelligenceProtectionAction" : "something else", + "Identity" : "Standard Preset Security Policy314195" # should be ignored + }, + { + "MailboxIntelligenceProtectionAction" : "Not Quarantine", + "Identity" : "Office365 AntiPhish Default" # The default policy should be ignored here + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails = "Requirement met" +} + +test_MailIntProtectionActionCustom_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "Message action SHOULD be set to quarantine if the message is detected as impersonated: mailbox non-default policies" + Output := tests with input as { + "anti_phish_policies" : [ + { + "MailboxIntelligenceProtectionAction" : "Quarantine", + "Identity" : "Custom 1" + }, + { + "MailboxIntelligenceProtectionAction" : "Not Quarantine", + "Identity" : "Custom 2" + }, + { + "MailboxIntelligenceProtectionAction" : "something else", + "Identity" : "Standard Preset Security Policy314195" # should be ignored + }, + { + "MailboxIntelligenceProtectionAction" : "Not Quarantine", + "Identity" : "Office365 AntiPhish Default" # The default policy should be ignored here + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails = "1 non-default anti phish policy(ies) found where the action for messages flagged by mailbox intelligence is not quarantine: Custom 2" +} + +# +# Policy 6 +#-- +test_AuthenticationFailAction_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "Mail classified as spoofed SHALL be quarantined: default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "AuthenticationFailAction" : "Quarantine", + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AuthenticationFailAction_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "Mail classified as spoofed SHALL be quarantined: default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "AuthenticationFailAction" : "Not Quarantine", + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_AuthenticationFailActionNonDefault_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "Mail classified as spoofed SHOULD be quarantined: non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "AuthenticationFailAction" : "Quarantine", + "Identity" : "Not Standard Preset SecurityPolicy1659535429826" + }, + { + "AuthenticationFailAction" : "Quarantine", + "Identity" : "Not Standard Preset SecurityPolicy Either" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AuthenticationFailActionNonDefault_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "Mail classified as spoofed SHOULD be quarantined: non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "AuthenticationFailAction" : "Not Quarantine", + "Identity" : "Not Standard Preset SecurityPolicy1659535429826" + }, + { + "AuthenticationFailAction" : "Quarantine", + "Identity" : "Not Standard Preset SecurityPolicy Either" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti phish policy(ies) found where the action for spoofed emails is not set to quarantine: Not Standard Preset SecurityPolicy1659535429826" + #Custom anti phish policy(ies) found where the action for spoofed emails is not set to quarantine. +} + +# +# Policy 7 +#-- +test_EnableFirstContactSafetyTipsDefault_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHALL be enabled: first contact default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableFirstContactSafetyTips" : true, + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableFirstContactSafetyTipsDefault_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHALL be enabled: first contact default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableFirstContactSafetyTips" : false, + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableFirstContactSafetyTipsNonDefault_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHOULD be enabled: first contact non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableFirstContactSafetyTips" : true, + "Identity" : "Custom policy 1" + }, + { + "EnableFirstContactSafetyTips" : true, + "Identity" : "Custom policy 2" + }, + { + "EnableFirstContactSafetyTips" : true, + "Identity" : "Custom policy 3" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableFirstContactSafetyTipsNonDefault_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHOULD be enabled: first contact non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableFirstContactSafetyTips" : true, + "Identity" : "Custom policy 1" + }, + { + "EnableFirstContactSafetyTips" : false, + "Identity" : "Custom policy 2" + }, + { + "EnableFirstContactSafetyTips" : true, + "Identity" : "Custom policy 3" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti phish policy(ies) found where first contact safety tips are not enabled: Custom policy 2" +} + +test_EnableSimilarUsersSafetyTipsDefault_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHALL be enabled: user impersonation default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableSimilarUsersSafetyTips" : true, + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableSimilarUsersSafetyTipsDefault_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHALL be enabled: user impersonation default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableSimilarUsersSafetyTips" : false, + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableSimilarUserSafetyTipsNonDefault_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHOULD be enabled: user impersonation non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableSimilarUsersSafetyTips" : true, + "Identity" : "Custom policy 1" + }, + { + "EnableSimilarUsersSafetyTips" : true, + "Identity" : "Custom policy 2" + }, + { + "EnableSimilarUsersSafetyTips" : true, + "Identity" : "Custom policy 3" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableSimilarUserSafetyTipsNonDefault_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHOULD be enabled: user impersonation non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableSimilarUsersSafetyTips" : true, + "Identity" : "Custom policy 1" + }, + { + "EnableSimilarUsersSafetyTips" : false, + "Identity" : "Custom policy 2" + }, + { + "EnableSimilarUsersSafetyTips" : true, + "Identity" : "Custom policy 3" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti phish policy(ies) found where similar user safety tips are not enabled: Custom policy 2" +} + +test_EnableSimilarDomainsSafetyTipsDomains_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHALL be enabled: domain impersonation default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableSimilarDomainsSafetyTips" : true, + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableSimilarDomainsSafetyTipsDefault_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHALL be enabled: domain impersonation default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableSimilarDomainsSafetyTips" : false, + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableSimilarDomainsSafetyTipsNonDefault_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHOULD be enabled: domain impersonation non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableSimilarDomainsSafetyTips" : true, + "Identity" : "Custom policy 1" + }, + { + "EnableSimilarDomainsSafetyTips" : true, + "Identity" : "Custom policy 2" + }, + { + "EnableSimilarDomainsSafetyTips" : true, + "Identity" : "Custom policy 3" + }, + { + "EnableSimilarDomainsSafetyTips" : false, # The default policy should be ignored + "Identity" : "Office365 AntiPhish Default" + }, + { + "EnableSimilarDomainsSafetyTips" : false, # The preset policy should be ignored too + "Identity" : "Standard Preset Security Policy12345" + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableSimilarDomainsSafetyTipsNonDefault_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHOULD be enabled: domain impersonation non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableSimilarDomainsSafetyTips" : true, + "Identity" : "Custom policy 1" + }, + { + "EnableSimilarDomainsSafetyTips" : false, + "Identity" : "Custom policy 2" + }, + { + "EnableSimilarDomainsSafetyTips" : true, + "Identity" : "Custom policy 3" + }, + { + "EnableSimilarDomainsSafetyTips" : false, # The default policy should be ignored + "Identity" : "Office365 AntiPhish Default" + }, + { + "EnableSimilarDomainsSafetyTips" : false, # The preset policy should be ignored too + "Identity" : "Standard Preset Security Policy12345" + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti phish policy(ies) found where similar domains safety tips are not enabled: Custom policy 2" +} + +test_EnableUnusualCharactersSafetyTips_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHALL be enabled: user impersonation unusual characters default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableUnusualCharactersSafetyTips" : true, + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableUnusualCharactersSafetyTips_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHALL be enabled: user impersonation unusual characters default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableUnusualCharactersSafetyTips" : false, + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableUnusualCharSafetyTipsNonDefault_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHOULD be enabled: user impersonation unusual characters non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableUnusualCharactersSafetyTips" : true, + "Identity" : "Custom policy 1" + }, + { + "EnableUnusualCharactersSafetyTips" : true, + "Identity" : "Custom policy 2" + }, + { + "EnableUnusualCharactersSafetyTips" : true, + "Identity" : "Custom policy 3" + }, + { + "EnableUnusualCharactersSafetyTips" : false, # The default policy should be ignored + "Identity" : "Office365 AntiPhish Default" + }, + { + "EnableUnusualCharactersSafetyTips" : false, # The preset policy should be ignored too + "Identity" : "Standard Preset Security Policy12345" + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableUnusualCharSafetyTipsNonDefault_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHOULD be enabled: user impersonation unusual characters non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableUnusualCharactersSafetyTips" : true, + "Identity" : "Custom policy 1" + }, + { + "EnableUnusualCharactersSafetyTips" : false, + "Identity" : "Custom policy 2" + }, + { + "EnableUnusualCharactersSafetyTips" : true, + "Identity" : "Custom policy 3" + }, + { + "EnableUnusualCharactersSafetyTips" : false, # The default policy should be ignored + "Identity" : "Office365 AntiPhish Default" + }, + { + "EnableUnusualCharactersSafetyTips" : false, # The preset policy should be ignored too + "Identity" : "Standard Preset Security Policy12345" + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti phish policy(ies) found where unusual character safety tips are not enabled: Custom policy 2" +} + +test_EnableViaTagDefault_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHALL be enabled: \"via\" tag default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableViaTag" : true, + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableViaTagDefault_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHALL be enabled: \"via\" tag default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableViaTag" : false, + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableViaTagSafetyTipsNonDefault_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHOULD be enabled: \"via\" tag non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableViaTag" : true, + "Identity" : "Custom policy 1" + }, + { + "EnableViaTag" : true, + "Identity" : "Custom policy 2" + }, + { + "EnableViaTag" : true, + "Identity" : "Custom policy 3" + }, + { + "EnableViaTag" : false, # The default policy should be ignored + "Identity" : "Office365 AntiPhish Default" + }, + { + "EnableViaTag" : false, # The preset policy should be ignored too + "Identity" : "Standard Preset Security Policy12345" + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableViaTagSafetyTipsNonDefault_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHOULD be enabled: \"via\" tag non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableViaTag" : true, + "Identity" : "Custom policy 1" + }, + { + "EnableViaTag" : false, + "Identity" : "Custom policy 2" + }, + { + "EnableViaTag" : true, + "Identity" : "Custom policy 3" + }, + { + "EnableViaTag" : false, # The default policy should be ignored + "Identity" : "Office365 AntiPhish Default" + }, + { + "EnableViaTag" : false, # The preset policy should be ignored too + "Identity" : "Standard Preset Security Policy12345" + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti phish policy(ies) found where via tag is not enabled: Custom policy 2" +} + +test_EnableUnauthenticatedSenderDefault_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHALL be enabled: \"?\" for unauthenticated senders for spoof default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableUnauthenticatedSender" : true, + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableUnauthenticatedSenderDefault_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHALL be enabled: \"?\" for unauthenticated senders for spoof default policy" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableUnauthenticatedSender" : false, + "Identity" : "Office365 AntiPhish Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableUnauthSenderTipsNonDefault_Correct if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHOULD be enabled: \"?\" for unauthenticated senders for spoof non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableUnauthenticatedSender" : true, + "Identity" : "Custom policy 1" + }, + { + "EnableUnauthenticatedSender" : true, + "Identity" : "Custom policy 2" + }, + { + "EnableUnauthenticatedSender" : true, + "Identity" : "Custom policy 3" + }, + { + "EnableUnauthenticatedSender" : false, # The default policy should be ignored + "Identity" : "Office365 AntiPhish Default" + }, + { + "EnableUnauthenticatedSender" : false, # The preset policy should be ignored too + "Identity" : "Standard Preset Security Policy12345" + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableUnauthSenderTipsNonDefault_Incorrect if { + ControlNumber := "Defender 2.5" + Requirement := "All safety tips SHOULD be enabled: \"?\" for unauthenticated senders for spoof non-default policies" + + Output := tests with input as { + "anti_phish_policies" : [ + { + "EnableUnauthenticatedSender" : true, + "Identity" : "Custom policy 1" + }, + { + "EnableUnauthenticatedSender" : false, + "Identity" : "Custom policy 2" + }, + { + "EnableUnauthenticatedSender" : true, + "Identity" : "Custom policy 3" + }, + { + "EnableUnauthenticatedSender" : false, # The default policy should be ignored + "Identity" : "Office365 AntiPhish Default" + }, + { + "EnableUnauthenticatedSender" : false, # The preset policy should be ignored too + "Identity" : "Standard Preset Security Policy12345" + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti phish policy(ies) found where '?' for unauthenticated sender is not enabled: Custom policy 2" +} \ No newline at end of file diff --git a/Testing/Defender/DefenderConfig2_06_test.rego b/Testing/Defender/DefenderConfig2_06_test.rego new file mode 100644 index 0000000000..1fe85f5f6e --- /dev/null +++ b/Testing/Defender/DefenderConfig2_06_test.rego @@ -0,0 +1,1201 @@ +package defender +import future.keywords + + +# +# Policy 1 +#-- +test_BulkThreshold_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "The bulk complaint level (BCL) threshold SHOULD be set to six or lower: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "BulkThreshold": 6, + "Identity" : "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_BulkThreshold_CorrectV2 if { + ControlNumber := "Defender 2.6" + Requirement := "The bulk complaint level (BCL) threshold SHOULD be set to six or lower: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "BulkThreshold": 5, + "Identity" : "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_BulkThreshold_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "The bulk complaint level (BCL) threshold SHOULD be set to six or lower: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "BulkThreshold": 7, + "Identity" : "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +### + +test_NonDefBulkThreshold_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "The bulk complaint level (BCL) threshold SHOULD be set to six or lower: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "BulkThreshold": 6, + "Identity":"Not Default" + }, + { + "BulkThreshold": 6, + "Identity":"Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_NonDefBulkThreshold_CorrectV2 if { + ControlNumber := "Defender 2.6" + Requirement := "The bulk complaint level (BCL) threshold SHOULD be set to six or lower: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "BulkThreshold": 5, + "Identity":"Not Default" + }, + { + "BulkThreshold": 5, + "Identity":"Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_NonDefBulkThreshold_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "The bulk complaint level (BCL) threshold SHOULD be set to six or lower: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "BulkThreshold": 7, + "Identity":"Not Default" + }, + { + "BulkThreshold": 7, + "Identity":"Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti-spam policy(ies) found where bulk complaint level threshold is set to 7 or more: Not Default" +} + +# +# Policy 2 +#-- +test_SpamAction_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Spam SHALL be moved to either the junk email folder or the quarantine folder: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "SpamAction": "Quarantine", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_SpamAction_CorrectV2 if { + ControlNumber := "Defender 2.6" + Requirement := "Spam SHALL be moved to either the junk email folder or the quarantine folder: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "SpamAction": "MoveToJmf", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_SpamAction_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Spam SHALL be moved to either the junk email folder or the quarantine folder: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "SpamAction": "Not MoveToJmf", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_SpamAction_Incorrect_V2 if { + ControlNumber := "Defender 2.6" + Requirement := "Spam SHALL be moved to either the junk email folder or the quarantine folder: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "SpamAction": "Not Quarantine", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_HighConfSpamAction_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "High confidence spam SHALL be moved to either the junk email folder or the quarantine folder: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "HighConfidenceSpamAction": "Quarantine", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_HighConfSpamAction_CorrectV2 if { + ControlNumber := "Defender 2.6" + Requirement := "High confidence spam SHALL be moved to either the junk email folder or the quarantine folder: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "HighConfidenceSpamAction": "MoveToJmf", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_HighConfSpamAction_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "High confidence spam SHALL be moved to either the junk email folder or the quarantine folder: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "HighConfidenceSpamAction": "Not MoveToJmf", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_HighConfSpamAction_Incorrect_V2 if { + ControlNumber := "Defender 2.6" + Requirement := "High confidence spam SHALL be moved to either the junk email folder or the quarantine folder: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "HighConfidenceSpamAction": "Not Quarantine", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_Non_Def_SpamAction_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Spam SHOULD be moved to either the junk email folder or the quarantine folder: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "SpamAction": "Quarantine", + "Identity": "Not Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Non_Def_SpamAction_CorrectV2 if { + ControlNumber := "Defender 2.6" + Requirement := "Spam SHOULD be moved to either the junk email folder or the quarantine folder: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "SpamAction": "MoveToJmf", + "Identity": "Not Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Non_Def_SpamAction_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Spam SHOULD be moved to either the junk email folder or the quarantine folder: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "SpamAction": "Not MoveToJmf", + "Identity": "Not Default" + }, + { + "SpamAction": "Not MoveToJmf", + "Identity": "Default" + } ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti-spam policy(ies) found where spam is not being sent to the Quarantine folder or the Junk Mail Folder: Not Default" +} + +test_Non_Def_SpamAction_Incorrect_V2 if { + ControlNumber := "Defender 2.6" + Requirement := "Spam SHOULD be moved to either the junk email folder or the quarantine folder: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "SpamAction": "Not Quarantine", + "Identity": "Not Default" + }, + { + "SpamAction": "Not Quarantine", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti-spam policy(ies) found where spam is not being sent to the Quarantine folder or the Junk Mail Folder: Not Default" +} + +test_Non_Def_HighConfSpamAction_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "High confidence spam SHOULD be moved to either the junk email folder or the quarantine folder: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "HighConfidenceSpamAction": "Quarantine", + "Identity": "Not Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Non_Def_HighConfSpamAction_CorrectV2 if { + ControlNumber := "Defender 2.6" + Requirement := "High confidence spam SHOULD be moved to either the junk email folder or the quarantine folder: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "HighConfidenceSpamAction": "MoveToJmf", + "Identity": "Not Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Non_Def_HighConfSpamAction_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "High confidence spam SHOULD be moved to either the junk email folder or the quarantine folder: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "HighConfidenceSpamAction": "Not MoveToJmf", + "Identity": "Not Default" + }, + { + "HighConfidenceSpamAction": "Not MoveToJmf", + "Identity": "Default" + } ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti-spam policy(ies) found where high confidence spam is not being sent to the Quarantine folder or the Junk Mail Folder: Not Default"} + +test_Non_Def_HighConfSpamAction_Incorrect_V2 if { + ControlNumber := "Defender 2.6" + Requirement := "High confidence spam SHOULD be moved to either the junk email folder or the quarantine folder: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "HighConfidenceSpamAction": "Not Quarantine", + "Identity": "Not Default" + }, + { + "HighConfidenceSpamAction": "Not Quarantine", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti-spam policy(ies) found where high confidence spam is not being sent to the Quarantine folder or the Junk Mail Folder: Not Default"} + +# +# Policy 3 +#-- +test_PhishingSpamAction_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Phishing SHALL be quarantined: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "PhishSpamAction": "Quarantine", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_PhishingSpamAction_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Phishing SHALL be quarantined: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "PhishSpamAction": "Not Quarantine", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_HighConfidencePhishAction_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "High confidence phishing SHALL be quarantined: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "HighConfidencePhishAction": "Quarantine", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_HighConfidencePhishAction_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "High confidence phishing SHALL be quarantined: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "HighConfidencePhishAction": "Not Quarantine", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_Non_Def_PhishingSpamAction_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Phishing SHOULD be quarantined: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "PhishSpamAction": "Quarantine", + "Identity": "Not Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Non_Def_PhishingSpamAction_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Phishing SHOULD be quarantined: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "PhishSpamAction": "Not Quarantine", + "Identity": "Not Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti-spam policy(ies) found where phishing isn't moved to the quarantine folder: Not Default" +} + +test_Non_Def_HighConfidencePhishAction_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "High confidence phishing SHOULD be quarantined: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "HighConfidencePhishAction": "Quarantine", + "Identity": "Not Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Non_Def_HighConfidencePhishAction_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "High confidence phishing SHOULD be quarantined: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "HighConfidencePhishAction": "Not Quarantine", + "Identity": "Not Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti-spam policy(ies) found where high-confidence phishing isn't moved to quarantine folder: Not Default" +} + +# +# Policy 4 +#-- +test_BulkSpamAction_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Bulk email SHOULD be moved to either the junk email folder or the quarantine folder: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "BulkSpamAction": "Quarantine", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_BulkSpamAction_CorrectV2 if { + ControlNumber := "Defender 2.6" + Requirement := "Bulk email SHOULD be moved to either the junk email folder or the quarantine folder: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "BulkSpamAction": "MoveToJmf", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_BulkSpamAction_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Bulk email SHOULD be moved to either the junk email folder or the quarantine folder: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "BulkSpamAction": "Not Quarantine", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_BulkSpamAction_Incorrect_V2 if { + ControlNumber := "Defender 2.6" + Requirement := "Bulk email SHOULD be moved to either the junk email folder or the quarantine folder: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "BulkSpamAction": "Not MoveToJmf", + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +# +# Policy 5 +#-- +test_QuarantineRetentionPeriod_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Spam in quarantine SHOULD be retained for at least 30 days: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "QuarantineRetentionPeriod": 30, + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_QuarantineRetentionPeriod_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Spam in quarantine SHOULD be retained for at least 30 days: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "QuarantineRetentionPeriod": 31, + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_QuarantineRetentionPeriod_Incorrect_V2 if { + ControlNumber := "Defender 2.6" + Requirement := "Spam in quarantine SHOULD be retained for at least 30 days: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "QuarantineRetentionPeriod": 29, + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_QuarantineRetentionPeriodCustom_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Spam in quarantine SHOULD be retained for at least 30 days: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "QuarantineRetentionPeriod": 30, + "Identity": "Custom 1" + }, + { + "QuarantineRetentionPeriod": 3, + "Identity": "Default" # This policy should be ignored + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_QuarantineRetentionPeriodCustom_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Spam in quarantine SHOULD be retained for at least 30 days: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "QuarantineRetentionPeriod": 30, + "Identity": "Custom 1" + }, + { + "QuarantineRetentionPeriod": 29, + "Identity": "Custom 2" + }, + { + "QuarantineRetentionPeriod": 3, + "Identity": "Default" # This policy should be ignored + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti-spam policy(ies) found where spam in quarantine isn't retained for 30 days: Custom 2" +} + +# +# Policy 6 +#-- +test_InlineSafetyTipsEnabled_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Spam safety tips SHOULD be turned on: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "InlineSafetyTipsEnabled": true, + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_InlineSafetyTipsEnabled_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Spam safety tips SHOULD be turned on: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "InlineSafetyTipsEnabled": false, + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_InlineSafetyTipsEnabledCustom_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Spam safety tips SHOULD be turned on: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "InlineSafetyTipsEnabled": false, + "Identity": "Default" # should be ignored + }, + { + "InlineSafetyTipsEnabled": true, + "Identity": "Custom 1" + }, + { + "InlineSafetyTipsEnabled": true, + "Identity": "Custom 2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_InlineSafetyTipsEnabledCustom_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Spam safety tips SHOULD be turned on: non-default policies" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "InlineSafetyTipsEnabled": false, + "Identity": "Default" # should be ignored + }, + { + "InlineSafetyTipsEnabled": false, + "Identity": "Custom 1" + }, + { + "InlineSafetyTipsEnabled": true, + "Identity": "Custom 2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti-spam policy(ies) found where spam safety tips is disabled: Custom 1" +} + +# +# Policy 7 +#-- +test_ZapEnabled_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Zero-hour auto purge (ZAP) SHALL be enabled: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "ZapEnabled": true, + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_ZapEnabled_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Zero-hour auto purge (ZAP) SHALL be enabled: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "ZapEnabled": false, + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_SpamZapEnabled_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Zero-hour auto purge (ZAP) SHALL be enabled for spam messages: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "SpamZapEnabled": true, + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_SpamZapEnabled_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Zero-hour auto purge (ZAP) SHALL be enabled for spam messages: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "SpamZapEnabled": false, + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_PhishZapEnabled_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Zero-hour auto purge (ZAP) SHALL be enabled for phishing: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "PhishZapEnabled": true, + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_PhishZapEnabled_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Zero-hour auto purge (ZAP) SHALL be enabled for phishing: default policy" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "PhishZapEnabled": false, + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_ZapEnabledCustom_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Zero-hour auto purge (ZAP) SHOULD be enabled: non-default" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "ZapEnabled": true, + "Identity": "Not Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_ZapEnabledCustom_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Zero-hour auto purge (ZAP) SHOULD be enabled: non-default" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "ZapEnabled": false, + "Identity": "Not Default" + }, + { + "ZapEnabled": false, + "Identity": "Default" # SHOULD be ignored + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti-spam policies found where Zero-hour auto purge is disabled: Not Default" +} + +test_SpamZapEnabled_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Zero-hour auto purge (ZAP) SHOULD be enabled for Spam: non-default" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "SpamZapEnabled": true, + "Identity": "Not Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_SpamZapEnabledCustom_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Zero-hour auto purge (ZAP) SHOULD be enabled for Spam: non-default" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "SpamZapEnabled": false, + "Identity": "Not Default" + }, + { + "SpamZapEnabled": false, # should be ignored + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti-spam policies found where Zero-hour auto purge for spam is disabled: Not Default" +} + +test_PhishZapEnabledCustom_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Zero-hour auto purge (ZAP) SHOULD be enabled for phishing: non-default" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "PhishZapEnabled": true, + "Identity": "Not Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_PhishZapEnabledCustom_Incorrect if { + ControlNumber := "Defender 2.6" + Requirement := "Zero-hour auto purge (ZAP) SHOULD be enabled for phishing: non-default" + + Output := tests with input as { + "hosted_content_filter_policies": [ + { + "PhishZapEnabled": false, + "Identity": "Not Default" + }, + { + "PhishZapEnabled": false, # should be ignored + "Identity": "Default" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 custom anti-spam policy(ies) found where Zero-hour auto purge for phishing is disabled: Not Default" +} + +# +# Policy 8 +#-- +test_NotImplemented_Correct if { + ControlNumber := "Defender 2.6" + Requirement := "Allowed senders MAY be added but allowed domains SHALL NOT be added" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Defender Secure Configuration Baseline policy 2.8 for instructions on manual check" +} \ No newline at end of file diff --git a/Testing/Defender/DefenderConfig2_07_test.rego b/Testing/Defender/DefenderConfig2_07_test.rego new file mode 100644 index 0000000000..e8214a5f3e --- /dev/null +++ b/Testing/Defender/DefenderConfig2_07_test.rego @@ -0,0 +1,1358 @@ +package defender +import future.keywords + + +# +# Policy 1 +#-- +test_Domains_Correct if { + ControlNumber := "Defender 2.7" + Requirement := "The Safe Links Policy SHALL include all agency domains-and by extension-all users" + + Output := tests with input as { + "safe_links_rules" : [ + { + "RecipientDomainIs": ["Test Domain"], + "Identity" : "Test Policy", + "State" : "Enabled" + } + ], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_DomainName_Incorrect_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "The Safe Links Policy SHALL include all agency domains-and by extension-all users" + + Output := tests with input as { + "safe_links_rules" : [ + { + "RecipientDomainIs": [""], + "Identity" : "Test Policy", + "State" : "Enabled" + } + ], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No policy found that applies to all domains: Test Domain" +} + +test_DomainName_Incorrect_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "The Safe Links Policy SHALL include all agency domains-and by extension-all users" + + Output := tests with input as { + "safe_links_rules" : [ + { + "RecipientDomainIs": ["Test Domain2"], + "Identity" : "Test Policy", + "State" : "Enabled" + } + ], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No policy found that applies to all domains: Test Domain" +} + +test_Domains_Incorrect_v3 if { + ControlNumber := "Defender 2.7" + Requirement := "The Safe Links Policy SHALL include all agency domains-and by extension-all users" + + Output := tests with input as { + "safe_links_rules" : [ + { + "RecipientDomainIs": ["Test Domain"], + "Identity" : "Test Policy", + "State" : "Disabled" + } + ], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No policy found that applies to all domains: Test Domain" +} + +test_Domains_Incorrect_V4 if { + # If no defender license is present, the provider will output "safe_links_rules" + # and "safe_links_policies" as empty lists + ControlNumber := "Defender 2.7" + Requirement := "The Safe Links Policy SHALL include all agency domains-and by extension-all users" + + Output := tests with input as { + "safe_links_rules": [], + "safe_links_policies": [], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "defender_license" : false + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met **NOTE: Your tenant appears to not have a license for Defender for Microsoft Defender for Office 365, which is required for this feature.**" +} + +# +# Policy 2 +#-- +test_EnableSafeLinksForEmail_Correct_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "URL rewriting and malicious link click checking SHALL be enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Enabled", + "Identity" : "a" + }, + { + "State" : "Enabled", + "Identity" : "b" + } + ], + "safe_links_policies": [ + { + "EnableSafeLinksForEmail" : true, + "Identity": "a" + }, + { + "EnableSafeLinksForEmail" : false, + "Identity": "b" + }, + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableSafeLinksForEmail_Correct_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "URL rewriting and malicious link click checking SHALL be enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Enabled", + "Identity" : "a" + }, + { + "State" : "Disabled", + "Identity" : "b" + } + ], + "safe_links_policies": [ + { + "EnableSafeLinksForEmail" : true, + "Identity": "a" + }, + { + "EnableSafeLinksForEmail" : true, + "Identity": "b" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableSafeLinksForEmail_Incorrect_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "URL rewriting and malicious link click checking SHALL be enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "Identity" : "Default", + "State" : "Enabled" + } + ], + "safe_links_policies": [ + { + "EnableSafeLinksForEmail" : false, + "Identity": "Default" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableSafeLinksForEmail_Incorrect_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "URL rewriting and malicious link click checking SHALL be enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "Identity" : "Default", + "State" : "Disabled" + } + ], + "safe_links_policies": [ + { + "EnableSafeLinksForEmail" : true, + "Identity": "Default" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableSafeLinksForEmail_Incorrect_V3 if { + # If no defender license is present, the provider will output "safe_links_rules" + # and "safe_links_policies" as empty lists + ControlNumber := "Defender 2.7" + Requirement := "URL rewriting and malicious link click checking SHALL be enabled" + + Output := tests with input as { + "safe_links_rules" : [], + "safe_links_policies" : [], + "defender_license" : false + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met **NOTE: Your tenant appears to not have a license for Defender for Microsoft Defender for Office 365, which is required for this feature.**" +} + +# +# Policy 3 +#-- +test_EnableSafeLinksForTeams_Correct_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "Malicious link click checking SHALL be enabled with Microsoft Teams" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Enabled", + "Identity" : "a" + }, + { + "State" : "Enabled", + "Identity" : "a" + } + ], + "safe_links_policies": [ + { + "EnableSafeLinksForTeams" : true, + "Identity": "a" + }, + { + "EnableSafeLinksForTeams" : false, + "Identity": "b" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableSafeLinksForTeams_Correct_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "Malicious link click checking SHALL be enabled with Microsoft Teams" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Disabled", + "Identity" : "a" + }, + { + "State" : "Enabled", + "Identity" : "b" + } + ], + "safe_links_policies": [ + { + "EnableSafeLinksForTeams" : true, + "Identity": "a" + }, + { + "EnableSafeLinksForTeams" : true, + "Identity": "b" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableSafeLinksForTeams_Incorrect_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "Malicious link click checking SHALL be enabled with Microsoft Teams" + + Output := tests with input as { + "safe_links_rules": [ + { + "Identity" : "Default", + "State" : "Enabled" + } + ], + "safe_links_policies": [ + { + "EnableSafeLinksForTeams" : false, + "Identity": "Default" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableSafeLinksForTeams_Incorrect_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "Malicious link click checking SHALL be enabled with Microsoft Teams" + + Output := tests with input as { + "safe_links_rules": [ + { + "Identity" : "Default", + "State" : "Disabled" + } + ], + "safe_links_policies": [ + { + "EnableSafeLinksForTeams" : false, + "Identity": "Default" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableSafeLinksForTeams_Incorrect_V3 if { + # If no defender license is present, the provider will output "safe_links_rules" + # and "safe_links_policies" as empty lists + ControlNumber := "Defender 2.7" + Requirement := "Malicious link click checking SHALL be enabled with Microsoft Teams" + + Output := tests with input as { + "safe_links_rules" : [], + "safe_links_policies" : [], + "defender_license" : false + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met **NOTE: Your tenant appears to not have a license for Defender for Microsoft Defender for Office 365, which is required for this feature.**" +} + +# +# Policy 4 +#-- +test_ScanUrls_Correct_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "Real-time suspicious URL and file-link scanning SHALL be enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Enabled", + "Identity" : "a" + }, + { + "State" : "Enabled", + "Identity" : "b" + } + ], + "safe_links_policies": [ + { + "ScanUrls" : false, + "Identity": "a" + }, + { + "ScanUrls" : true, + "Identity": "b" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_ScanUrls_Correct_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "Real-time suspicious URL and file-link scanning SHALL be enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Disabled", + "Identity" : "a" + }, + { + "State" : "Enabled", + "Identity" : "b" + } + ], + "safe_links_policies": [ + { + "ScanUrls" : true, + "Identity": "a" + }, + { + "ScanUrls" : true, + "Identity": "b" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_ScanUrls_Incorrect_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "Real-time suspicious URL and file-link scanning SHALL be enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "Identity" : "Default", + "State" : "Enabled" + } + ], + "safe_links_policies": [ + { + "ScanUrls" : false, + "Identity": "Default" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_ScanUrls_Incorrect_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "Real-time suspicious URL and file-link scanning SHALL be enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "Identity" : "Default", + "State" : "Disabled" + } + ], + "safe_links_policies": [ + { + "ScanUrls" : false, + "Identity": "Default" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_ScanUrls_Incorrect_V3 if { + # If no defender license is present, the provider will output "safe_links_rules" + # and "safe_links_policies" as empty lists + ControlNumber := "Defender 2.7" + Requirement := "Real-time suspicious URL and file-link scanning SHALL be enabled" + + Output := tests with input as { + "safe_links_rules" : [], + "safe_links_policies" : [], + "defender_license" : false + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met **NOTE: Your tenant appears to not have a license for Defender for Microsoft Defender for Office 365, which is required for this feature.**" +} + +# +# Policy 5 +#-- +test_DeliverMessageAfterScan_Correct_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "URLs SHALL be scanned completely before message delivery" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Enabled", + "Identity" : "a" + }, + { + "State" : "Enabled", + "Identity" : "b" + } + ], + "safe_links_policies": [ + { + "DeliverMessageAfterScan" : true, + "Identity": "a" + }, + { + "DeliverMessageAfterScan" : false, + "Identity": "b" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_DeliverMessageAfterScan_Correct_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "URLs SHALL be scanned completely before message delivery" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Enabled", + "Identity" : "a" + }, + { + "State" : "Disabled", + "Identity" : "b" + } + ], + "safe_links_policies": [ + { + "DeliverMessageAfterScan" : true, + "Identity": "a" + }, + { + "DeliverMessageAfterScan" : true, + "Identity": "b" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_DeliverMessageAfterScan_Incorrect_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "URLs SHALL be scanned completely before message delivery" + + Output := tests with input as { + "safe_links_rules": [ + { + "Identity" : "Default", + "State" : "Enabled" + } + ], + "safe_links_policies": [ + { + "DeliverMessageAfterScan" : false, + "Identity": "Default" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_DeliverMessageAfterScan_Incorrect_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "URLs SHALL be scanned completely before message delivery" + + Output := tests with input as { + "safe_links_rules": [ + { + "Identity" : "Default", + "State" : "Disabled" + } + ], + "safe_links_policies": [ + { + "DeliverMessageAfterScan" : false, + "Identity": "Default" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_DeliverMessageAfterScan_Incorrect_V3 if { + # If no defender license is present, the provider will output "safe_links_rules" + # and "safe_links_policies" as empty lists + ControlNumber := "Defender 2.7" + Requirement := "URLs SHALL be scanned completely before message delivery" + + Output := tests with input as { + "safe_links_rules" : [], + "safe_links_policies" : [], + "defender_license" : false + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met **NOTE: Your tenant appears to not have a license for Defender for Microsoft Defender for Office 365, which is required for this feature.**" +} + +# +# Policy 6 +#-- +test_EnableForInternalSenders_Correct_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "Internal agency email messages SHALL have safe links enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Enabled", + "Identity" : "a" + }, + { + "State" : "Enabled", + "Identity" : "b" + } + ], + "safe_links_policies": [ + { + "Identity" : "a", + "EnableForInternalSenders" : false + }, + { + "Identity" : "b", + "EnableForInternalSenders" : true + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableForInternalSenders_Correct_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "Internal agency email messages SHALL have safe links enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Disabled", + "Identity" : "a" + }, + { + "State" : "Enabled", + "Identity" : "b" + } + ], + "safe_links_policies": [ + { + "Identity" : "a", + "EnableForInternalSenders" : true + }, + { + "Identity" : "b", + "EnableForInternalSenders" : true + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableForInternalSenders_Incorrect_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "Internal agency email messages SHALL have safe links enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "Identity" : "Default", + "State" : "Enabled" + } + ], + "safe_links_policies": [ + { + "Identity" : "Not Built-in Protection Policy", + "EnableForInternalSenders" : false + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableForInternalSenders_Incorrect_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "Internal agency email messages SHALL have safe links enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "Identity" : "Default", + "State" : "Disabled" + } + ], + "safe_links_policies": [ + { + "Identity" : "Not Built-in Protection Policy", + "EnableForInternalSenders" : false + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableForInternalSenders_Incorrect_V3 if { + # If no defender license is present, the provider will output "safe_links_rules" + # and "safe_links_policies" as empty lists + ControlNumber := "Defender 2.7" + Requirement := "Internal agency email messages SHALL have safe links enabled" + + Output := tests with input as { + "safe_links_rules" : [], + "safe_links_policies" : [], + "defender_license" : false + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met **NOTE: Your tenant appears to not have a license for Defender for Microsoft Defender for Office 365, which is required for this feature.**" +} + +# +# Policy 7 +#-- +test_TrackClicks_Correct_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "User click tracking SHALL be enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Enabled", + "Identity" : "a" + }, + { + "State" : "Enabled", + "Identity" : "b" + } + ], + "safe_links_policies": [ + { + "TrackClicks" : false, + "Identity": "a" + }, + { + "TrackClicks" : true, + "Identity": "b" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_TrackClicks_Correct_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "User click tracking SHALL be enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Enabled", + "Identity" : "a" + }, + { + "State" : "Disabled", + "Identity" : "b" + } + ], + "safe_links_policies": [ + { + "TrackClicks" : true, + "Identity": "a" + }, + { + "TrackClicks" : true, + "Identity": "b" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_TrackClicks_Incorrect_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "User click tracking SHALL be enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "Identity" : "Default", + "State" : "Enabled" + } + ], + "safe_links_policies": [ + { + "TrackClicks" : false, + "Identity": "Default" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_TrackClicks_Incorrect_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "User click tracking SHALL be enabled" + + Output := tests with input as { + "safe_links_rules": [ + { + "Identity" : "Default", + "State" : "Disabled" + } + ], + "safe_links_policies": [ + { + "TrackClicks" : false, + "Identity": "Default" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_TrackClicks_Incorrect_V3 if { + # If no defender license is present, the provider will output "safe_links_rules" + # and "safe_links_policies" as empty lists + ControlNumber := "Defender 2.7" + Requirement := "User click tracking SHALL be enabled" + + Output := tests with input as { + "safe_links_rules" : [], + "safe_links_policies" : [], + "defender_license" : false + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met **NOTE: Your tenant appears to not have a license for Defender for Microsoft Defender for Office 365, which is required for this feature.**" +} + +# +# Policy 8 +#-- +test_EnableSafeLinksForOffice_Correct_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "Safe Links in Office 365 apps SHALL be turned on" + + Output := tests with input as { + "safe_links_rules": [ + { + "Identity" : "a", + "State" : "Enabled" + }, + { + "Identity" : "b", + "State" : "Enabled" + } + ], + "safe_links_policies": [ + { + "EnableSafeLinksForOffice" : true, + "Identity" : "a" + }, + { + # Only one policy needs to be true. + "EnableSafeLinksForOffice" : false, + "Identity" : "b" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableSafeLinksForOffice_Correct_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "Safe Links in Office 365 apps SHALL be turned on" + + Output := tests with input as { + "safe_links_rules": [ + { + "Identity" : "a", + "State" : "Enabled" + }, + { + # Only one policy needs to be true. + "Identity" : "b", + "State" : "Disabled" + } + ], + "safe_links_policies": [ + { + "EnableSafeLinksForOffice" : true, + "Identity" : "a" + }, + { + "EnableSafeLinksForOffice" : true, + "Identity" : "b" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableSafeLinksForOffice_Incorrect_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "Safe Links in Office 365 apps SHALL be turned on" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Disabled", + "Identity" : "a" + } + ], + "safe_links_policies": [ + { + "EnableSafeLinksForOffice" : false, + "Identity" : "a" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableSafeLinksForOffice_Incorrect_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "Safe Links in Office 365 apps SHALL be turned on" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Disabled", + "Identity" : "a" + } + ], + "safe_links_policies": [ + { + "EnableSafeLinksForOffice" : true, + "Identity" : "a" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableSafeLinksForOffice_Incorrect_V3 if { + ControlNumber := "Defender 2.7" + Requirement := "Safe Links in Office 365 apps SHALL be turned on" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Enabled", + "Identity" : "a" + } + ], + "safe_links_policies": [ + { + "EnableSafeLinksForOffice" : false, + "Identity" : "a" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableSafeLinksForOffice_Incorrect_V4 if { + # If no defender license is present, the provider will output "safe_links_rules" + # and "safe_links_policies" as empty lists + ControlNumber := "Defender 2.7" + Requirement := "Safe Links in Office 365 apps SHALL be turned on" + + Output := tests with input as { + "safe_links_rules" : [], + "safe_links_policies" : [], + "defender_license" : false + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met **NOTE: Your tenant appears to not have a license for Defender for Microsoft Defender for Office 365, which is required for this feature.**" +} + +# +# Policy 9 +#-- +test_AllowClickThrough_Correct_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "Users SHALL NOT be enabled to click through to the original URL" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Enabled", + "Identity" : "a" + }, + { + "State" : "Enabled", + "Identity" : "b" + } + ], + "safe_links_policies": [ + { + "AllowClickThrough" : false, + "Identity" : "a" + }, + { + "AllowClickThrough" : true, + "Identity" : "b" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowClickThrough_Correct_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "Users SHALL NOT be enabled to click through to the original URL" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Enabled", + "Identity" : "a" + }, + { + "State" : "Disabled", + "Identity" : "b" + } + ], + "safe_links_policies": [ + { + "AllowClickThrough" : false, + "Identity" : "a" + }, + { + "AllowClickThrough" : false, + "Identity" : "b" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowClickThrough_Incorrect_V1 if { + ControlNumber := "Defender 2.7" + Requirement := "Users SHALL NOT be enabled to click through to the original URL" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Disabled", + "Identity" : "a" + } + ], + "safe_links_policies": [ + { + "AllowClickThrough" : true, + "Identity" : "a" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_AllowClickThrough_Incorrect_V2 if { + ControlNumber := "Defender 2.7" + Requirement := "Users SHALL NOT be enabled to click through to the original URL" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Disabled", + "Identity" : "a" + } + ], + "safe_links_policies": [ + { + "AllowClickThrough" : false, + "Identity" : "a" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_AllowClickThrough_Incorrect_V3 if { + ControlNumber := "Defender 2.7" + Requirement := "Users SHALL NOT be enabled to click through to the original URL" + + Output := tests with input as { + "safe_links_rules": [ + { + "State" : "Enabled", + "Identity" : "a" + } + ], + "safe_links_policies": [ + { + "AllowClickThrough" : true, + "Identity" : "a" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_AllowClickThrough_Incorrect_V4 if { + # If no defender license is present, the provider will output "safe_links_rules" + # and "safe_links_policies" as empty lists + ControlNumber := "Defender 2.7" + Requirement := "Users SHALL NOT be enabled to click through to the original URL" + + Output := tests with input as { + "safe_links_rules" : [], + "safe_links_policies" : [], + "defender_license" : false + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met **NOTE: Your tenant appears to not have a license for Defender for Microsoft Defender for Office 365, which is required for this feature.**" +} \ No newline at end of file diff --git a/Testing/Defender/DefenderConfig2_08_test.rego b/Testing/Defender/DefenderConfig2_08_test.rego new file mode 100644 index 0000000000..63b626973b --- /dev/null +++ b/Testing/Defender/DefenderConfig2_08_test.rego @@ -0,0 +1,455 @@ +package defender +import future.keywords + + +# +# Policy 1 +#-- +test_Domains_Correct if { + ControlNumber := "Defender 2.8" + Requirement := "At least one Safe Attachments Policy SHALL include all agency domains-and by extension-all users" + + Output := tests with input as { + "safe_attachment_rules" : [ + { + "RecipientDomainIs": ["Test Domain"], + "Identity" : "Test Policy" + } + ], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_DomainName_Incorrect_V1 if { + ControlNumber := "Defender 2.8" + Requirement := "At least one Safe Attachments Policy SHALL include all agency domains-and by extension-all users" + + Output := tests with input as { + "safe_attachment_rules" : [ + { + "RecipientDomainIs": [""], + "Identity" : "Test Policy" + } + ], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No policy found that applies to all domains: Test Domain" +} + +test_DomainName_Incorrect_V2 if { + ControlNumber := "Defender 2.8" + Requirement := "At least one Safe Attachments Policy SHALL include all agency domains-and by extension-all users" + + Output := tests with input as { + "safe_attachment_rules" : [ + { + "RecipientDomainIs": ["Test Domain2"], + "Identity" : "Test Policy" + } + ], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No policy found that applies to all domains: Test Domain" +} + +test_DomainName_Incorrect_V3 if { + # If no defender license is present, the provider will output "safe_attachment_rules" + # and "safe_attachment_policies" as empty lists + ControlNumber := "Defender 2.8" + Requirement := "At least one Safe Attachments Policy SHALL include all agency domains-and by extension-all users" + + Output := tests with input as { + "safe_attachment_rules" : [], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "safe_attachment_policies" : [], + "defender_license" : false + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met **NOTE: Your tenant appears to not have a license for Defender for Microsoft Defender for Office 365, which is required for this feature.**" +} + +# +# Policy 2 +#-- +test_Action_Correct if { + ControlNumber := "Defender 2.8" + Requirement := "The action for malware in email attachments SHALL be set to block" + + Output := tests with input as { + "safe_attachment_rules": [ + { + "RecipientDomainIs": ["Test Domain"], + "Identity" : "Test Policy" + } + ], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "safe_attachment_policies" : [ + { + "Action" : "Block", + "Enable" : true, + "RedirectAddress" : "127.0.0.1", + "Identity" : "Test Policy" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_RecipientDomainIs_Incorrect_V1 if { + ControlNumber := "Defender 2.8" + Requirement := "The action for malware in email attachments SHALL be set to block" + + Output := tests with input as { + "safe_attachment_rules": [ + { + "RecipientDomainIs": [""], + "Identity" : "Test Policy" + } + ], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "safe_attachment_policies" : [ + { + "Action" : "Block", + "Enable" : true, + "RedirectAddress" : "127.0.0.1", + "Identity" : "Test Policy" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No enabled policy found with action set to block that apply to all domains" +} + +test_RecipientDomainIs_Incorrect_V2 if { + ControlNumber := "Defender 2.8" + Requirement := "The action for malware in email attachments SHALL be set to block" + + Output := tests with input as { + "safe_attachment_rules": [ + { + "RecipientDomainIs": ["Test Domain2"], + "Identity" : "Test Policy" + } + ], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "safe_attachment_policies" : [ + { + "Action" : "Block", + "Enable" : true, + "RedirectAddress" : "127.0.0.1", + "Identity" : "Test Policy" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No enabled policy found with action set to block that apply to all domains" +} + +test_Action_Incorrect_V1 if { + ControlNumber := "Defender 2.8" + Requirement := "The action for malware in email attachments SHALL be set to block" + + Output := tests with input as { + "safe_attachment_rules": [ + { + "RecipientDomainIs": ["Test Domain"], + "Identity" : "Test Policy" + } + ], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "safe_attachment_policies" : [ + { + "Action" : "Not Block", + "Enable" : true, + "RedirectAddress" : "127.0.0.1", + "Identity" : "Test Policy" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No enabled policy found with action set to block that apply to all domains" +} + +test_Action_Incorrect_V2 if { + # If no defender license is present, the provider will output "safe_attachment_rules" + # and "safe_attachment_policies" as empty lists + ControlNumber := "Defender 2.8" + Requirement := "The action for malware in email attachments SHALL be set to block" + + Output := tests with input as { + "safe_attachment_rules" : [], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "safe_attachment_policies" : [], + "defender_license" : false + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met **NOTE: Your tenant appears to not have a license for Defender for Microsoft Defender for Office 365, which is required for this feature.**" +} + +test_Enable_Incorrect if { + ControlNumber := "Defender 2.8" + Requirement := "The action for malware in email attachments SHALL be set to block" + + Output := tests with input as { + "safe_attachment_rules": [ + { + "RecipientDomainIs": ["Test Domain"], + "Identity" : "Test Policy" + } + ], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "safe_attachment_policies" : [ + { + "Action" : "Block", + "Enable" : false, + "RedirectAddress" : "127.0.0.1", + "Identity" : "Test Policy" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No enabled policy found with action set to block that apply to all domains" +} + +test_Identity_Incorrect if { + ControlNumber := "Defender 2.8" + Requirement := "The action for malware in email attachments SHALL be set to block" + + Output := tests with input as { + "safe_attachment_rules": [ + { + "RecipientDomainIs": ["Test Domain"], + "Identity" : "Test Policy" + } + ], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "safe_attachment_policies" : [ + { + "Action" : "Block", + "Enable" : true, + "RedirectAddress" : "127.0.0.1", + "Identity" : "" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No enabled policy found with action set to block that apply to all domains" +} + +# +# Policy 3 +#-- +test_RedirectPolicies_Correct if { + ControlNumber := "Defender 2.8" + Requirement := "Redirect emails with detected attachments to an agency-specified email SHOULD be enabled" + + Output := tests with input as { + "safe_attachment_rules": [ + { + "RecipientDomainIs": ["Test Domain"], + "Identity" : "Test Policy" + } + ], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "safe_attachment_policies" : [ + { + "Action" : "Block", + "Enable" : true, + "RedirectAddress" : "127.0.0.1", + "Identity" : "Test Policy" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_RedirectAddress_Incorrect if { + ControlNumber := "Defender 2.8" + Requirement := "Redirect emails with detected attachments to an agency-specified email SHOULD be enabled" + + Output := tests with input as { + "safe_attachment_rules": [ + { + "RecipientDomainIs": ["Test Domain"], + "Identity" : "Test Policy" + } + ], + "all_domains" : [ + { + "DomainName" : "Test Domain" + } + ], + "safe_attachment_policies" : [ + { + "Action" : "Block", + "Enable" : true, + "RedirectAddress" : "", + "Identity" : "Test Policy" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No enabled policy found with action set to block and at least one contact specified" +} + +# +# Policy 4 +#-- +test_Spot_Correct if { + ControlNumber := "Defender 2.8" + Requirement := "Safe attachments SHOULD be enabled for SharePoint, OneDrive, and Microsoft Teams" + + Output := tests with input as { + "atp_policy_for_o365" : [ + { + "EnableATPForSPOTeamsODB" : true, + "Identity" : "Default" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Spot_Incorrect if { + ControlNumber := "Defender 2.8" + Requirement := "Safe attachments SHOULD be enabled for SharePoint, OneDrive, and Microsoft Teams" + + Output := tests with input as { + "atp_policy_for_o365" : [ + { + "EnableATPForSPOTeamsODB" : false, + "Identity" : "Default" + } + ], + "defender_license" : true + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} \ No newline at end of file diff --git a/Testing/Defender/DefenderConfig2_09_test.rego b/Testing/Defender/DefenderConfig2_09_test.rego new file mode 100644 index 0000000000..0fa1e3e6ca --- /dev/null +++ b/Testing/Defender/DefenderConfig2_09_test.rego @@ -0,0 +1,233 @@ +package defender +import future.keywords + + +# +# Policy 1 +#-- +test_Disabled_Correct if { + ControlNumber := "Defender 2.9" + Requirement := "At a minimum, the alerts required by the Exchange Online Minimum Viable Secure Configuration Baseline SHALL be enabled" + + Output := tests with input as { + "protection_alerts": [ + { + "Name": "Suspicious email sending patterns detected", + "Disabled": false + }, + { + "Name": "Unusual increase in email reported as phish", + "Disabled": false + }, + { + "Name": "Suspicious Email Forwarding Activity", + "Disabled": false + }, + { + "Name": "Messages have been delayed", + "Disabled": false + }, + { + "Name": "Tenant restricted from sending unprovisioned email", + "Disabled": false + }, + { + "Name": "User restricted from sending email", + "Disabled": false + }, + { + "Name": "Malware campaign detected after delivery", + "Disabled": false + }, + { + "Name": "A potentially malicious URL click was detected", + "Disabled": false + }, + { + "Name": "Suspicious connector activity", + "Disabled": false + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Disabled_Correct_V2 if { + # Test having extra alerts enabled that aren't required by the baseline + # SHOULDn't matter + ControlNumber := "Defender 2.9" + Requirement := "At a minimum, the alerts required by the Exchange Online Minimum Viable Secure Configuration Baseline SHALL be enabled" + + Output := tests with input as { + "protection_alerts": [ + { + "Name": "Suspicious email sending patterns detected", + "Disabled": false + }, + { + "Name": "Unusual increase in email reported as phish", + "Disabled": false + }, + { + "Name": "Suspicious Email Forwarding Activity", + "Disabled": false + }, + { + "Name": "Messages have been delayed", + "Disabled": false + }, + { + "Name": "Tenant restricted from sending unprovisioned email", + "Disabled": false + }, + { + "Name": "User restricted from sending email", + "Disabled": false + }, + { + "Name": "Malware campaign detected after delivery", + "Disabled": false + }, + { + "Name": "A potentially malicious URL click was detected", + "Disabled": false + }, + { + "Name": "Suspicious connector activity", + "Disabled": false + }, + { + "Name": "Successful exact data match upload", # Not required + "Disabled": false + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Disabled_Incorrect if { + ControlNumber := "Defender 2.9" + Requirement := "At a minimum, the alerts required by the Exchange Online Minimum Viable Secure Configuration Baseline SHALL be enabled" + + Output := tests with input as { + "protection_alerts": [ + { + "Name": "Suspicious email sending patterns detected", + "Disabled": true # SHOULD be false + }, + { + "Name": "Unusual increase in email reported as phish", + "Disabled": false + }, + { + "Name": "Suspicious Email Forwarding Activity", + "Disabled": false + }, + { + "Name": "Messages have been delayed", + "Disabled": false + }, + { + "Name": "Tenant restricted from sending unprovisioned email", + "Disabled": false + }, + { + "Name": "User restricted from sending email", + "Disabled": false + }, + { + "Name": "Malware campaign detected after delivery", + "Disabled": false + }, + { + "Name": "A potentially malicious URL click was detected", + "Disabled": false + }, + { + "Name": "Suspicious connector activity", + "Disabled": false + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 disabled required alert(s) found: Suspicious email sending patterns detected" +} + +test_Disabled_Incorrect_V2 if { + # What happens if the alert is entirely missing instead of just disabled? + ControlNumber := "Defender 2.9" + Requirement := "At a minimum, the alerts required by the Exchange Online Minimum Viable Secure Configuration Baseline SHALL be enabled" + + Output := tests with input as { + "protection_alerts": [ + { + "Name": "Unusual increase in email reported as phish", + "Disabled": false + }, + { + "Name": "Suspicious Email Forwarding Activity", + "Disabled": false + }, + { + "Name": "Messages have been delayed", + "Disabled": false + }, + { + "Name": "Tenant restricted from sending unprovisioned email", + "Disabled": false + }, + { + "Name": "User restricted from sending email", + "Disabled": false + }, + { + "Name": "Malware campaign detected after delivery", + "Disabled": false + }, + { + "Name": "A potentially malicious URL click was detected", + "Disabled": false + }, + { + "Name": "Suspicious connector activity", + "Disabled": false + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 disabled required alert(s) found: Suspicious email sending patterns detected" +} + +# +# Policy 2 +#-- +test_NotImplemented_Correct if { + ControlNumber := "Defender 2.9" + Requirement := "The alerts SHOULD be sent to a monitored address or incorporated into a SIEM" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Defender Secure Configuration Baseline policy 2.9 for instructions on manual check" +} \ No newline at end of file diff --git a/Testing/Defender/DefenderConfig2_10_test.rego b/Testing/Defender/DefenderConfig2_10_test.rego new file mode 100644 index 0000000000..22841ce0d0 --- /dev/null +++ b/Testing/Defender/DefenderConfig2_10_test.rego @@ -0,0 +1,72 @@ +package defender +import future.keywords + + +# +# Policy 1 +#-- +test_AdminAuditLogEnabled_Correct if { + ControlNumber := "Defender 2.10" + Requirement := "Unified audit logging SHALL be enabled" + + Output := tests with input as { + "admin_audit_log_config": { + "UnifiedAuditLogIngestionEnabled" : true + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AdminAuditLogEnabled_Incorrect if { + ControlNumber := "Defender 2.10" + Requirement := "Unified audit logging SHALL be enabled" + + Output := tests with input as { + "admin_audit_log_config": { + "UnifiedAuditLogIngestionEnabled" : false + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +# +# Policy 2 +#-- +test_NotImplemented_Correct_V1 if { + ControlNumber := "Defender 2.10" + Requirement := "Advanced audit SHALL be enabled" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Defender Secure Configuration Baseline policy 2.10 for instructions on manual check" +} + +# +# Policy 3 +#-- +test_NotImplemented_Correct_V2 if { + ControlNumber := "Defender 2.10" + Requirement := "Audit logs SHALL be maintained for at least the minimum duration dictated by OMB M-21-31" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Defender Secure Configuration Baseline policy 2.10 for instructions on manual check" +} diff --git a/Testing/EXO/EXOConfig2_01_test.rego b/Testing/EXO/EXOConfig2_01_test.rego new file mode 100644 index 0000000000..cb699a6ea4 --- /dev/null +++ b/Testing/EXO/EXOConfig2_01_test.rego @@ -0,0 +1,102 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_AutoForwardEnabled_Correct if { + ControlNumber := "EXO 2.1" + Requirement := "Automatic forwarding to external domains SHALL be disabled" + + Output := tests with input as { + "remote_domains": [ + { + "AutoForwardEnabled" : false, + "DomainName" : "Test name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AutoForwardEnabled_Incorrect_V1 if { + ControlNumber := "EXO 2.1" + Requirement := "Automatic forwarding to external domains SHALL be disabled" + + Output := tests with input as { + "remote_domains": [ + { + "AutoForwardEnabled" : true, + "DomainName" : "Test name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 remote domain(s) that allows automatic forwarding: Test name" +} + +test_AutoForwardEnabled_Incorrect_V2 if { + ControlNumber := "EXO 2.1" + Requirement := "Automatic forwarding to external domains SHALL be disabled" + + Output := tests with input as { + "remote_domains": [ + { + "AutoForwardEnabled" : true, + "DomainName" : "Test name" + }, + { + "AutoForwardEnabled" : true, + "DomainName" : "Test name 2" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "2 remote domain(s) that allows automatic forwarding: Test name, Test name 2" +} + +test_AutoForwardEnabled_Incorrect_V3 if { + ControlNumber := "EXO 2.1" + Requirement := "Automatic forwarding to external domains SHALL be disabled" + + Output := tests with input as { + "remote_domains": [ + { + "AutoForwardEnabled" : true, + "DomainName" : "Test name" + }, + { + "AutoForwardEnabled" : true, + "DomainName" : "Test name 2" + }, + { + "AutoForwardEnabled" : false, + "DomainName" : "Test name 3" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "2 remote domain(s) that allows automatic forwarding: Test name, Test name 2" +} + +# TODO: what about the case where "remote_domains" is empty? +# Is this possible? Or will the default domain "*" always be there? +# Requires exploration online and manual testing. \ No newline at end of file diff --git a/Testing/EXO/EXOConfig2_02_test.rego b/Testing/EXO/EXOConfig2_02_test.rego new file mode 100644 index 0000000000..a6a414d735 --- /dev/null +++ b/Testing/EXO/EXOConfig2_02_test.rego @@ -0,0 +1,197 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_NotImplemented_Correct if { + ControlNumber := "EXO 2.2" + Requirement := "A list of approved IP addresses for sending mail SHALL be maintained" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Exchange Online Secure Configuration Baseline policy 2.# for instructions on manual check" +} + +# +# Policy 2 +#-- +test_Rdata_Correct if { + ControlNumber := "EXO 2.2" + Requirement := "An SPF policy(s) that designates only these addresses as approved senders SHALL be published" + + Output := tests with input as { + "spf_records": [ + { + "rdata" : ["v=spf1 "], + "domain" : "Test name" + } + ] + } + + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Rdata_Correct_V2 if { + ControlNumber := "EXO 2.2" + Requirement := "An SPF policy(s) that designates only these addresses as approved senders SHALL be published" + + Output := tests with input as { + "spf_records": [ + { + "rdata" : ["v=spf1 something"], + "domain" : "Test name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Rdata_Incorrect if { + ControlNumber := "EXO 2.2" + Requirement := "An SPF policy(s) that designates only these addresses as approved senders SHALL be published" + + Output := tests with input as { + "spf_records": [ + { + "rdata" : ["spf1 "], + "domain" : "Test name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 of 1 agency domain(s) found in violation: Test name" +} + +test_Rdata_Incorrect_V2 if { + ControlNumber := "EXO 2.2" + Requirement := "An SPF policy(s) that designates only these addresses as approved senders SHALL be published" + + Output := tests with input as { + "spf_records": [ + { + "rdata" : [""], + "domain" : "Test name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 of 1 agency domain(s) found in violation: Test name" +} + +test_Rdata_Incorrect_V3 if { + ControlNumber := "EXO 2.2" + Requirement := "An SPF policy(s) that designates only these addresses as approved senders SHALL be published" + + Output := tests with input as { + "spf_records": [ + { + "rdata" : ["v=spf1 "], + "domain" : "good.com" + }, + { + "rdata" : [""], + "domain" : "bad.com" + }, + { + "rdata" : [""], + "domain" : "2bad.com" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + contains(RuleOutput[0].ReportDetails, "2 of 3 agency domain(s) found in violation: ") + startswith(RuleOutput[0].ReportDetails, "2 of 3 agency domain(s) found in violation: ") + contains(RuleOutput[0].ReportDetails, "bad.com") # I'm not sure + + # if we can make any assumptions about the order these domains + # will be printed in, hence the "contains" operator instead of == + contains(RuleOutput[0].ReportDetails, "2bad.com") +} + +test_Rdata_Multiple_Correct_V1 if { + ControlNumber := "EXO 2.2" + Requirement := "An SPF policy(s) that designates only these addresses as approved senders SHALL be published" + + Output := tests with input as { + "spf_records": [ + { + "rdata" : ["v=spf1 ", "extra stuff that shouldn't matter"], + "domain" : "good.com" + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Rdata_Multiple_Correct_V2 if { + ControlNumber := "EXO 2.2" + Requirement := "An SPF policy(s) that designates only these addresses as approved senders SHALL be published" + + Output := tests with input as { + "spf_records": [ + { + "rdata" : ["extra stuff that shouldn't matter", "v=spf1 "], + "domain" : "good.com" + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Rdata_Multiple_Incorrect if { + ControlNumber := "EXO 2.2" + Requirement := "An SPF policy(s) that designates only these addresses as approved senders SHALL be published" + + Output := tests with input as { + "spf_records": [ + { + "rdata" : ["extra stuff that shouldn't matter", "hello world"], + "domain" : "bad.com" + }, + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 of 1 agency domain(s) found in violation: bad.com" +} \ No newline at end of file diff --git a/Testing/EXO/EXOConfig2_03_test.rego b/Testing/EXO/EXOConfig2_03_test.rego new file mode 100644 index 0000000000..4ae9e0e0a7 --- /dev/null +++ b/Testing/EXO/EXOConfig2_03_test.rego @@ -0,0 +1,347 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_Enabled_Correct_V1 if { + ControlNumber := "EXO 2.3" + Requirement := "DKIM SHOULD be enabled for any custom domain" + + Output := tests with input as { + "dkim_config": [ + { + "Enabled" : true, + "Domain" : "test.name" + } + ], + "dkim_records": [ + { + "rdata" : "v=DKIM1;", + "domain" : "test.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Enabled_Correct_V2 if { + # Test with incorrect default domain + ControlNumber := "EXO 2.3" + Requirement := "DKIM SHOULD be enabled for any custom domain" + + Output := tests with input as { + "dkim_config": [ + { + "Enabled" : true, + "Domain" : "test.name" + }, + { + "Enabled" : false, + "Domain" : "example.onmicrosoft.com" # The baseline policy + # doesn't apply to the default domains, so this should be + # ignored. + } + ], + "dkim_records": [ + { + "rdata" : "v=DKIM1;", + "domain" : "test.name" + }, + { + "rdata" : "", + "domain" : "example.onmicrosoft.com" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + }, + { + "rdata" : "spf1 ", + "domain" : "example.onmicrosoft.com" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Enabled_Correct_V3 if { + # Test for multiple custom domains + ControlNumber := "EXO 2.3" + Requirement := "DKIM SHOULD be enabled for any custom domain" + + Output := tests with input as { + "dkim_config": [ + { + "Enabled" : true, + "Domain" : "test.name" + }, + { + "Enabled" : true, + "Domain" : "test2.name" + } + ], + "dkim_records": [ + { + "rdata" : "v=DKIM1;", + "domain" : "test.name" + }, + { + "rdata" : "v=DKIM1;", + "domain" : "test2.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + }, + { + "rdata" : "spf1 ", + "domain" : "test2.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Enabled_Correct_V4 if { + # Test for no custom domains, just the default domain + ControlNumber := "EXO 2.3" + Requirement := "DKIM SHOULD be enabled for any custom domain" + + Output := tests with input as { + "dkim_config": [ + { + "Enabled" : true, + "Domain" : "example.onmicrosoft.com" + } + ], + "dkim_records": [ + { + "rdata" : "v=DKIM1;", + "domain" : "example.onmicrosoft.com" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "example.onmicrosoft.com" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Enabled_Incorrect if { + ControlNumber := "EXO 2.3" + Requirement := "DKIM SHOULD be enabled for any custom domain" + + Output := tests with input as { + "dkim_config": [ + { + "Enabled" : false, + "Domain" : "test.name" + } + ], + "dkim_records": [ + { + "rdata" : "v=DKIM1;", + "domain" : "test.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 of 1 agency domain(s) found in violation: test.name" +} + +test_Rdata_Incorrect if { + ControlNumber := "EXO 2.3" + Requirement := "DKIM SHOULD be enabled for any custom domain" + + Output := tests with input as { + "dkim_config": [ + { + "Enabled" : true, + "Domain" : "test.name" + } + ], + "dkim_records": [ + { + "rdata" : " ", + "domain" : "test.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 of 1 agency domain(s) found in violation: test.name" +} + +test_Rdata_Incorrect_V2 if { + ControlNumber := "EXO 2.3" + Requirement := "DKIM SHOULD be enabled for any custom domain" + + Output := tests with input as { + "dkim_config": [ + { + "Enabled" : true, + "Domain" : "test.name" + } + ], + "dkim_records": [ + { + "rdata" : "Hello World", + "domain" : "test.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 of 1 agency domain(s) found in violation: test.name" +} + +test_Enabled_Correct_V2 if { + ControlNumber := "EXO 2.3" + Requirement := "DKIM SHOULD be enabled for any custom domain" + + Output := tests with input as { + "dkim_config": [ + { + "Enabled" : true, + "Domain" : "test.name" + }, + { + "Enabled" : true, + "Domain" : "test2.name" + } + ], + "dkim_records": [ + { + "rdata" : "v=DKIM1;", + "domain" : "test.name" + }, + { + "rdata" : "v=DKIM1;", + "domain" : "test2.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + }, + { + "rdata" : "spf1 ", + "domain" : "test2.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Enabled_Inorrect_V3 if { + ControlNumber := "EXO 2.3" + Requirement := "DKIM SHOULD be enabled for any custom domain" + + Output := tests with input as { + "dkim_config": [ + { + "Enabled" : true, + "Domain" : "test.name" + }, + { + "Enabled" : false, + "Domain" : "test2.name" + } + ], + "dkim_records": [ + { + "rdata" : "v=DKIM1;", + "domain" : "test.name" + }, + { + "rdata" : "v=DKIM1;", + "domain" : "test2.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + }, + { + "rdata" : "spf1 ", + "domain" : "test2.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 of 2 agency domain(s) found in violation: test2.name" +} \ No newline at end of file diff --git a/Testing/EXO/EXOConfig2_04_test.rego b/Testing/EXO/EXOConfig2_04_test.rego new file mode 100644 index 0000000000..19002ed66d --- /dev/null +++ b/Testing/EXO/EXOConfig2_04_test.rego @@ -0,0 +1,335 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_Rdata_Correct if { + ControlNumber := "EXO 2.4" + Requirement := "A DMARC policy SHALL be published for every second-level domain" + + Output := tests with input as { + "dmarc_records":[ + { + "rdata" : "v=DMARC1; p=reject; pct=100; rua=mailto:DMARC@hq.dhs.gov, mailto:reports@dmarc.cyber.dhs.gov", + "domain" : "test.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Rdata_Incorrect if { + ControlNumber := "EXO 2.4" + Requirement := "A DMARC policy SHALL be published for every second-level domain" + + Output := tests with input as { + "dmarc_records":[ + { + "rdata" : " ", + "domain" : "test.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 of 1 agency domain(s) found in violation: test.name" +} + +test_Rdata_Incorrect_V2 if { + ControlNumber := "EXO 2.4" + Requirement := "A DMARC policy SHALL be published for every second-level domain" + + Output := tests with input as { + "dmarc_records":[ + { + "rdata" : "v=DMARC1", + "domain" : "test.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 of 1 agency domain(s) found in violation: test.name" +} + +test_Rdata_Incorrect_V3 if { + ControlNumber := "EXO 2.4" + Requirement := "A DMARC policy SHALL be published for every second-level domain" + + Output := tests with input as { + "dmarc_records": [ + { + "rdata" : "v=DMARC1; p=reject; pct=100; rua=mailto:DMARC@hq.dhs.gov, mailto:reports@dmarc.cyber.dhs.gov", + "domain" : "test.name" + }, + { + "rdata" : "", + "domain" : "bad.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + }, + { + "rdata" : "spf1 ", + "domain" : "bad.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 of 2 agency domain(s) found in violation: bad.name" +} + +# +# Policy 2 +#-- +test_Rdata_Correct_V2 if { + ControlNumber := "EXO 2.4" + Requirement := "The DMARC message rejection option SHALL be \"p=reject\"" + + Output := tests with input as { + "dmarc_records": [ + { + "rdata" : "v=DMARC1; p=reject; pct=100; rua=mailto:DMARC@hq.dhs.gov, mailto:reports@dmarc.cyber.dhs.gov", + "domain" : "test.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Rdata_Incorrect_V4 if { + ControlNumber := "EXO 2.4" + Requirement := "The DMARC message rejection option SHALL be \"p=reject\"" + + Output := tests with input as { + "dmarc_records": [ + { + "rdata" : "v=DMARC1; p=none; mailto:reports@dmarc.cyber.dhs.gov mailto:jsmith@dhs.gov mailto:jsomething@dhs.gov", + "domain" : "test.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 of 1 agency domain(s) found in violation: test.name" +} + +test_Rdata_Incorrect_V5 if { + ControlNumber := "EXO 2.4" + Requirement := "The DMARC message rejection option SHALL be \"p=reject\"" + + Output := tests with input as { + "dmarc_records": [ + { + "rdata" : "v=DMARC1; mailto:reports@dmarc.cyber.dhs.gov mailto:jsmith@dhs.gov mailto:jsomething@dhs.gov", + "domain" : "test.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 of 1 agency domain(s) found in violation: test.name" +} + +# +# Policy 3 +#-- +test_Rdata_Correct_V3 if { + ControlNumber := "EXO 2.4" + Requirement := "The DMARC point of contact for aggregate reports SHALL include reports@dmarc.cyber.dhs.gov" + + Output := tests with input as { + "dmarc_records": [ + { + "rdata" : "v=DMARC1; p=reject; pct=100; rua=mailto:DMARC@hq.dhs.gov, mailto:reports@dmarc.cyber.dhs.gov", + "domain" : "test.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Rdata_Incorrect_V6 if { + ControlNumber := "EXO 2.4" + Requirement := "The DMARC point of contact for aggregate reports SHALL include reports@dmarc.cyber.dhs.gov" + + Output := tests with input as { + "dmarc_records": [ + { + "rdata" : "v=DMARC1; p=reject; pct=100;", + "domain" : "test.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 of 1 agency domain(s) found in violation: test.name" +} + +test_Rdata_Incorrect_V7 if { + ControlNumber := "EXO 2.4" + Requirement := "The DMARC point of contact for aggregate reports SHALL include reports@dmarc.cyber.dhs.gov" + + Output := tests with input as { + "dmarc_records": [ + { + "rdata" : "v=DMARC1; p=reject; pct=100; rua=mailto:reports@wrong.address", + "domain" : "test.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 of 1 agency domain(s) found in violation: test.name" +} + +# +# Policy 4 +#-- +test_Rdata_Incorrect_V4 if { + ControlNumber := "EXO 2.4" + Requirement := "An agency point of contact SHOULD be included for aggregate and/or failure reports" + + Output := tests with input as { + "dmarc_records": [ + { + "rdata" : "v=DMARC1; p=reject; pct=100; rua=mailto:DMARC@hq.dhs.gov, mailto:reports@dmarc.cyber.dhs.gov", + "domain" : "test.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Rdata_Incorrect_V8 if { + ControlNumber := "EXO 2.4" + Requirement := "An agency point of contact SHOULD be included for aggregate and/or failure reports" + + Output := tests with input as { + "dmarc_records": [ + { + "rdata" : "v=DMARC1; p=reject; pct=100; rua=mailto:reports@dmarc.cyber.dhs.gov", + "domain" : "test.name" + } + ], + "spf_records": [ + { + "rdata" : "spf1 ", + "domain" : "test.name" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 of 1 agency domain(s) found in violation: test.name" +} \ No newline at end of file diff --git a/Testing/EXO/EXOConfig2_05_test.rego b/Testing/EXO/EXOConfig2_05_test.rego new file mode 100644 index 0000000000..5a9514e93e --- /dev/null +++ b/Testing/EXO/EXOConfig2_05_test.rego @@ -0,0 +1,40 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_SmtpClientAuthenticationDisabled_Correct if { + ControlNumber := "EXO 2.5" + Requirement := "SMTP AUTH SHALL be disabled in Exchange Online" + + Output := tests with input as { + "transport_config": { + "SmtpClientAuthenticationDisabled" : true + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_SmtpClientAuthenticationDisabled_Incorrect if { + ControlNumber := "EXO 2.5" + Requirement := "SMTP AUTH SHALL be disabled in Exchange Online" + + Output := tests with input as { + "transport_config": { + "SmtpClientAuthenticationDisabled" : false + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} \ No newline at end of file diff --git a/Testing/EXO/EXOConfig2_06_test.rego b/Testing/EXO/EXOConfig2_06_test.rego new file mode 100644 index 0000000000..86798ac8ed --- /dev/null +++ b/Testing/EXO/EXOConfig2_06_test.rego @@ -0,0 +1,89 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_Domains_Contacts_Correct if { + ControlNumber := "EXO 2.6" + Requirement := "Contact folders SHALL NOT be shared with all domains, although they MAY be shared with specific domains" + + Output := tests with input as { + "sharing_policy": { + "Domains" : [ + "domain1", + "domain2" + ] + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Domains_Contacts_Incorrect if { + ControlNumber := "EXO 2.6" + Requirement := "Contact folders SHALL NOT be shared with all domains, although they MAY be shared with specific domains" + + Output := tests with input as { + "sharing_policy": { + "Domains" : [ + "domain1", + "*" + ] + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Wildcard domain (\"*\") in shared domains list, enabling sharing will all domains by default" +} + +# +# Policy 2 +#-- +test_Domains_Calender_Correct if { + ControlNumber := "EXO 2.6" + Requirement := "Calendar details SHALL NOT be shared with all domains, although they MAY be shared with specific domains" + + Output := tests with input as { + "sharing_policy": { + "Domains" : [ + "domain1", + "domain2" + ] + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_Domains_Calender_Incorrect if { + ControlNumber := "EXO 2.6" + Requirement := "Calendar details SHALL NOT be shared with all domains, although they MAY be shared with specific domains" + + Output := tests with input as { + "sharing_policy": { + "Domains" : [ + "domain1", + "*" + ] + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Wildcard domain (\"*\") in shared domains list, enabling sharing will all domains by default" +} \ No newline at end of file diff --git a/Testing/EXO/EXOConfig2_07_test.rego b/Testing/EXO/EXOConfig2_07_test.rego new file mode 100644 index 0000000000..893676ed91 --- /dev/null +++ b/Testing/EXO/EXOConfig2_07_test.rego @@ -0,0 +1,88 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_FromScope_Correct if { + ControlNumber := "EXO 2.7" + Requirement := "External sender warnings SHALL be implemented" + + Output := tests with input as { + "transport_rule": [ + { + "FromScope" : "NotInOrganization" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_FromScope_Incorrect if { + ControlNumber := "EXO 2.7" + Requirement := "External sender warnings SHALL be implemented" + + Output := tests with input as { + "transport_rule": [ + { + "FromScope" : "" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No transport rule found with that applies to emails received from outside the organization" +} + +test_FromScope_Multiple_Correct if { + ControlNumber := "EXO 2.7" + Requirement := "External sender warnings SHALL be implemented" + + Output := tests with input as { + "transport_rule": [ + { + "FromScope" : "" + }, + { + "FromScope" : "NotInOrganization" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_FromScope_Multiple_Incorrect if { + ControlNumber := "EXO 2.7" + Requirement := "External sender warnings SHALL be implemented" + + Output := tests with input as { + "transport_rule": [ + { + "FromScope" : "" + }, + { + "FromScope" : "Hello there" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No transport rule found with that applies to emails received from outside the organization" +} \ No newline at end of file diff --git a/Testing/EXO/EXOConfig2_08_test.rego b/Testing/EXO/EXOConfig2_08_test.rego new file mode 100644 index 0000000000..81423b9d84 --- /dev/null +++ b/Testing/EXO/EXOConfig2_08_test.rego @@ -0,0 +1,35 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_3rdParty_Correct_V1 if { + ControlNumber := "EXO 2.8" + Requirement := "A DLP solution SHALL be used. The selected DLP solution SHOULD offer services comparable to the native DLP solution offered by Microsoft" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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" +} + +# +# Policy 2 +#-- +test_3rdParty_Correct_V2 if { + ControlNumber := "EXO 2.8" + 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" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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" +} \ No newline at end of file diff --git a/Testing/EXO/EXOConfig2_09_test.rego b/Testing/EXO/EXOConfig2_09_test.rego new file mode 100644 index 0000000000..43b6257e9a --- /dev/null +++ b/Testing/EXO/EXOConfig2_09_test.rego @@ -0,0 +1,51 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_3rdParty_Correct_V1 if { + ControlNumber := "EXO 2.9" + 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" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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" +} + +# +# Policy 2 +#-- +test_3rdParty_Correct_V2 if { + ControlNumber := "EXO 2.9" + Requirement := "The attachment filter SHOULD attempt to determine the true file type and assess the file extension" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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" +} + +# +# Policy 3 +#-- +test_3rdParty_Correct_V3 if { + ControlNumber := "EXO 2.9" + 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)" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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" +} \ No newline at end of file diff --git a/Testing/EXO/EXOConfig2_10_test.rego b/Testing/EXO/EXOConfig2_10_test.rego new file mode 100644 index 0000000000..6e42ce7d24 --- /dev/null +++ b/Testing/EXO/EXOConfig2_10_test.rego @@ -0,0 +1,51 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_3rdParty_Correct_V1 if { + ControlNumber := "EXO 2.10" + Requirement := "Emails SHALL be scanned for malware" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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" +} + +# +# Policy 2 +#-- +test_3rdParty_Correct_V2 if { + ControlNumber := "EXO 2.10" + Requirement := "Emails identified as containing malware SHALL be quarantined or dropped" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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" +} + +# +# Policy 3 +#-- +test_3rdParty_Correct_V3 if { + ControlNumber := "EXO 2.10" + Requirement := "Email scanning SHOULD be capable of reviewing emails after delivery" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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" +} \ No newline at end of file diff --git a/Testing/EXO/EXOConfig2_11_test.rego b/Testing/EXO/EXOConfig2_11_test.rego new file mode 100644 index 0000000000..3d1fca13ed --- /dev/null +++ b/Testing/EXO/EXOConfig2_11_test.rego @@ -0,0 +1,51 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_3rdParty_Correct_V1 if { + ControlNumber := "EXO 2.11" + Requirement := "Impersonation protection checks SHOULD be used" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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" +} + +# +# Policy 2 +#-- +test_3rdParty_Correct_V2 if { + ControlNumber := "EXO 2.11" + Requirement := "User warnings, comparable to the user safety tips included with EOP, SHOULD be displayed" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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" +} + +# +# Policy 3 +#-- +test_3rdParty_Correct_V3 if { + ControlNumber := "EXO 2.11" + Requirement := "The phishing protection solution SHOULD include an AI-based phishing detection tool comparable to EOP Mailbox Intelligence" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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" +} \ No newline at end of file diff --git a/Testing/EXO/EXOConfig2_12_test.rego b/Testing/EXO/EXOConfig2_12_test.rego new file mode 100644 index 0000000000..8c388f347a --- /dev/null +++ b/Testing/EXO/EXOConfig2_12_test.rego @@ -0,0 +1,117 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_IPAllowList_Correct_V1 if { + ControlNumber := "EXO 2.12" + Requirement := "IP allow lists SHOULD NOT be created" + + Output := tests with input as { + "conn_filter": { + "IPAllowList" : [], + "EnableSafeList": false + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_IPAllowList_Correct_V2 if { + ControlNumber := "EXO 2.12" + Requirement := "IP allow lists SHOULD NOT be created" + + Output := tests with input as { + "conn_filter": { + "IPAllowList" : [], + "EnableSafeList": true + } # it shouldn't matter that safe list is enabled + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_IPAllowList_Incorrect if { + ControlNumber := "EXO 2.12" + Requirement := "IP allow lists SHOULD NOT be created" + + Output := tests with input as { + "conn_filter": { + "IPAllowList" : ["trust.me.please"], + "EnableSafeList": false + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Allow-list is in use" +} + +# +# Policy 2 +#-- +test_EnableSafeList_Correct_V1 if { + ControlNumber := "EXO 2.12" + Requirement := "Safe lists SHOULD NOT be enabled" + + Output := tests with input as { + "conn_filter": { + "IPAllowList" : [], + "EnableSafeList": false + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_EnableSafeList_Incorrect_V1 if { + ControlNumber := "EXO 2.12" + Requirement := "Safe lists SHOULD NOT be enabled" + + Output := tests with input as { + "conn_filter": { + "IPAllowList" : [], + "EnableSafeList": true + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_EnableSafeList_Correct_V2 if { + ControlNumber := "EXO 2.12" + Requirement := "Safe lists SHOULD NOT be enabled" + + Output := tests with input as { + "conn_filter": { + "IPAllowList" : ["this.shouldnt.matter"], + "EnableSafeList": false + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} \ No newline at end of file diff --git a/Testing/EXO/EXOConfig2_13_test.rego b/Testing/EXO/EXOConfig2_13_test.rego new file mode 100644 index 0000000000..d171513409 --- /dev/null +++ b/Testing/EXO/EXOConfig2_13_test.rego @@ -0,0 +1,42 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_AuditDisabled_Correct if { + ControlNumber := "EXO 2.13" + Requirement := "Mailbox auditing SHALL be enabled" + + Output := tests with input as { + "org_config": { + "AuditDisabled" : false, + "Identity" : "Test name" + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AuditDisabled_Incorrect if { + ControlNumber := "EXO 2.13" + Requirement := "Mailbox auditing SHALL be enabled" + + Output := tests with input as { + "org_config": { + "AuditDisabled" : true, + "Identity" : "Test name" + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} \ No newline at end of file diff --git a/Testing/EXO/EXOConfig2_14_test.rego b/Testing/EXO/EXOConfig2_14_test.rego new file mode 100644 index 0000000000..8cba3792bf --- /dev/null +++ b/Testing/EXO/EXOConfig2_14_test.rego @@ -0,0 +1,51 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_3rdParty_Correct_V1 if { + ControlNumber := "EXO 2.14" + Requirement := "A spam filter SHALL be enabled. The filtering solution selected SHOULD offer services comparable to the native spam filtering offered by Microsoft" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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" +} + +# +# Policy 2 +#-- +test_3rdParty_Correct_V2 if { + ControlNumber := "EXO 2.14" + Requirement := "Spam and high confidence spam SHALL be moved to either the junk email folder or the quarantine folder" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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" +} + +# +# Policy 3 +#-- +test_3rdParty_Correct_V3 if { + ControlNumber := "EXO 2.14" + Requirement := "Allowed senders MAY be added, but allowed domains SHALL NOT be added" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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" +} \ No newline at end of file diff --git a/Testing/EXO/EXOConfig2_15_test.rego b/Testing/EXO/EXOConfig2_15_test.rego new file mode 100644 index 0000000000..24ef2cbab6 --- /dev/null +++ b/Testing/EXO/EXOConfig2_15_test.rego @@ -0,0 +1,51 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_3rdParty_Correct_V1 if { + ControlNumber := "EXO 2.15" + Requirement := "URL comparison with a block-list SHOULD be enabled" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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" +} + +# +# Policy 2 +#-- +test_3rdParty_Correct_V2 if { + ControlNumber := "EXO 2.15" + Requirement := "Direct download links SHOULD be scanned for malware" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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" +} + +# +# Policy 3 +#-- +test_3rdParty_Correct_V3 if { + ControlNumber := "EXO 2.15" + Requirement := "User click tracking SHOULD be enabled" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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" +} \ No newline at end of file diff --git a/Testing/EXO/EXOConfig2_16_test.rego b/Testing/EXO/EXOConfig2_16_test.rego new file mode 100644 index 0000000000..182df67c11 --- /dev/null +++ b/Testing/EXO/EXOConfig2_16_test.rego @@ -0,0 +1,35 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_3rdParty_Correct_V1 if { + ControlNumber := "EXO 2.16" + Requirement := "At a minimum, the following alerts SHALL be enabled...[see Exchange Online secure baseline for list]" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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" +} + +# +# Policy 2 +#-- +test_3rdParty_Correct_V2 if { + ControlNumber := "EXO 2.16" + Requirement := "The alerts SHOULD be sent to a monitored address or incorporated into a SIEM" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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" +} \ No newline at end of file diff --git a/Testing/EXO/EXOConfig2_17_test.rego b/Testing/EXO/EXOConfig2_17_test.rego new file mode 100644 index 0000000000..3a3c88b227 --- /dev/null +++ b/Testing/EXO/EXOConfig2_17_test.rego @@ -0,0 +1,51 @@ +package exo +import future.keywords + + +# +# Policy 1 +#-- +test_3rdParty_Correct_V1 if { + ControlNumber := "EXO 2.17" + Requirement := "Unified audit logging SHALL be enabled" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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" +} + +# +# Policy 2 +#-- +test_3rdParty_Correct_V2 if { + ControlNumber := "EXO 2.17" + Requirement := "Advanced audit SHALL be enabled" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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" +} + +# +# Policy 3 +#-- +test_3rdParty_Correct_V3 if { + ControlNumber := "EXO 2.17" + Requirement := "Audit logs SHALL be maintained for at least the minimum duration dictated by OMB M-21-31" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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" +} \ No newline at end of file diff --git a/Testing/OneDrive/OneDriveConfig2_01_test.rego b/Testing/OneDrive/OneDriveConfig2_01_test.rego new file mode 100644 index 0000000000..d7da79ba20 --- /dev/null +++ b/Testing/OneDrive/OneDriveConfig2_01_test.rego @@ -0,0 +1,40 @@ +package onedrive +import future.keywords + + +# +# Policy 1 +#-- +test_OneDriveLoopSharingCapability_Correct if { + ControlNumber := "OneDrive 2.1" + Requirement := "Anyone links SHOULD be disabled" + + Output := tests with input as { + "SPO_tenant_info": { + "OneDriveLoopSharingCapability" : 1 + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_OneDriveLoopSharingCapability_Incorrect if { + ControlNumber := "OneDrive 2.1" + Requirement := "Anyone links SHOULD be disabled" + + Output := tests with input as { + "SPO_tenant_info": { + "OneDriveLoopSharingCapability" : 2 + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} \ No newline at end of file diff --git a/Testing/OneDrive/OneDriveConfig2_02_test.rego b/Testing/OneDrive/OneDriveConfig2_02_test.rego new file mode 100644 index 0000000000..3050bf875b --- /dev/null +++ b/Testing/OneDrive/OneDriveConfig2_02_test.rego @@ -0,0 +1,95 @@ +package onedrive +import future.keywords + + +# +# Policy 1 +#-- +test_ExternalUserExpirationRequired_Correct if { + ControlNumber := "OneDrive 2.2" + Requirement := "An expiration date SHOULD be set for Anyone links" + + Output := tests with input as { + "SPO_tenant_info": { + "ExternalUserExpirationRequired" : true + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_ExternalUserExpirationRequired_Incorrect if { + ControlNumber := "OneDrive 2.2" + Requirement := "An expiration date SHOULD be set for Anyone links" + + Output := tests with input as { + "SPO_tenant_info": { + "ExternalUserExpirationRequired" : false + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + + +# +# Policy 2 +#-- +test_ExternalUserExpireInDays_Correct if { + ControlNumber := "OneDrive 2.2" + Requirement := "Expiration date SHOULD be set to thirty days" + + Output := tests with input as { + "SPO_tenant_info": { + "ExternalUserExpireInDays" : 30 + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_ExternalUserExpireInDays_Incorrect_V1 if { + ControlNumber := "OneDrive 2.2" + Requirement := "Expiration date SHOULD be set to thirty days" + + Output := tests with input as { + "SPO_tenant_info": { + "ExternalUserExpireInDays" : 31 + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_ExternalUserExpireInDays_Incorrect_V2 if { + ControlNumber := "OneDrive 2.2" + Requirement := "Expiration date SHOULD be set to thirty days" + + Output := tests with input as { + "SPO_tenant_info": { + "ExternalUserExpireInDays" : 29 + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} \ No newline at end of file diff --git a/Testing/OneDrive/OneDriveConfig2_03_test.rego b/Testing/OneDrive/OneDriveConfig2_03_test.rego new file mode 100644 index 0000000000..29a0a92c7f --- /dev/null +++ b/Testing/OneDrive/OneDriveConfig2_03_test.rego @@ -0,0 +1,40 @@ +package onedrive +import future.keywords + + +# +# Policy 1 +#-- +test_DefaultLinkPermission_Correct if { + ControlNumber := "OneDrive 2.3" + Requirement := "Anyone link permissions SHOULD be limited to View" + + Output := tests with input as { + "SPO_tenant_info": { + "DefaultLinkPermission" : 1 + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_DefaultLinkPermission_Incorrect if { + ControlNumber := "OneDrive 2.3" + Requirement := "Anyone link permissions SHOULD be limited to View" + + Output := tests with input as { + "SPO_tenant_info": { + "DefaultLinkPermission" : 2 + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} \ No newline at end of file diff --git a/Testing/OneDrive/OneDriveConfig2_04_test.rego b/Testing/OneDrive/OneDriveConfig2_04_test.rego new file mode 100644 index 0000000000..174f49d6fa --- /dev/null +++ b/Testing/OneDrive/OneDriveConfig2_04_test.rego @@ -0,0 +1,53 @@ +package onedrive +import future.keywords + + +# +# Policy 1 +#-- +test_AllowedDomainList_Correct if { + ControlNumber := "OneDrive 2.4" + Requirement := "OneDrive Client for Windows SHALL be restricted to agency-Defined Domain(s)" + + Output := tests with input as { + "Expected_results": { + "Owner" : "c64580cf-5b99-4c0a-b15b-db035c63e177" + }, + "Tenant_sync_info": { + "AllowedDomainList": [ + "786548dd-877b-4760-a749-6b1efbc1190a", + "877564ff-877b-4760-a749-6b1efbc1190a", + "c64580cf-5b99-4c0a-b15b-db035c63e177" + ] + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowedDomainList_Incorrect if { + ControlNumber := "OneDrive 2.4" + Requirement := "OneDrive Client for Windows SHALL be restricted to agency-Defined Domain(s)" + + Output := tests with input as { + "Expected_results": { + "Owner" : "c64580cf-5b99-4c0a-b15b-db035c63e177" + }, + "Tenant_sync_info": { + "AllowedDomainList": [ + "786548dd-877b-4760-a749-6b1efbc1190a", + "877564ff-877b-4760-a749-6b1efbc1190a" + ] + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} \ No newline at end of file diff --git a/Testing/OneDrive/OneDriveConfig2_05_test.rego b/Testing/OneDrive/OneDriveConfig2_05_test.rego new file mode 100644 index 0000000000..2642671e86 --- /dev/null +++ b/Testing/OneDrive/OneDriveConfig2_05_test.rego @@ -0,0 +1,40 @@ +package onedrive +import future.keywords + + +# +# Policy 1 +#-- +test_BlockMacSync_Correct if { + ControlNumber := "OneDrive 2.5" + Requirement := "OneDrive Client Sync SHALL only be allowed only within the local domain" + + Output := tests with input as { + "Tenant_sync_info": { + "BlockMacSync" : false + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_BlockMacSync_Incorrect if { + ControlNumber := "OneDrive 2.5" + Requirement := "OneDrive Client Sync SHALL only be allowed only within the local domain" + + Output := tests with input as { + "Tenant_sync_info": { + "BlockMacSync" : true + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} \ No newline at end of file diff --git a/Testing/OneDrive/OneDriveConfig2_06_test.rego b/Testing/OneDrive/OneDriveConfig2_06_test.rego new file mode 100644 index 0000000000..aa0a29a35a --- /dev/null +++ b/Testing/OneDrive/OneDriveConfig2_06_test.rego @@ -0,0 +1,19 @@ +package onedrive +import future.keywords + + +# +# Policy 1 +#-- +test_NotImplemented_Correct if { + ControlNumber := "OneDrive 2.6" + Requirement := "OneDrive Client Sync SHALL be restricted to the local domain" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Onedrive Secure Configuration Baseline policy 2.6 for instructions on manual check" +} \ No newline at end of file diff --git a/Testing/OneDrive/OneDriveConfig2_07_test.rego b/Testing/OneDrive/OneDriveConfig2_07_test.rego new file mode 100644 index 0000000000..bd9370cd2a --- /dev/null +++ b/Testing/OneDrive/OneDriveConfig2_07_test.rego @@ -0,0 +1,19 @@ +package onedrive +import future.keywords + + +# +# Policy 1 +#-- +test_NotImplemented_Correct if { + ControlNumber := "OneDrive 2.7" + Requirement := "Legacy Authentication SHALL be blocked" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Onedrive Secure Configuration Baseline policy 2.7 for instructions on manual check" +} \ No newline at end of file diff --git a/Testing/PowerPlatform/PowerPlatformConfig2_01_test.rego b/Testing/PowerPlatform/PowerPlatformConfig2_01_test.rego new file mode 100644 index 0000000000..cfb0e3a70f --- /dev/null +++ b/Testing/PowerPlatform/PowerPlatformConfig2_01_test.rego @@ -0,0 +1,40 @@ +package powerplatform +import future.keywords + + +# +# Policy 1 +#-- +test_disableEnvironmentCreationByNonAdminUsers_Correct if { + ControlNumber := "Power Platform 2.1" + Requirement := "The ability to create additional environments SHALL be restricted to admins" + + Output := tests with input as { + "environment_creation": { + "disableEnvironmentCreationByNonAdminUsers" : true + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_disableEnvironmentCreationByNonAdminUsers_Incorrect if { + ControlNumber := "Power Platform 2.1" + Requirement := "The ability to create additional environments SHALL be restricted to admins" + + Output := tests with input as { + "environment_creation": { + "disableEnvironmentCreationByNonAdminUsers" : false + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} diff --git a/Testing/PowerPlatform/PowerPlatformConfig2_02_test.rego b/Testing/PowerPlatform/PowerPlatformConfig2_02_test.rego new file mode 100644 index 0000000000..a7818e978f --- /dev/null +++ b/Testing/PowerPlatform/PowerPlatformConfig2_02_test.rego @@ -0,0 +1,293 @@ +package powerplatform +import future.keywords + + +# +# Policy 1 +#-- +test_name_Correct if { + ControlNumber := "Power Platform 2.2" + Requirement := "A DLP policy SHALL be created to restrict connector access in the default Power Platform environment" + + Output := tests with input as { + "tenant_id": "Test Id", + "dlp_policies": { + "value": [{ + "displayName": "Block Third-Party Connectors", + "environments": [{ + "name": "Default-Test Id" + }] + }] + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_name_Incorrect if { + ControlNumber := "Power Platform 2.2" + Requirement := "A DLP policy SHALL be created to restrict connector access in the default Power Platform environment" + + Output := tests with input as { + "tenant_id": "Test Id", + "dlp_policies": { + "value": [{ + "displayName": "Block Third-Party Connectors", + "environments": [{ + "name": "NotDefault-Test Id" + }] + }] + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "No policy found that applies to default environment" +} + +# +# Policy 2 +#-- +test_environment_list_Correct if { + ControlNumber := "Power Platform 2.2" + Requirement := "Non-default environments SHOULD have at least one DLP policy that affects them" + + Output := tests with input as { + "dlp_policies": { + "value": [{ + "displayName": "Block Third-Party Connectors", + "environments": [{ + "name": "Default-Test Id" + }] + }] + }, + "environment_list": [{ + "EnvironmentName": "Default-Test Id" + }] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_environment_list_Incorrect if { + ControlNumber := "Power Platform 2.2" + Requirement := "Non-default environments SHOULD have at least one DLP policy that affects them" + + Output := tests with input as { + "dlp_policies": { + "value": [{ + "displayName": "Block Third-Party Connectors", + "environments": [{ + "name": "Default-Test Id" + }] + }] + }, + "environment_list": [{ + "EnvironmentName": "Default-Test Id" + + }, + { + "EnvironmentName": "Test1" + + }] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 Subsequent environments without DLP policies: Test1" +} + +# +# Policy 3 +#-- +test_classification_Correct_V1 if { + ControlNumber := "Power Platform 2.2" + Requirement := "All connectors except those listed...[see Power Platform secure baseline for list]...SHOULD be added to the Blocked category in the default environment policy" + + Output := tests with input as { + "tenant_id": "Test Id", + "dlp_policies": { + "value": [{ + "connectorGroups": [{ + "classification": "Confidential", + "connectors": [{ + "id": "/providers/Microsoft.PowerApps/apis/shared_powervirtualagents" + }] + }], + "environments": [{ + "name": "Default-Test Id" + }] + }] + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_classification_Correct_V2 if { + ControlNumber := "Power Platform 2.2" + Requirement := "All connectors except those listed...[see Power Platform secure baseline for list]...SHOULD be added to the Blocked category in the default environment policy" + + Output := tests with input as { + "tenant_id": "Test Id", + "dlp_policies": { + "value": [{ + "connectorGroups": [{ + "classification": "General", + "connectors": [{ + "id": "/providers/Microsoft.PowerApps/apis/shared_powervirtualagents" + }] + }], + "environments": [{ + "name": "Default-Test Id" + }] + }] + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_connectorGroups_Correct if { + ControlNumber := "Power Platform 2.2" + Requirement := "All connectors except those listed...[see Power Platform secure baseline for list]...SHOULD be added to the Blocked category in the default environment policy" + + Output := tests with input as { + "tenant_id": "Test Id", + "dlp_policies": { + "value": [{ + "connectorGroups": [{ + "classification": "Confidential", + "connectors": [{ + "id": "/providers/Microsoft.PowerApps/apis/shared_powervirtualagents" + }] + }, + { + "classification": "General", + "connectors": [{ + "id": "/providers/Microsoft.PowerApps/apis/shared_powervirtualagents" + }] + }], + "environments": [{ + "name": "Default-Test Id" + }] + }] + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_classification_Incorrect_V1 if { + ControlNumber := "Power Platform 2.2" + Requirement := "All connectors except those listed...[see Power Platform secure baseline for list]...SHOULD be added to the Blocked category in the default environment policy" + + Output := tests with input as { + "tenant_id": "Test Id", + "dlp_policies": { + "value": [{ + "connectorGroups": [{ + "classification": "Confidential", + "connectors": [{ + "id": "HttpWebhook" + }] + }], + "environments": [{ + "name": "Default-Test Id" + }] + }] + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 Connectors are allowed that should be blocked: HttpWebhook" +} + +test_classification_Incorrect_V2 if { + ControlNumber := "Power Platform 2.2" + Requirement := "All connectors except those listed...[see Power Platform secure baseline for list]...SHOULD be added to the Blocked category in the default environment policy" + + Output := tests with input as { + "tenant_id": "Test Id", + "dlp_policies": { + "value": [{ + "connectorGroups": [{ + "classification": "General", + "connectors": [{ + "id": "HttpWebhook" + }] + }], + "environments": [{ + "name": "Default-Test Id" + }] + }] + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 Connectors are allowed that should be blocked: HttpWebhook" +} + +test_connectorGroups_Incorrect if { + ControlNumber := "Power Platform 2.2" + Requirement := "All connectors except those listed...[see Power Platform secure baseline for list]...SHOULD be added to the Blocked category in the default environment policy" + + Output := tests with input as { + "tenant_id": "Test Id", + "dlp_policies": { + "value": [{ + "connectorGroups": [{ + "classification": "Confidential", + "connectors": [{ + "id": "HttpWebhook" + }] + }, + { + "classification": "General", + "connectors": [{ + "id": "HttpWebhook" + }] + }], + "environments": [{ + "name": "Default-Test Id" + }] + }] + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 Connectors are allowed that should be blocked: HttpWebhook" +} \ No newline at end of file diff --git a/Testing/PowerPlatform/PowerPlatformConfig2_03_test.rego b/Testing/PowerPlatform/PowerPlatformConfig2_03_test.rego new file mode 100644 index 0000000000..d57481b9d8 --- /dev/null +++ b/Testing/PowerPlatform/PowerPlatformConfig2_03_test.rego @@ -0,0 +1,60 @@ +package powerplatform +import future.keywords + + +# +# Policy 1 +#-- +test_isDisabled_Correct if { + ControlNumber := "Power Platform 2.3" + Requirement := "Power Platform tenant isolation SHALL be enabled" + + Output := tests with input as { + "tenant_isolation": { + "properties" : { + "isDisabled" : false + } + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_isDisabled_Incorrect if { + ControlNumber := "Power Platform 2.3" + Requirement := "Power Platform tenant isolation SHALL be enabled" + + Output := tests with input as { + "tenant_isolation": { + "properties" : { + "isDisabled" : true + } + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +# +# Policy 2 +#-- +test_NotImplemented_Correct if { + ControlNumber := "Power Platform 2.3" + Requirement := "An inbound/outbound connection allowlist SHOULD be configured" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Power Platform Secure Configuration Baseline policy 2.3 for instructions on manual check" +} \ No newline at end of file diff --git a/Testing/PowerPlatform/PowerPlatformConfig2_04_test.rego b/Testing/PowerPlatform/PowerPlatformConfig2_04_test.rego new file mode 100644 index 0000000000..fd1076375c --- /dev/null +++ b/Testing/PowerPlatform/PowerPlatformConfig2_04_test.rego @@ -0,0 +1,19 @@ +package powerplatform +import future.keywords + + +# +# Policy 1 +#-- +test_NotImplemented_Correct if { + ControlNumber := "Power Platform 2.4" + Requirement := "Content security policies for model-driven Power Apps SHALL be enabled" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Power Platform Secure Configuration Baseline policy 2.4 for instructions on manual check" +} \ No newline at end of file diff --git a/Testing/RunTestCases.ps1 b/Testing/RunTestCases.ps1 new file mode 100644 index 0000000000..83cb60b051 --- /dev/null +++ b/Testing/RunTestCases.ps1 @@ -0,0 +1,227 @@ +[CmdletBinding()] +param ( + [Parameter()] + [ValidateSet('AAD','Defender','EXO','OneDrive','PowerPlatform','Sharepoint','Teams')] + [string[]]$p = "", + [Parameter()] + [string[]]$b = "", + [Parameter()] + [string[]]$t = "", + [Parameter()] + [switch]$h, + [Parameter()] + [switch]$help, + [Parameter()] + [switch]$v +) + +$Tens = @('01','02','03','04','05','06','07','08','09') +$ScriptName = $MyInvocation.MyCommand + +function Show-Menu { + Write-Output "" + Write-Output "`t==================================== Flags ====================================" + Write-Output "`t-h, -help`tshows help menu" + Write-Output "" + Write-Output "`t-p`t`tproduct name, can take a comma-separated list of product names" + Write-Output "" + Write-Output "`t-b`t`tbaseline item number, can take a comma-separated list of item numbers" + Write-Output "" + Write-Output "`t-t`t`ttest name, can take a comma-separated list of test names" + Write-Output "" + Write-Output "`t-v`t`tverbose, verbose opa output" + Write-Output "" + Write-Output "`t==================================== Usage ====================================" + Write-Output "`tRuning all tests is default, no flags are necessary" + Write-Output "`t.\$ScriptName" + Write-Output "" + Write-Output "`tTo run all test cases for specified products, must indicate products with -p" + Write-Output "`t.\$ScriptName [-p] " + Write-Output "" + Write-Output "`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] [-b] " + Write-Output "" + Write-Output "`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] [-b] [-t] " + Write-Output "" + Write-Output "`tVerbose flag can be added to any test at beginning or end of command line" + Write-Output "`t.\$ScriptName [-v]" + Write-Output "" + Write-Output "`t==================================== Examples ====================================" + Write-Output "`t.\$ScriptName -p AAD, Defender, OneDrive" + Write-Output "" + Write-Output "`t.\$ScriptName -p AAD -b 01, 2, 10" + Write-Output "" + Write-Output "`t.\$ScriptName -p AAD -b 01 -t test_IncludeApplications_Incorrect, test_Conditions_Correct" + Write-Output "" + Write-Output "`t.\$ScriptName -p AAD -v" + Write-Output "" + Write-Output "`t.\$ScriptName -v -p AAD -b 01 -t test_IncludeApplications_Incorrect" + Write-Output "" + 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] [-b] [-t] " + } + BaselineItemFlagMissing { + Write-Output "ERROR: Missing value(s) to run opa for specific baseline item(s)" + Write-Output ".\$ScriptName [-p] [-b] " + } + BaselineItemNumber { + Write-Output "ERROR: Unrecognized number '$b'" + Write-Output "Must be an integer (1, 2, 3, ...) or baseline syntax (01, 02, 03..09, 10, ...)" + } + FileIOError { + $Filename = $Flag[1] + Write-Output "ERROR: '$Filename' not found" + } + Default { + Write-Output "ERROR: Unknown" + } + } + $host.ui.RawUI.ForegroundColor = $FontColor + $host.ui.RawUI.BackgroundColor = $BackgroundColor + exit +} + +function Invoke-Product { + [CmdletBinding()] + param ( + [Parameter()] + [string]$Flag, + [Parameter()] + [string[]]$Products + ) + + foreach($Product in $Products) { + Write-Output "...Testing $Product" + ..\opa_windows_amd64.exe test ..\Rego\ .\$Product $Flag + Write-Output "" + } +} + +function Get-Baseline { + [CmdletBinding()] + param ( + [string] $Baseline + ) + if(($Baseline -match "^\d+$") -or ($Baseline -in $Tens)) { + return $true + } + return $false +} + +function Invoke-BaselineItem { + [CmdletBinding()] + param ( + [Parameter()] + [string]$Flag + ) + $Product = $p[0] + foreach($Baseline in $b) { + if(Get-Baseline $Baseline){ + if ([int]$Baseline -lt 10) { + $Baseline = $Tens[[int]$Baseline-1] + } + $FileName = $Product+"\"+$Product+"Config2_"+$Baseline+"_test.rego" + if(Test-Path -Path $FileName -PathType Leaf) { + Write-Output "...Testing $Baseline" + ..\opa_windows_amd64.exe test ..\Rego\ .\$FileName $Flag + Write-Output "" + } + else { + Get-ErrorMsg FileIOError, $FileName + } + } + else { + Get-ErrorMsg BaselineItemNumber + } + } +} + +function Invoke-TestName { + [CmdletBinding()] + param ( + [Parameter()] + [string]$Flag + ) + + $Product = $p[0] + $Baseline = $b[0] + if(Get-Baseline $Baseline){ + if ([int]$Baseline -lt 10) { + $Baseline = $Tens[[int]$Baseline-1] + } + $FileName = $Product+"\"+$Product+"Config2_"+$Baseline+"_test.rego" + if(Test-Path -Path $FileName -PathType Leaf) { + foreach($Test in $t) { + Write-Output "...Testing $Test" + ..\opa_windows_amd64.exe test ..\Rego\ .\$FileName -r $Test $Flag + Write-Output "" + } + } + else { + Get-ErrorMsg FileIOError, $FileName + } + } + else { + Get-ErrorMsg BaselineItemNumber + } +} + +$pEmpty = $p[0] -eq "" +$bEmpty = $b[0] -eq "" +$tEmpty = $t[0] -eq "" +$Flag = "" + +if (($h.IsPresent) -or ($help.IsPresent)) { + Show-Menu +} +if ($v.IsPresent) { + $Flag = "-v" +} +if($pEmpty -and $bEmpty -and $tEmpty) { + Invoke-Product $Flag @('AAD','Defender','EXO','OneDrive','PowerPlatform','Sharepoint','Teams') +} +elseif((-not $pEmpty) -and (-not $bEmpty) -and (-not $tEmpty)) { + if (($p.Count -gt 1) -or ($b.Count -gt 1)) { + $FirstArgP = $p[0] + $FirstArgB = $b[0] + Write-Output "**WARNING** can only take 1 argument for each: product & baseline item" + Write-Output "...Running test for $FirstArgP and $FirstArgB only" + } + Invoke-TestName $Flag +} +elseif((-not $pEmpty) -and (-not $bEmpty) -and $tEmpty) { + if ($p.Count -gt 1) { + $FirstArgP = $p[0] + Write-Output "**WARNING** can only take 1 argument for product" + Write-Output "...Running test for $FirstArgP only" + } + Invoke-BaselineItem $Flag +} +elseif((-not $pEmpty) -and $bEmpty -and $tEmpty) { + Invoke-Product $Flag $p +} +elseif($pEmpty -or $bEmpty -and (-not $tEmpty)) { + Get-ErrorMsg TestNameFlagsMissing +} +elseif($pEmpty -and (-not $bEmpty) -and $tEmpty) { + Get-ErrorMsg BaselineItemFlagMissing +} \ No newline at end of file diff --git a/Testing/Sharepoint/SharepointConfig2_01_test.rego b/Testing/Sharepoint/SharepointConfig2_01_test.rego new file mode 100644 index 0000000000..7c38b6d8e0 --- /dev/null +++ b/Testing/Sharepoint/SharepointConfig2_01_test.rego @@ -0,0 +1,40 @@ +package sharepoint +import future.keywords + + +# +# Policy 1 +#-- +test_DefaultSharingLinkType_Correct if { + ControlNumber := "Sharepoint 2.1" + Requirement := "File and folder links default sharing setting SHALL be set to \"Specific People (Only the People the User Specifies)\"" + + Output := tests with input as { + "SPO_tenant": { + "DefaultSharingLinkType" : 1 + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_DefaultSharingLinkType_Incorrect if { + ControlNumber := "Sharepoint 2.1" + Requirement := "File and folder links default sharing setting SHALL be set to \"Specific People (Only the People the User Specifies)\"" + + Output := tests with input as { + "SPO_tenant": { + "DefaultSharingLinkType" : 2 + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} \ No newline at end of file diff --git a/Testing/Sharepoint/SharepointConfig2_02_test.rego b/Testing/Sharepoint/SharepointConfig2_02_test.rego new file mode 100644 index 0000000000..4a7e0da95b --- /dev/null +++ b/Testing/Sharepoint/SharepointConfig2_02_test.rego @@ -0,0 +1,40 @@ +package sharepoint +import future.keywords + + +# +# Policy 1 +#-- +test_SharingCapability_Correct if { + ControlNumber := "Sharepoint 2.2" + Requirement := "External sharing SHOULD be limited to approved domains and security groups per interagency collaboration needs" + + Output := tests with input as { + "SPO_tenant": { + "SharingCapability" : 1 + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_SharingCapability_Incorrect if { + ControlNumber := "Sharepoint 2.2" + Requirement := "External sharing SHOULD be limited to approved domains and security groups per interagency collaboration needs" + + Output := tests with input as { + "SPO_tenant": { + "SharingCapability" : 2 + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} \ No newline at end of file diff --git a/Testing/Sharepoint/SharepointConfig2_03_test.rego b/Testing/Sharepoint/SharepointConfig2_03_test.rego new file mode 100644 index 0000000000..a4a4fb52a6 --- /dev/null +++ b/Testing/Sharepoint/SharepointConfig2_03_test.rego @@ -0,0 +1,19 @@ +package sharepoint +import future.keywords + + +# +# Policy 1 +#-- +test_NotImplemented_Correct if { + ControlNumber := "Sharepoint 2.3" + Requirement := "Sharing settings for specific SharePoint sites SHOULD align to their sensitivity level" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Currently cannot be checked automatically. See Sharepoint Secure Configuration Baseline policy 2.3 for instructions on manual check" +} \ No newline at end of file diff --git a/Testing/Sharepoint/SharepointConfig2_04_test.rego b/Testing/Sharepoint/SharepointConfig2_04_test.rego new file mode 100644 index 0000000000..d697072fed --- /dev/null +++ b/Testing/Sharepoint/SharepointConfig2_04_test.rego @@ -0,0 +1,94 @@ +package sharepoint +import future.keywords + + +# +# Policy 1 +#-- +test_ExternalUserExpirationRequired_Correct if { + ControlNumber := "Sharepoint 2.4" + Requirement := "Expiration timers for 'guest access to a site or OneDrive' and 'people who use a verification code' SHOULD be set" + + Output := tests with input as { + "SPO_tenant": { + "ExternalUserExpirationRequired" : true + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_ExternalUserExpirationRequired_Incorrect if { + ControlNumber := "Sharepoint 2.4" + Requirement := "Expiration timers for 'guest access to a site or OneDrive' and 'people who use a verification code' SHOULD be set" + + Output := tests with input as { + "SPO_tenant": { + "ExternalUserExpirationRequired" : false + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +# +# Policy 2 +#-- +test_ExternalUserExpireInDays_Correct if { + ControlNumber := "Sharepoint 2.4" + Requirement := "Expiration timers SHOULD be set to 30 days" + + Output := tests with input as { + "SPO_tenant": { + "ExternalUserExpireInDays" : 30 + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_ExternalUserExpireInDays_Incorrect_V1 if { + ControlNumber := "Sharepoint 2.4" + Requirement := "Expiration timers SHOULD be set to 30 days" + + Output := tests with input as { + "SPO_tenant": { + "ExternalUserExpireInDays" : 29 + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_ExternalUserExpireInDays_Incorrect_V2 if { + ControlNumber := "Sharepoint 2.4" + Requirement := "Expiration timers SHOULD be set to 30 days" + + Output := tests with input as { + "SPO_tenant": { + "ExternalUserExpireInDays" : 31 + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} \ No newline at end of file diff --git a/Testing/Sharepoint/SharepointConfig2_05_test.rego b/Testing/Sharepoint/SharepointConfig2_05_test.rego new file mode 100644 index 0000000000..6c52e14383 --- /dev/null +++ b/Testing/Sharepoint/SharepointConfig2_05_test.rego @@ -0,0 +1,40 @@ +package sharepoint +import future.keywords + + +# +# Policy 1 +#-- +test_DenyAddAndCustomizePages_Correct if { + ControlNumber := "Sharepoint 2.5" + Requirement := "Users SHALL be prevented from running custom scripts" + + Output := tests with input as { + "SPO_site": { + "DenyAddAndCustomizePages" : 1 + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_DenyAddAndCustomizePages_Incorrect if { + ControlNumber := "Sharepoint 2.5" + Requirement := "Users SHALL be prevented from running custom scripts" + + Output := tests with input as { + "SPO_site": { + "DenyAddAndCustomizePages" : 2 + } + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} \ No newline at end of file diff --git a/Testing/Teams/TeamsConfig2_01_test.rego b/Testing/Teams/TeamsConfig2_01_test.rego new file mode 100644 index 0000000000..605e7ec827 --- /dev/null +++ b/Testing/Teams/TeamsConfig2_01_test.rego @@ -0,0 +1,112 @@ +package teams +import future.keywords + + +# +# Policy 1 +#-- +test_ExternalParticipantControl_Correct_V1 if { + ControlNumber := "Teams 2.1" + 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" + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowExternalParticipantGiveRequestControl" : false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_ExternalParticipantControl_Correct_V2 if { + ControlNumber := "Teams 2.1" + 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" + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Tag:FirstCustomPolicy", + "AllowExternalParticipantGiveRequestControl" : false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_ExternalParticipantControl_Incorrect_V1 if { + ControlNumber := "Teams 2.1" + 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" + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowExternalParticipantGiveRequestControl" : true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 meeting policy(ies) found that allows external control: Global" +} + +test_ExternalParticipantControl_Incorrect_V2 if { + ControlNumber := "Teams 2.1" + 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" + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Tag:FirstCustomPolicy", + "AllowExternalParticipantGiveRequestControl" : true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 meeting policy(ies) found that allows external control: Tag:FirstCustomPolicy" +} + +test_ExternalParticipantControl_MultiplePolicies if { + ControlNumber := "Teams 2.1" + 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" + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowExternalParticipantGiveRequestControl" : true + }, + { + "Identity": "Tag:FirstCustomPolicy", + "AllowExternalParticipantGiveRequestControl" : false + }, + { + "Identity": "Tag:SecondCustomPolicy", + "AllowExternalParticipantGiveRequestControl" : true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + startswith(RuleOutput[0].ReportDetails, "2 meeting policy(ies) found that allows external control: ") + contains(RuleOutput[0].ReportDetails, "Global") # Not sure if we can assume the order these will appear in, + # hence the "contains" instead of a simple "==" + contains(RuleOutput[0].ReportDetails, "Tag:SecondCustomPolicy") +} diff --git a/Testing/Teams/TeamsConfig2_02_test.rego b/Testing/Teams/TeamsConfig2_02_test.rego new file mode 100644 index 0000000000..39676b227f --- /dev/null +++ b/Testing/Teams/TeamsConfig2_02_test.rego @@ -0,0 +1,117 @@ +package teams +import future.keywords + + +# +# Policy 1 +#-- +test_AnonymousMeetingStart_Correct_V1 if { + ControlNumber := "Teams 2.2" + 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" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowAnonymousUsersToStartMeeting" : false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AnonymousMeetingStart_Correct_V2 if { + ControlNumber := "Teams 2.2" + 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" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Tag:FirstCustomPolicy", + "AllowAnonymousUsersToStartMeeting" : false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AnonymousMeetingStart_Incorrect_V1 if { + ControlNumber := "Teams 2.2" + 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" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowAnonymousUsersToStartMeeting" : true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 meeting policy(ies) found that allows anonymous users to start meetings: Global" +} + +test_AnonymousMeetingStart_Incorrect_V2 if { + ControlNumber := "Teams 2.2" + 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" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Tag:FirstCustomPolicy", + "AllowAnonymousUsersToStartMeeting" : true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 meeting policy(ies) found that allows anonymous users to start meetings: Tag:FirstCustomPolicy" +} + +test_AnonymousMeetingStart_MultiplePolicies if { + ControlNumber := "Teams 2.2" + 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" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowAnonymousUsersToStartMeeting" : true + }, + { + "Identity": "Tag:FirstCustomPolicy", + "AllowAnonymousUsersToStartMeeting" : false + }, + { + "Identity": "Tag:SecondCustomPolicy", + "AllowAnonymousUsersToStartMeeting" : true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + startswith(RuleOutput[0].ReportDetails, "2 meeting policy(ies) found that allows anonymous users to start meetings: ") + contains(RuleOutput[0].ReportDetails, "Global") # Not sure if we can assume the order these will appear in, + # hence the "contains" instead of a simple "==" + contains(RuleOutput[0].ReportDetails, "Tag:SecondCustomPolicy") +} \ No newline at end of file diff --git a/Testing/Teams/TeamsConfig2_03_test.rego b/Testing/Teams/TeamsConfig2_03_test.rego new file mode 100644 index 0000000000..d4c416043f --- /dev/null +++ b/Testing/Teams/TeamsConfig2_03_test.rego @@ -0,0 +1,179 @@ +package teams +import future.keywords + + +# +# Policy 1 +#-- +test_meeting_policies_Correct if { + ControlNumber := "Teams 2.3" + Requirement := "Anonymous users, including dial-in users, SHOULD NOT be admitted automatically" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowPSTNUsersToBypassLobby": false, + "AutoAdmittedUsers": "EveryoneInCompany" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowPSTNUsersToBypassLobby_Incorrect if { + ControlNumber := "Teams 2.3" + Requirement := "Anonymous users, including dial-in users, SHOULD NOT be admitted automatically" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowPSTNUsersToBypassLobby": true, + "AutoAdmittedUsers": "EveryoneInCompany" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met: Dial-in users are enabled to bypass the lobby" +} + +test_AutoAdmittedUsers_Incorrect if { + ControlNumber := "Teams 2.3" + Requirement := "Anonymous users, including dial-in users, SHOULD NOT be admitted automatically" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowPSTNUsersToBypassLobby": true, + "AutoAdmittedUsers": "Everyone" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met: All users are admitted automatically" +} + +# It shouldn't matter that the custom policy is incorrect as this policy only applies to the Global policy +test_Multiple_Correct if { + ControlNumber := "Teams 2.3" + Requirement := "Anonymous users, including dial-in users, SHOULD NOT be admitted automatically" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowPSTNUsersToBypassLobby": false, + "AutoAdmittedUsers": "EveryoneInCompany" + }, + { + "Identity": "Tag:CustomPolicy", + "AllowPSTNUsersToBypassLobby": true, + "AutoAdmittedUsers": "Everyone" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +# +# Policy 2 +#-- +test_AutoAdmittedUsers_Correct_V1 if { + ControlNumber := "Teams 2.3" + Requirement := "Internal users SHOULD be admitted automatically" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AutoAdmittedUsers" : "EveryoneInSameAndFederatedCompany" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AutoAdmittedUsers_Correct_V2 if { + ControlNumber := "Teams 2.3" + Requirement := "Internal users SHOULD be admitted automatically" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AutoAdmittedUsers" : "EveryoneInCompanyExcludingGuests" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AutoAdmittedUsers_Incorrect_V2 if { + ControlNumber := "Teams 2.3" + Requirement := "Internal users SHOULD be admitted automatically" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AutoAdmittedUsers" : "OrganizerOnly" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_AutoAdmittedUsers_Incorrect_V3 if { + ControlNumber := "Teams 2.3" + Requirement := "Internal users SHOULD be admitted automatically" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AutoAdmittedUsers" : "InvitedUsers" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} \ No newline at end of file diff --git a/Testing/Teams/TeamsConfig2_04_test.rego b/Testing/Teams/TeamsConfig2_04_test.rego new file mode 100644 index 0000000000..5c3b20dfe1 --- /dev/null +++ b/Testing/Teams/TeamsConfig2_04_test.rego @@ -0,0 +1,332 @@ +package teams +import future.keywords + + +# +# Policy 1 +#-- +test_AllowFederatedUsers_Correct_V1 if { + ControlNumber := "Teams 2.4" + Requirement := "External access SHALL only be enabled on a per-domain basis" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowFederatedUsers" : false, + "AllowedDomains": [] + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowFederatedUsers_Correct_V2 if { + ControlNumber := "Teams 2.4" + Requirement := "External access SHALL only be enabled on a per-domain basis" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowFederatedUsers" : false, + "AllowedDomains": [ + { + "AllowedDomain": ["Domain=test365.cisa.dhs.gov"] + } + ] + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowedDomains_Correct if { + ControlNumber := "Teams 2.4" + Requirement := "External access SHALL only be enabled on a per-domain basis" + + Output := tests with input as { + "federation_configuration":[ + { + "Identity": "Global", + "AllowFederatedUsers" : true, + "AllowedDomains": [ + { + "AllowedDomain": ["Domain=test365.cisa.dhs.gov"] + } + ] + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowedDomains_Incorrect if { + ControlNumber := "Teams 2.4" + Requirement := "External access SHALL only be enabled on a per-domain basis" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowFederatedUsers" : true, + "AllowedDomains": [] + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 meeting policy(ies) that allow external access across all domains: Global" +} + +test_AllowFederatedUsers_Correct_V1_multi if { + ControlNumber := "Teams 2.4" + Requirement := "External access SHALL only be enabled on a per-domain basis" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowFederatedUsers" : false, + "AllowedDomains": [] + }, + { + "Identity": "Tag:AllOn", + "AllowFederatedUsers" : false, + "AllowedDomains": [] + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowFederatedUsers_Correct_V2_multi if { + ControlNumber := "Teams 2.4" + Requirement := "External access SHALL only be enabled on a per-domain basis" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowFederatedUsers" : false, + "AllowedDomains": [ + { + "AllowedDomain": ["Domain=test365.cisa.dhs.gov"] + } + ] + }, + { + "Identity": "Tag:AllOn", + "AllowFederatedUsers" : false, + "AllowedDomains": [ + { + "AllowedDomain": ["Domain=test365.cisa.dhs.gov"] + } + ] + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + + +test_AllowedDomains_Correct_multi if { + ControlNumber := "Teams 2.4" + Requirement := "External access SHALL only be enabled on a per-domain basis" + + Output := tests with input as { + "federation_configuration":[ + { + "Identity": "Global", + "AllowFederatedUsers" : true, + "AllowedDomains": [ + { + "AllowedDomain": ["Domain=test365.cisa.dhs.gov"] + } + ] + }, + { + "Identity": "Tag:AllOn", + "AllowFederatedUsers" : true, + "AllowedDomains": [ + { + "AllowedDomain": ["Domain=test365.cisa.dhs.gov"] + } + ] + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowedDomains_Incorrect_multi if { + ControlNumber := "Teams 2.4" + Requirement := "External access SHALL only be enabled on a per-domain basis" + + Output := tests with input as { + "federation_configuration":[ + { + "Identity": "Global", + "AllowFederatedUsers" : true, + "AllowedDomains": [] + }, + { + "Identity": "Tag:AllOn", + "AllowFederatedUsers" : true, + "AllowedDomains": [] + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "2 meeting policy(ies) that allow external access across all domains: Global, Tag:AllOn" +} + +# +# Policy 2 +#-- +test_AllowAnonymousUsersToJoinMeeting_Correct_V1 if { + ControlNumber := "Teams 2.4" + Requirement := "Anonymous users SHOULD be enabled to join meetings" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowAnonymousUsersToJoinMeeting" : true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowAnonymousUsersToJoinMeeting_Correct_V2 if { + ControlNumber := "Teams 2.4" + Requirement := "Anonymous users SHOULD be enabled to join meetings" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Tag:FirstCustomPolicy", + "AllowAnonymousUsersToJoinMeeting" : true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowAnonymousUsersToJoinMeeting_Incorrect_V1 if { + ControlNumber := "Teams 2.4" + Requirement := "Anonymous users SHOULD be enabled to join meetings" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowAnonymousUsersToJoinMeeting" : false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 meeting policy(ies) found that don't allow anonymous users to join meetings: Global" +} + +test_AllowAnonymousUsersToJoinMeeting_Incorrect_V2 if { + ControlNumber := "Teams 2.4" + Requirement := "Anonymous users SHOULD be enabled to join meetings" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Tag:FirstCustomPolicy", + "AllowAnonymousUsersToJoinMeeting" : false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 meeting policy(ies) found that don't allow anonymous users to join meetings: Tag:FirstCustomPolicy" +} + +test_AllowAnonymousUsersToJoinMeeting_MultiplePolicies if { + ControlNumber := "Teams 2.4" + Requirement := "Anonymous users SHOULD be enabled to join meetings" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowAnonymousUsersToJoinMeeting" : true + }, + { + "Identity": "Tag:FirstCustomPolicy", + "AllowAnonymousUsersToJoinMeeting" : false + }, + { + "Identity": "Tag:SecondCustomPolicy", + "AllowAnonymousUsersToJoinMeeting" : true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 meeting policy(ies) found that don't allow anonymous users to join meetings: Tag:FirstCustomPolicy" +} + + diff --git a/Testing/Teams/TeamsConfig2_05_test.rego b/Testing/Teams/TeamsConfig2_05_test.rego new file mode 100644 index 0000000000..5f0d3c1d66 --- /dev/null +++ b/Testing/Teams/TeamsConfig2_05_test.rego @@ -0,0 +1,290 @@ +package teams +import future.keywords + + +# +# Policy 1 +#-- +test_AllowTeamsConsumerInbound_Correct_V1 if { + ControlNumber := "Teams 2.5" + Requirement := "Unmanaged users SHALL NOT be enabled to initiate contact with internal users" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowTeamsConsumer" : false, + "AllowTeamsConsumerInbound": false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowTeamsConsumerInbound_Correct_V1_multi if { + ControlNumber := "Teams 2.5" + Requirement := "Unmanaged users SHALL NOT be enabled to initiate contact with internal users" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowTeamsConsumer" : false, + "AllowTeamsConsumerInbound": false + }, + { + "Identity": "Tag:AllOn", + "AllowTeamsConsumer" : false, + "AllowTeamsConsumerInbound": false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowTeamsConsumerInbound_Correct_V2 if { + ControlNumber := "Teams 2.5" + Requirement := "Unmanaged users SHALL NOT be enabled to initiate contact with internal users" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowTeamsConsumer" : false, + "AllowTeamsConsumerInbound": true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowTeamsConsumerInbound_Correct_V2_multi if { + ControlNumber := "Teams 2.5" + Requirement := "Unmanaged users SHALL NOT be enabled to initiate contact with internal users" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowTeamsConsumer" : false, + "AllowTeamsConsumerInbound": true + }, + { + "Identity": "Tag:AllOn", + "AllowTeamsConsumer" : false, + "AllowTeamsConsumerInbound": true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowTeamsConsumer_Incorrect if { + ControlNumber := "Teams 2.5" + Requirement := "Unmanaged users SHALL NOT be enabled to initiate contact with internal users" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowTeamsConsumer" : true, + "AllowTeamsConsumerInbound": true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 Configuration allowed unmanaged users to initiate contact with internal user across domains: Global" +} + +test_AllowTeamsConsumer_Incorrect_multi if { + ControlNumber := "Teams 2.5" + Requirement := "Unmanaged users SHALL NOT be enabled to initiate contact with internal users" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowTeamsConsumer" : true, + "AllowTeamsConsumerInbound": true + }, + { + "Identity": "Tag:AllOn", + "AllowTeamsConsumer" : true, + "AllowTeamsConsumerInbound": true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "2 Configuration allowed unmanaged users to initiate contact with internal user across domains: Global, Tag:AllOn" +} + +test_AllowTeamsConsumer_Incorrect if { + ControlNumber := "Teams 2.5" + Requirement := "Unmanaged users SHALL NOT be enabled to initiate contact with internal users" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowTeamsConsumer" : true, + "AllowTeamsConsumerInbound": false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowTeamsConsumer_Incorrect_multi if { + ControlNumber := "Teams 2.5" + Requirement := "Unmanaged users SHALL NOT be enabled to initiate contact with internal users" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowTeamsConsumer" : true, + "AllowTeamsConsumerInbound": false + }, + { + "Identity": "Tag:AllOn", + "AllowTeamsConsumer" : true, + "AllowTeamsConsumerInbound": false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} +# +# Policy 2 +#-- +test_AllowTeamsConsumer_Correct if { + ControlNumber := "Teams 2.5" + Requirement := "Internal users SHOULD NOT be enabled to initiate contact with unmanaged users" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowTeamsConsumer" : false, + "AllowTeamsConsumerInbound": false # the value here doesn't matter for this control + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowTeamsConsumer_Correct_multi if { + ControlNumber := "Teams 2.5" + Requirement := "Internal users SHOULD NOT be enabled to initiate contact with unmanaged users" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowTeamsConsumer" : false, + "AllowTeamsConsumerInbound": false # the value here doesn't matter for this control + }, + { + "Identity": "Tag:AllOn", + "AllowTeamsConsumer" : false, + "AllowTeamsConsumerInbound": false # the value here doesn't matter for this control + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowTeamsConsumer_Incorrect if { + ControlNumber := "Teams 2.5" + Requirement := "Internal users SHOULD NOT be enabled to initiate contact with unmanaged users" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowTeamsConsumer" : true, + "AllowTeamsConsumerInbound": false # the value here doesn't matter for this control + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 Internal users are enabled to initiate contact with unmanaged users across domains: Global" +} + +test_AllowTeamsConsumer_Incorrect_multi if { + ControlNumber := "Teams 2.5" + Requirement := "Internal users SHOULD NOT be enabled to initiate contact with unmanaged users" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowTeamsConsumer" : true, + "AllowTeamsConsumerInbound": false # the value here doesn't matter for this control + }, + { + "Identity": "Tag:AllOn", + "AllowTeamsConsumer" : true, + "AllowTeamsConsumerInbound": false # the value here doesn't matter for this control + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "2 Internal users are enabled to initiate contact with unmanaged users across domains: Global, Tag:AllOn" +} diff --git a/Testing/Teams/TeamsConfig2_06_test.rego b/Testing/Teams/TeamsConfig2_06_test.rego new file mode 100644 index 0000000000..afc9e5147f --- /dev/null +++ b/Testing/Teams/TeamsConfig2_06_test.rego @@ -0,0 +1,94 @@ +package teams +import future.keywords + + +# +# Policy 1 +#-- +test_AllowPublicUsers_Correct if { + ControlNumber := "Teams 2.6" + Requirement := "Contact with Skype users SHALL be blocked" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowPublicUsers" : false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowPublicUsers_InCorrect if { + ControlNumber := "Teams 2.6" + Requirement := "Contact with Skype users SHALL be blocked" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowPublicUsers" : true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 domains that allows contact with Skype users: Global" +} + +test_AllowPublicUsers_Correct_multi if { + ControlNumber := "Teams 2.6" + Requirement := "Contact with Skype users SHALL be blocked" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowPublicUsers" : false + }, + { + "Identity": "Tag:AllOn", + "AllowPublicUsers" : false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowPublicUsers_InCorrect_multi if { + ControlNumber := "Teams 2.6" + Requirement := "Contact with Skype users SHALL be blocked" + + Output := tests with input as { + "federation_configuration": [ + { + "Identity": "Global", + "AllowPublicUsers" : true + }, + { + "Identity": "Tag:AllOn", + "AllowPublicUsers" : true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "2 domains that allows contact with Skype users: Global, Tag:AllOn" +} diff --git a/Testing/Teams/TeamsConfig2_07_test.rego b/Testing/Teams/TeamsConfig2_07_test.rego new file mode 100644 index 0000000000..6d2951cf2e --- /dev/null +++ b/Testing/Teams/TeamsConfig2_07_test.rego @@ -0,0 +1,222 @@ +package teams +import future.keywords + + +# +# Policy 1 +#-- +test_AllowEmailIntoChannel_Correct_V1 if { + ControlNumber := "Teams 2.7" + Requirement := "Teams email integration SHALL be disabled" + + Output := tests with input as { + "client_configuration": [ + { + "Identity": "Global", + "AllowEmailIntoChannel": false + } + ], + "teams_tenant_info": [ + { + "ServiceInstance": "MicrosoftCommunicationsOnline/NOAM-ED6-A6" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowEmailIntoChannel_Correct_V1_multi if { + ControlNumber := "Teams 2.7" + Requirement := "Teams email integration SHALL be disabled" + + Output := tests with input as { + "client_configuration": [ + { + "Identity": "Global", + "AllowEmailIntoChannel": false + }, + { + "Identity": "Tag:AllOn", + "AllowEmailIntoChannel": false + } + ], + "teams_tenant_info": [ + { + "ServiceInstance": "MicrosoftCommunicationsOnline/NOAM-ED6-A6" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowEmailIntoChannel_Incorrect if { + ControlNumber := "Teams 2.7" + Requirement := "Teams email integration SHALL be disabled" + + Output := tests with input as { + "client_configuration": [ + { + "Identity": "Global", + "AllowEmailIntoChannel": true + } + ], + "teams_tenant_info": [ + { + "ServiceInstance": "MicrosoftCommunicationsOnline/NOAM-ED6-A6" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 Requirement not met: Email integration is enabled across domain: Global" +} + +test_AllowEmailIntoChannel_Incorrect_multi if { + ControlNumber := "Teams 2.7" + Requirement := "Teams email integration SHALL be disabled" + + Output := tests with input as { + "client_configuration": [ + { + "Identity": "Global", + "AllowEmailIntoChannel": true + }, + { + "Identity": "Tag:AllOn", + "AllowEmailIntoChannel": true + } + ], + "teams_tenant_info": [ + { + "ServiceInstance": "MicrosoftCommunicationsOnline/NOAM-ED6-A6" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "2 Requirement not met: Email integration is enabled across domain: Global, Tag:AllOn" +} + +test_AllowEmailIntoChannel_Correct_V2 if { + ControlNumber := "Teams 2.7" + Requirement := "Teams email integration SHALL be disabled" + + Output := tests with input as { + "client_configuration": [ + { + "Identity": "Global", + "AllowEmailIntoChannel": false + } + ], + "teams_tenant_info": [ + { + "ServiceInstance": "MicrosoftCommunicationsOnline/GOV-1B-G6" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "N/A: Feature is unavailable in GCC environments" +} + +test_AllowEmailIntoChannel_Correct_V2_multi if { + ControlNumber := "Teams 2.7" + Requirement := "Teams email integration SHALL be disabled" + + Output := tests with input as { + "client_configuration": [ + { + "Identity": "Global", + "AllowEmailIntoChannel": false + }, + { + "Identity": "Tag:AllOn", + "AllowEmailIntoChannel": false + } + ], + "teams_tenant_info": [ + { + "ServiceInstance": "MicrosoftCommunicationsOnline/GOV-1B-G6" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "N/A: Feature is unavailable in GCC environments" +} + +test_AllowEmailIntoChannel_Correct_V3 if { + ControlNumber := "Teams 2.7" + Requirement := "Teams email integration SHALL be disabled" + + Output := tests with input as { + "client_configuration": [ + { + "Identity": "Global", + "AllowEmailIntoChannel": true + } + ], + "teams_tenant_info": [ + { + "ServiceInstance": "MicrosoftCommunicationsOnline/GOV-1B-G6" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "N/A: Feature is unavailable in GCC environments" +} + +test_AllowEmailIntoChannel_Correct_V3 if { + ControlNumber := "Teams 2.7" + Requirement := "Teams email integration SHALL be disabled" + + Output := tests with input as { + "client_configuration": [ + { + "Identity": "Global", + "AllowEmailIntoChannel": true + }, + { + "Identity": "Tag:AllOn", + "AllowEmailIntoChannel": true + } + ], + "teams_tenant_info": [ + { + "ServiceInstance": "MicrosoftCommunicationsOnline/GOV-1B-G6" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "N/A: Feature is unavailable in GCC environments" +} diff --git a/Testing/Teams/TeamsConfig2_08_test.rego b/Testing/Teams/TeamsConfig2_08_test.rego new file mode 100644 index 0000000000..7c40b2efc0 --- /dev/null +++ b/Testing/Teams/TeamsConfig2_08_test.rego @@ -0,0 +1,355 @@ +package teams +import future.keywords + + +# +# Policy 1 +#-- +test_DefaultCatalogAppsType_Correct_V1 if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHOULD allow all apps published by Microsoft, but MAY block specific Microsoft apps as needed" + + Output := tests with input as { + "app_policies": [ + { + "Identity": "Global", + "DefaultCatalogAppsType": "BlockedAppList" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_DefaultCatalogAppsType_Correct_V2 if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHOULD allow all apps published by Microsoft, but MAY block specific Microsoft apps as needed" + + Output := tests with input as { + "app_policies": [ + { + "Identity": "Tag:TestPolicy", + "DefaultCatalogAppsType": "BlockedAppList" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_DefaultCatalogAppsType_Incorrect_V1 if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHOULD allow all apps published by Microsoft, but MAY block specific Microsoft apps as needed" + + Output := tests with input as { + "app_policies": [ + { + "Identity": "Global", + "DefaultCatalogAppsType": "AllowedAppList" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 meeting policy(ies) found that block Microsoft Apps by default: Global" +} + +test_DefaultCatalogAppsType_Incorrect_V2 if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHOULD allow all apps published by Microsoft, but MAY block specific Microsoft apps as needed" + + Output := tests with input as { + "app_policies": [ + { + "Identity": "Tag:TestPolicy", + "DefaultCatalogAppsType": "AllowedAppList" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 meeting policy(ies) found that block Microsoft Apps by default: Tag:TestPolicy" +} + +test_DefaultCatalogAppsType_Multiple if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHOULD allow all apps published by Microsoft, but MAY block specific Microsoft apps as needed" + + Output := tests with input as { + "app_policies": [ + { + "Identity": "Global", + "DefaultCatalogAppsType": "BlockedAppList" + }, + { + "Identity": "Tag:TestPolicy1", + "DefaultCatalogAppsType": "AllowedAppList" + }, + { + "Identity": "Tag:TestPolicy2", + "DefaultCatalogAppsType": "AllowedAppList" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + startswith(RuleOutput[0].ReportDetails, "2 meeting policy(ies) found that block Microsoft Apps by default: ") + contains(RuleOutput[0].ReportDetails, "Tag:TestPolicy1") + contains(RuleOutput[0].ReportDetails, "Tag:TestPolicy2") +} + +# +# Policy 2 +#-- +test_GlobalCatalogAppsType_Correct_V1 if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHOULD NOT allow installation of all third-party apps, but MAY allow specific apps as needed" + + Output := tests with input as { + "app_policies": [ + { + "Identity": "Global", + "GlobalCatalogAppsType": "AllowedAppList" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_GlobalCatalogAppsType_Correct_V2 if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHOULD NOT allow installation of all third-party apps, but MAY allow specific apps as needed" + + Output := tests with input as { + "app_policies": [ + { + "Identity": "Tag:TestPolicy", + "GlobalCatalogAppsType": "AllowedAppList" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_GlobalCatalogAppsType_Incorrect_V1 if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHOULD NOT allow installation of all third-party apps, but MAY allow specific apps as needed" + + Output := tests with input as { + "app_policies": [ + { + "Identity": "Global", + "GlobalCatalogAppsType": "BlockedAppList" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 meeting policy(ies) found that allow third-party apps by default: Global" +} + +test_GlobalCatalogAppsType_Incorrect_V2 if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHOULD NOT allow installation of all third-party apps, but MAY allow specific apps as needed" + + Output := tests with input as { + "app_policies": [ + { + "Identity": "Tag:TestPolicy", + "GlobalCatalogAppsType": "BlockedAppList" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 meeting policy(ies) found that allow third-party apps by default: Tag:TestPolicy" +} + +test_GlobalCatalogAppsType_Multiple if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHOULD NOT allow installation of all third-party apps, but MAY allow specific apps as needed" + + Output := tests with input as { + "app_policies": [ + { + "Identity": "Global", + "GlobalCatalogAppsType": "BlockedAppList" + }, + { + "Identity": "Tag:TestPolicy1", + "GlobalCatalogAppsType": "AllowedAppList" + }, + { + "Identity": "Tag:TestPolicy2", + "GlobalCatalogAppsType": "BlockedAppList" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + startswith(RuleOutput[0].ReportDetails, "2 meeting policy(ies) found that allow third-party apps by default: ") + contains(RuleOutput[0].ReportDetails, "Global") + contains(RuleOutput[0].ReportDetails, "Tag:TestPolicy2") +} + +test_PrivateCatalogAppsType_Correct_V1 if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHOULD NOT allow installation of all custom apps, but MAY allow specific apps as needed" + + Output := tests with input as { + "app_policies": [ + { + "Identity": "Global", + "PrivateCatalogAppsType": "AllowedAppList" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_PrivateCatalogAppsType_Correct_V2 if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHOULD NOT allow installation of all custom apps, but MAY allow specific apps as needed" + + Output := tests with input as { + "app_policies": [ + { + "Identity": "Tag:TestPolicy", + "PrivateCatalogAppsType": "AllowedAppList" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_PrivateCatalogAppsType_Incorrect_V1 if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHOULD NOT allow installation of all custom apps, but MAY allow specific apps as needed" + + Output := tests with input as { + "app_policies": [ + { + "Identity": "Global", + "PrivateCatalogAppsType": "BlockedAppList" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 meeting policy(ies) found that allow custom apps by default: Global" +} + +test_PrivateCatalogAppsType_Incorrect_V2 if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHOULD NOT allow installation of all custom apps, but MAY allow specific apps as needed" + + Output := tests with input as { + "app_policies": [ + { + "Identity": "Tag:TestPolicy", + "PrivateCatalogAppsType": "BlockedAppList" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 meeting policy(ies) found that allow custom apps by default: Tag:TestPolicy" +} + +test_PrivateCatalogAppsType_Multiple if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHOULD NOT allow installation of all custom apps, but MAY allow specific apps as needed" + + Output := tests with input as { + "app_policies": [ + { + "Identity": "Global", + "PrivateCatalogAppsType": "BlockedAppList" + }, + { + "Identity": "Tag:TestPolicy1", + "PrivateCatalogAppsType": "AllowedAppList" + }, + { + "Identity": "Tag:TestPolicy2", + "PrivateCatalogAppsType": "BlockedAppList" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + startswith(RuleOutput[0].ReportDetails, "2 meeting policy(ies) found that allow custom apps by default: ") + contains(RuleOutput[0].ReportDetails, "Global") + contains(RuleOutput[0].ReportDetails, "Tag:TestPolicy2") +} + +# +# Policy 3 +#-- +test_3rdParty_Correct if { + ControlNumber := "Teams 2.8" + Requirement := "Agencies SHALL establish policy dictating the app review and approval process to be used by the agency" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Cannot be checked automatically. See Microsoft Teams Secure Configuration Baseline policy 2.8 for instructions on manual check" +} \ No newline at end of file diff --git a/Testing/Teams/TeamsConfig2_09_test.rego b/Testing/Teams/TeamsConfig2_09_test.rego new file mode 100644 index 0000000000..b36b9d2cae --- /dev/null +++ b/Testing/Teams/TeamsConfig2_09_test.rego @@ -0,0 +1,165 @@ +package teams +import future.keywords + + +# +# Policy 1 +#-- +test_AllowCloudRecording_Correct if { + ControlNumber := "Teams 2.9" + Requirement := "Cloud video recording SHOULD be disabled in the global (org-wide default) meeting policy" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowCloudRecording": false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowCloudRecording_Incorrect if { + ControlNumber := "Teams 2.9" + Requirement := "Cloud video recording SHOULD be disabled in the global (org-wide default) meeting policy" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowCloudRecording": true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + +test_AllowCloudRecording_Multiple if { + ControlNumber := "Teams 2.9" + Requirement := "Cloud video recording SHOULD be disabled in the global (org-wide default) meeting policy" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowCloudRecording": false + }, + { + "Identity": "Tag:TestPolicy", + "AllowCloudRecording": true # This baseline only applies to the Global policy, + # so no failure will be produced for the non-global policies + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +# +# Policy 2 +#-- +test_AllowCloudRecording_Correct_V1 if { + ControlNumber := "Teams 2.9" + Requirement := "For all meeting polices that allow cloud recording, recordings SHOULD be stored inside the country of that agency’s tenant" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowCloudRecording": false, + "AllowRecordingStorageOutsideRegion": false + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowCloudRecording_Correct_V2 if { + ControlNumber := "Teams 2.9" + Requirement := "For all meeting polices that allow cloud recording, recordings SHOULD be stored inside the country of that agency’s tenant" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowCloudRecording": false, + "AllowRecordingStorageOutsideRegion": true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_AllowCloudRecording_Incorrect if { + ControlNumber := "Teams 2.9" + Requirement := "For all meeting polices that allow cloud recording, recordings SHOULD be stored inside the country of that agency’s tenant" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowCloudRecording": true, + "AllowRecordingStorageOutsideRegion": true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "1 meeting policy(ies) found that allow cloud recording and storage outside of the tenant's region: Global" +} + +test_AllowCloudRecording_Multiple if { + ControlNumber := "Teams 2.9" + Requirement := "For all meeting polices that allow cloud recording, recordings SHOULD be stored inside the country of that agency’s tenant" + + Output := tests with input as { + "meeting_policies": [ + { + "Identity": "Global", + "AllowCloudRecording": true, + "AllowRecordingStorageOutsideRegion": true + }, + { + "Identity": "Tag:custom", + "AllowCloudRecording": true, + "AllowRecordingStorageOutsideRegion": true + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + startswith(RuleOutput[0].ReportDetails, "2 meeting policy(ies) found that allow cloud recording and storage outside of the tenant's region: ") + contains(RuleOutput[0].ReportDetails, "Global") + contains(RuleOutput[0].ReportDetails, "Tag:custom") +} \ No newline at end of file diff --git a/Testing/Teams/TeamsConfig2_10_test.rego b/Testing/Teams/TeamsConfig2_10_test.rego new file mode 100644 index 0000000000..6b94ff69bf --- /dev/null +++ b/Testing/Teams/TeamsConfig2_10_test.rego @@ -0,0 +1,71 @@ +package teams +import future.keywords + + +# +# Policy 1 +#-- +test_BroadcastRecordingMode_Correct if { + ControlNumber := "Teams 2.10" + Requirement := "Record an event SHOULD be set to Organizer can record" + + Output := tests with input as { + "broadcast_policies": [ + { + "Identity": "Global", + "BroadcastRecordingMode": "UserOverride" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} + +test_BroadcastRecordingMode_Incorrect if { + ControlNumber := "Teams 2.10" + Requirement := "Record an event SHOULD be set to Organizer can record" + + Output := tests with input as { + "broadcast_policies": [ + { + "Identity": "Global", + "BroadcastRecordingMode": "AlwaysRecord" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + not RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement not met" +} + + +test_BroadcastRecordingMode_Multiple if { + ControlNumber := "Teams 2.10" + Requirement := "Record an event SHOULD be set to Organizer can record" + + Output := tests with input as { + "broadcast_policies": [ + { + "Identity": "Global", + "BroadcastRecordingMode": "UserOverride" + }, + { + "Identity": "Tag:TestPolicy", # Should be ignored + "BroadcastRecordingMode": "AlwaysRecord" + } + ] + } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + count(RuleOutput) == 1 + RuleOutput[0].RequirementMet + RuleOutput[0].ReportDetails == "Requirement met" +} \ No newline at end of file diff --git a/Testing/Teams/TeamsConfig2_11_test.rego b/Testing/Teams/TeamsConfig2_11_test.rego new file mode 100644 index 0000000000..16ba4a1f9f --- /dev/null +++ b/Testing/Teams/TeamsConfig2_11_test.rego @@ -0,0 +1,51 @@ +package teams +import future.keywords + + +# +# Policy 1 +#-- +test_3rdParty_Correct_V1 if { + ControlNumber := "Teams 2.11" + Requirement := "A DLP solution SHALL be enabled" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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" +} + +# +# Policy 2 +#-- +test_3rdParty_Correct_V2 if { + ControlNumber := "Teams 2.11" + Requirement := "Agencies SHOULD use either the native DLP solution offered by Microsoft or a DLP solution that offers comparable services" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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" +} + +# +# Policy 3 +#-- +test_3rdParty_Correct_V3 if { + ControlNumber := "Teams 2.11" + 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" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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" +} \ No newline at end of file diff --git a/Testing/Teams/TeamsConfig2_12_test.rego b/Testing/Teams/TeamsConfig2_12_test.rego new file mode 100644 index 0000000000..3a9262c9af --- /dev/null +++ b/Testing/Teams/TeamsConfig2_12_test.rego @@ -0,0 +1,35 @@ +package teams +import future.keywords + + +# +# Policy 1 +#-- +test_3rdParty_Correct_V1 if { + ControlNumber := "Teams 2.12" + Requirement := "Attachments included with Teams messages SHOULD be scanned for malware" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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" +} + +# +# Policy 2 +#-- +test_3rdParty_Correct_V2 if { + ControlNumber := "Teams 2.12" + Requirement := "Users SHOULD be prevented from opening or downloading files detected as malware" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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" +} \ No newline at end of file diff --git a/Testing/Teams/TeamsConfig2_13_test.rego b/Testing/Teams/TeamsConfig2_13_test.rego new file mode 100644 index 0000000000..b1da9e5446 --- /dev/null +++ b/Testing/Teams/TeamsConfig2_13_test.rego @@ -0,0 +1,51 @@ +package teams +import future.keywords + + +# +# Policy 1 +#-- +test_3rdParty_Correct_V1 if { + ControlNumber := "Teams 2.13" + Requirement := "URL comparison with a block-list SHOULD be enabled" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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" +} + +# +# Policy 2 +#-- +test_3rdParty_Correct_V2 if { + ControlNumber := "Teams 2.13" + Requirement := "Direct download links SHOULD be scanned for malware" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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" +} + +# +# Policy 3 +#-- +test_3rdParty_Correct_V3 if { + ControlNumber := "Teams 2.13" + Requirement := "User click tracking SHOULD be enabled" + + Output := tests with input as { } + + RuleOutput := [Result | Result = Output[_]; Result.Control == ControlNumber; Result.Requirement == Requirement] + + 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" +} \ No newline at end of file diff --git a/baselines/aad.md b/baselines/aad.md new file mode 100644 index 0000000000..d5c65398e0 --- /dev/null +++ b/baselines/aad.md @@ -0,0 +1,1375 @@ +# 1. Introduction + +## 1.1 Key Terminology + +The following are key terms and descriptions used in this document. + +**Hybrid Azure Active Directory (AD)** – This term denotes the scenario +when an organization has an on-premises AD domain that contains the +master user directory but federates access to the cloud Microsoft 365 +(M365) Azure AD tenant. + +**Resource Tenant** – In scenarios where external users are involved +(e.g., guest users), the [resource tenant](https://docs.microsoft.com/en-us/azure/active-directory/external-identities/authentication-conditional-access) +hosts the M365 resources being used. + +**Home Tenant** – In scenarios where external users are involved, the +[home tenant](https://docs.microsoft.com/en-us/azure/active-directory/external-identities/authentication-conditional-access) +is the one that owns the external user’s (e.g., guest) account. + +## 1.2 Assumptions + +The agency has created emergency access accounts in Azure AD and +implemented strong security measures to protect the credentials of those +accounts. Once created, those accounts should be placed into a group +named “Emergency Users” or a similar name. Throughout Microsoft’s +instructions, this entity is referred to as “emergency access or +break-glass accounts.” Use the following Microsoft guidance to create +and manage emergency access accounts. + +[Manage emergency access accounts in Azure AD](https://docs.microsoft.com/en-us/azure/active-directory/roles/security-emergency-access) + +The **License Requirements** sections of this document assume the +organization is using an [M365 E3](https://www.microsoft.com/en-us/microsoft-365/compare-microsoft-365-enterprise-plans) +or [G3](https://www.microsoft.com/en-us/microsoft-365/government) +license level. Therefore, only licenses not included in E3/G3 are +listed. + +## 1.3 Common guidance + +### 1.3.1 Conditional Access Policies + +This section provides common guidance that should be applied when +implementing baseline instructions related to Azure AD Conditional +Access policies. + +As described in Microsoft’s instructions and examples related to +conditional access policies, CISA recommends setting a policy to +**Report-only** when it is created and then performing thorough hands-on +testing to ensure that there are no unintended consequences before +toggling the policy from **Report-only** to **On**. One tool that can +assist with running test simulations is the [What If tool](https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/what-if-tool). +Microsoft also describes [Conditional Access insights and reporting features](https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/howto-conditional-access-insights-reporting) +that can assist with testing. + +### 1.3.2 Azure AD Privileged Identity Management + +Some of the guidance in this baseline document leverages specific +features of the Azure AD Privileged Identity Management (PIM) service to +demonstrate how to improve the security of highly privileged Azure AD +roles. The PIM service provides what is referred to as “Privileged +Access Management (PAM)” capabilities in industry. As an alternative to +Azure AD PIM, there are third-party vendors that provide products or +services with privileged access management capabilities that can be +leveraged if an agency chooses to do so. + +## 1.4 Resources + +License Compliance and Copyright + +Portions of this document are adapted from documents in Microsoft’s +[Microsoft 365](https://github.com/MicrosoftDocs/microsoft-365-docs/blob/public/LICENSE) +and +[Azure](https://github.com/MicrosoftDocs/azure-docs/blob/main/LICENSE) +GitHub repositories. The respective documents are subject to copyright +and are adapted under the terms of the Creative Commons Attribution 4.0 +International license. Source documents are linked throughout this +document. The United States government has adapted selections of these +documents to develop innovative and scalable configuration standards to +strengthen the security of widely used cloud-based software services. + +# 2. Baseline + +## 2.1 Legacy Authentication SHALL Be Blocked + +Block legacy authentication protocols using a conditional access policy. +Legacy authentication does not support multifactor authentication (MFA), +which is required to minimize the impact of user credential theft. + +### 2.1.1 Policy + +- Legacy authentication SHALL be blocked. + +### 2.1.2 Resources + +- [Conditional Access: Block Legacy Authentication](https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/howto-conditional-access-policy-block-legacy) + +- [Five steps to securing your identity infrastructure](https://docs.microsoft.com/en-us/azure/security/fundamentals/steps-secure-identity) + +### 2.1.3 License Requirements + +- N/A + +### 2.1.4 Implementation + +1. Before blocking legacy authentication across the entire application + base, follow [these instructions](https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/block-legacy-authentication#identify-legacy-authentication-use) + to determine if any of the agency’s existing applications are + presently using legacy authentication. This helps develop a plan to + address policy impacts. + +2. Follow [the instructions on this page](https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/howto-conditional-access-policy-block-legacy) + to block legacy authentication. **Note:** The instructions suggest + using Report-only mode which will not block legacy authentication. + +## 2.2 High Risk Users SHALL Be Blocked + +Azure AD Identity Protection uses various signals to detect the risk +level for each user and determine if an account has likely been +compromised. Users who are determined to be high risk are to be blocked +from accessing the system via Conditional Access until an administrator +remediates their account. Once a respective conditional access policy +with a block is implemented, if a high-risk user attempts to login, the +user will receive an error message with instructions to contact the +administrator to re-enable their access. + +### 2.2.1 Policy + +- Users detected as high risk SHALL be blocked. + +- A notification SHOULD be sent to the administrator when high-risk + users are detected. + +### 2.2.2 Resources + +- [Conditional Access: User risk-based Conditional Access](https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/howto-conditional-access-policy-risk-user) + +- [User-linked detections](https://docs.microsoft.com/en-us/azure/active-directory/identity-protection/concept-identity-protection-risks#user-linked-detections) + +- [Simulating risk detections in Identity Protection](https://docs.microsoft.com/en-us/azure/active-directory/identity-protection/howto-identity-protection-simulate-risk) + +- [User experiences with Azure AD Identity Protection](https://docs.microsoft.com/en-us/azure/active-directory/identity-protection/concept-identity-protection-user-experience) + (Examples of how these policies are applied in practice) + +- [Five steps to securing your identity infrastructure](https://docs.microsoft.com/en-us/azure/security/fundamentals/steps-secure-identity) + +### 2.2.3 License Requirements + +- Requires an AAD P2 license + +### 2.2.4 Implementation + +**Policy \#1:** + +1. To create the conditional access policy that implements the block + for users at the risk level of High, follow the instructions in the + [Enable with Conditional Access policy](https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/howto-conditional-access-policy-risk-user#enable-with-conditional-access-policy) + section, but set the policy to block access as follows: + +2. Under **Access Controls** -\> **Grant**, select **Block access**. + +**Policy \#2**: + +1. Follow the instructions in the [Configure users at risk detected alerts](https://docs.microsoft.com/en-us/azure/active-directory/identity-protection/howto-identity-protection-configure-notifications#configure-users-at-risk-detected-alerts) + section to configure Azure AD Identity Protection to email the + security operations team/administrator when a user account is + determined to be high risk so that they can review and respond to + threats. + +## 2.3 High Risk Sign-ins SHALL Be Blocked + +Azure AD Identity Protection uses various signals to detect the risk +level for each user sign-in. Sign-ins detected as high risk are to be +blocked via Conditional Access. + +### 2.3.1 Policy + +Sign-ins detected as high risk SHALL be blocked. + +### 2.3.2 Resources + +- [Conditional Access: Sign-in risk-based Conditional + Access](https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/howto-conditional-access-policy-risk) + +- [Sign-in + risk](https://docs.microsoft.com/en-us/azure/active-directory/identity-protection/concept-identity-protection-risks#sign-in-risk) + +- [Simulating risk detections in Identity + Protection](https://docs.microsoft.com/en-us/azure/active-directory/identity-protection/howto-identity-protection-simulate-risk) + +- [User experiences with Azure AD Identity + Protection](https://docs.microsoft.com/en-us/azure/active-directory/identity-protection/concept-identity-protection-user-experience) + (Examples of how these policies are applied in practice) + +### 2.3.3 License Requirements + +- Requires an AAD P2 license + +### 2.3.4 Implementation + +To create the conditional access policy that implements the block for +sign-ins at the risk level of **High**, follow the instructions in the +[Enable with Conditional Access +policy](https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/howto-conditional-access-policy-risk#enable-with-conditional-access-policy) +section, but set the risk level to **High** and block access. + +1. Under **Select the sign-in risk level this policy will apply to**, + select **High.** + +2. Under **Access Controls** -\> **Grant**, select **Block access.** + +**Note**: If after implementing this, it is observed that numerous +legitimate user sign-ins are consistently being blocked due to their +location being interpreted as suspicious and this creates an operational +burden on the agency, then [a Trusted Location can be +configured](https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/location-condition#ip-address-ranges) +in the Conditional Access blade for each of the legitimate sign-in +locations. Azure AD Identity Protection considers the Trusted Location +data when it calculates sign-in risk, and this may help to prevent users +signing in from legitimate locations from being flagged as high risk. + +## 2.4 Phishing-Resistant Multifactor Authentication SHALL Be Required for All Users + +Phishing-resistant multifactor authentication protects against +sophisticated phishing attacks. Recognizing the significant risk these +attack present, the Office of Management and Budget (OMB), requires +federal agencies to [implement phishing-resistant +authentication](https://www.whitehouse.gov/wp-content/uploads/2022/01/M-22-09.pdf). + +However, phishing-resistant MFA may not always be immediately available, +especially on mobile devices. Where phishing-resistant MFA is not yet +available, organization should adopt an MFA method from the list below. +Organizations must upgrade to a phishing-resistant MFA method as soon as +possible to become compliant with this policy and address the critical +security threat posed by modern phishing attacks. + +**Note**: Figure adapted from [MS Build +Page](https://docs.microsoft.com/en-us/azure/active-directory/authentication/concept-authentication-methods) +article (12/29/2021). + + + +Figure 1: Options for Weak MFA, Stronger MFA Options, and Strongest MFA + +### 2.4.1 Policy + +- MFA SHALL be required for all users. + +- Phishing-resistant MFA SHALL be used for all users. + + - Phishing-resistant methods: + + - Federal PIV card (Azure AD Certificate-Based authentication + \[CBA\]) + + - FIDO2 Security Key + + - Windows Hello for Business + + - Federal Personal Identity Verification (PIV) card (Federated from + agency Active Directory or other identity provider) + +- If phishing-resistant MFA cannot be used, an MFA method from the list + below SHALL be used in the interim: + + - Microsoft Authenticator (Push Notifications) + + - Microsoft Authenticator (Phone Sign-in) (Also referred to as + Passwordless Sign-in) + + - When using Microsoft Authenticator: + + - Number Matching SHALL be enabled. + + - Additional Context SHALL be enabled. + + - Software Tokens One-Time Password (OTP) – This option is commonly implemented using mobile phone authenticator apps + + - Hardware tokens OTP + +- SMS or Voice as the MFA method SHALL NOT be used. + +### 2.4.2 Resources + +- [What authentication and verification methods are available in Azure + Active + Directory?](https://docs.microsoft.com/en-us/azure/active-directory/authentication/concept-authentication-methods) + +- [Use number matching in multifactor authentication (MFA) notifications + (Preview) - Azure Active Directory - Microsoft Entra \| Microsoft + Docs](https://docs.microsoft.com/en-us/azure/active-directory/authentication/how-to-mfa-number-match#enable-number-matching-in-the-portal) + +- [Use additional context in Microsoft Authenticator notifications + (Preview) - Azure Active Directory - Microsoft Entra \| Microsoft + Docs](https://docs.microsoft.com/en-us/azure/active-directory/authentication/how-to-mfa-additional-context#enable-additional-context-in-the-portal) + +- [M-22-09 Federal Zero Trust + Strategy](https://www.whitehouse.gov/wp-content/uploads/2022/01/M-22-09.pdf) + +### 2.4.3 License Requirements + +- N/A + +### 2.4.4 Implementation + +**Policy \#1:** + +1. Follow [these + instructions](https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/howto-conditional-access-policy-all-users-mfa) + to create a conditional access policy that requires all users to + authenticate with MFA. + +**Policy \#2:** + +Use the following instructions to configure a phishing-resistant MFA +method for users with highly privileged roles. If the agency is +configuring a phishing-resistant MFA method for all users, then the +instructions in this section also apply, but set the **Target** to **All +Users** instead of a specific group in the respective configuration +screens. + +CISA recommends placing highly privileged users into an Azure AD group +named “Highly Privileged Admins” or an equivalent and then referencing +the group in the MFA configuration. Newly created, highly privileged +users should be added to the group so they can register a +phishing-resistant method. CISA does not recommend assigning MFA methods +directly to individual users. + +Select one of the following phishing-resistant MFA methods to configure: + +**FIDO2 Security Key** + +1. Follow the instructions at [this + link](https://docs.microsoft.com/en-us/azure/active-directory/authentication/howto-authentication-passwordless-security-key#enable-fido2-security-key-method) + to configure FIDO2. +2. For **Enable**, select **Yes.** +3. For **Target**, select the **Highly Privileged Admins** group or an + equivalent. + +**Certificate Based Authentication (CBA)** + + +1. Follow the instructions at [this + link](https://docs.microsoft.com/en-us/azure/active-directory/authentication/how-to-certificate-based-authentication#steps-to-configure-and-test-azure-ad-cba) + to configure CBA. +2. On the tenant, in the instructions section named [**Enable CBA on + the + tenant**](https://docs.microsoft.com/en-us/azure/active-directory/authentication/how-to-certificate-based-authentication#step-4-enable-cba-on-the-tenant) + , under **Target**, select the **Highly Privileged Admins** group or + an equivalent. + +**Windows Hello for Business** + +1. Follow the instructions at [this + link](https://docs.microsoft.com/en-us/windows/security/identity-protection/hello-for-business/hello-deployment-guide) + to configure Windows Hello for Business. + +**Policy \#3:** + +If the agency is implementing a phishing-resistant MFA method for all +users, follow the instructions in the previous section. Otherwise use +the following instructions to configure a non-phishing resistant MFA +method for users that are not in highly privileged roles. + +**Microsoft Authenticator (Phone Sign-in) (Also referred to as +Passwordless Sign-in) or Microsoft Authenticator (Push Notifications)** + +1. In the Azure Portal navigate to **Azure Active Directory.** +2. Select **Security.** +3. Select **Manage** -\> **MFA.** +4. Under **Configure,** select **Additional cloud-based MFA settings.** +5. Under **verification options**, select **Notification through mobile + app.** +6. If desired, to enforce Microsoft Authenticator app usage and disable + third party authenticator apps usage, make sure that **Verification + code from mobile app** or **hardware token** is not selected. +7. Click **Save.** +8. Go back to the **Azure Active Directory** home tab and select + **Security**. +9. Select **Authentication Methods**. +10. In the **Policies** window, select **Microsoft Authenticator**. +11. For **Enable**, select **Yes**. +12. For **Target**, select **All users**. +13. In the row for the **All users**, click the … -\> **Configure**. +14. If configuring Phone Sign-in (aka Passwordless Sign-in), for + **Authentication mode**, select **Passwordless**. If configuring + Push Notifications, for **Authentication mode**, select **Push**. If + configuring the usage of both, for **Authentication mode**, select + **Any**. +15. For **Require number matching**, select **Enabled**. +16. For **Show additional context in notifications**, select + **Enabled**. +17. Select **Done**. +18. Click **Save**. + +**Software Tokens OTP or Hardware Tokens OTP** + +1. In the **Azure Portal**, navigate to **Azure Active Directory**. +2. Select **Security**. +3. Select **Manage** -\> **MFA**. +4. Under **Configure**, select **Additional cloud-based MFA settings**. +5. Under **verification options**, select **Verification code from + mobile app** or **hardware token**. +6. If configuring Hardware Tokens OTP, follow the additional steps at + [this link](https://docs.microsoft.com/en-us/azure/active-directory/authentication/concept-authentication-oath-tokens#oath-hardware-tokens-preview) + when provisioning a user. + +**Policy \#4:** + +1. In the **Azure Portal,** navigate to **Azure Active Directory**. +2. Select **Security**. +3. Select **Manage** -\> **MFA**. +4. Under **Configure**, select **Additional cloud-based MFA settings**. +5. Under **verification options**, make sure that **Text message to + phone** and **Call to phone** are **disabled**. + +## 2.5 Azure AD logs SHALL Be Collected + +Configure Azure AD to send critical logs to the agency’s centralized +SIEM and to CISA’s central analysis system so that they can be audited +and queried. Configure Azure AD to send logs to a storage account and +retain them for when incident response is needed. + +### 2.5.1 Policy + +- The following critical logs SHALL be sent at a minimum: AuditLogs, + SignInLogs, RiskyUsers, UserRiskEvents, NonInteractiveUserSignInLogs, + ServicePrincipalSignInLogs, ADFSSignInLogs, RiskyServicePrincipals, + ServicePrincipalRiskEvents. + + + +- If managed identities are used for Azure resources, also include the + ManagedIdentitySignInLogs log type. + +- If the Azure AD Provisioning Service is used to provision users to + SaaS apps or other systems, also include the ProvisioningLogs log + type. + + + +- The logs SHALL be sent to the agency’s SOC for monitoring. + +### 2.5.2 Resources + +- [Everything you wanted to know about Security and Audit Logging in + Office + 365](https://thecloudtechnologist.com/2021/10/15/everything-you-wanted-to-know-about-security-and-audit-logging-in-office-365/) + +- [Sign-in logs in Azure Active Directory - + preview](https://docs.microsoft.com/en-us/azure/active-directory/reports-monitoring/concept-all-sign-ins) + +- [National Cybersecurity Protection System-Cloud Interface Reference + Architecture Volume + 1](https://www.cisa.gov/sites/default/files/publications/NCPS%20Cloud%20Interface%20RA%20Volume%20One%20%282021-05-14%29.pdf) + +- [National Cybersecurity Protection System - Cloud Interface Reference + Architecture Volume + 2](https://www.cisa.gov/sites/default/files/publications/NCPS%20Cloud%20Interface%20RA%20Volume%20Two%202021-06-11%20%28508%20COMPLIANT%29.pdf) + +### 2.5.3 License Requirements + +- N/A + +### 2.5.4 Implementation + +[Follow these instructions](https://docs.microsoft.com/en-us/azure/active-directory/reports-monitoring/quickstart-azure-monitor-route-logs-to-storage-account) +to configure sending the logs to a storage account: + +1. From the **Diagnostic settings** page, click **Add diagnostic** + setting. + +2. Select the specific logs mentioned in the previous policy section. + +3. Under **Destination Details,** select the **Archive to a storage + account** check box and select the storage account that was + specifically created to host security logs. + +4. In the **Retention** field enter “365” days. + +## 2.6 Only Administrators SHALL Be Allowed to Register Third-Party Applications + +Ensure that only administrators can register third-party applications +that can access the tenant. + +### 2.6.1 Policy + +- Only administrators SHALL be allowed to register third-party + applications. + +### 2.6.2 Resources + +- [Restrict Application Registration for Non-Privileged + Users](https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActiveDirectory/users-can-register-applications.html) + +### 2.6.3 License Requirements + +- N/A + +### 2.6.4 Implementation + +1. In the **Azure Portal**, navigate to **Azure Active Directory.** + + + +2. Under **Manage**, select **Users**. + +3. Select **User settings**. + +4. Under **App Registrations** -\> **Users can register applications**, + select **No.** + +5. Click **Save**. + +## 2.7 Non-admin Users SHALL Be Prevented from Providing Consent to Third-Party Applications + +Ensure that only administrators can consent to third-party applications +and only administrators can control which permissions are granted. An +admin consent workflow can be configured in Azure AD, otherwise users +will be blocked when they try to access an application that requires +permissions to access organizational data. Develop a process for +approving and managing third-party applications. + +### 2.7.1 Policy + +- Only administrators SHALL be allowed to consent to third-party + applications. + +- An admin consent workflow SHALL be configured. + +- Group owners SHALL NOT be allowed to consent to third-party + applications. + +### 2.7.2 Resources + +- [Enforce Administrators to Provide Consent for Apps Before + Use](https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActiveDirectory/users-can-consent-to-apps-accessing-company-data-on-their-behalf.html) + +- [Configure the admin consent + workflow](https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/configure-admin-consent-workflow) + +### 2.7.3 License Requirements + +- N/A + +### 2.7.4 Implementation + +1. In the **Azure Portal**, navigate to **Azure Active Directory.** + + + +2. Create a new Azure AD Group that contains admin users responsible + for reviewing and adjudicating app requests. + +3. Under **Manage**, select **Enterprise Applications.** + +4. Under **Security**, select **Consent and permissions.** + +5. Under **User consent for applications**, select **Do not allow user + consent.** + +6. Under **Group owner consent for apps accessing data**, select **Do + not allow group owner consent.** + +7. In the menu, navigate back to **Enterprise Applications**. + +8. Under **Manage**, select **User Settings**. + +9. Under **Admin consent requests** -\> **Users can request admin + consent to apps they are unable to consent to**, select **Yes.** + +10. Under **Who can review admin consent requests**​, select the group + created in step two that is responsible for reviewing and + adjudicating app requests. + +11. Click **Save** + +## 2.8 Passwords SHALL NOT Expire + +Ensure that user passwords do not expire. Both the National Institute of +Standards and Technology (NIST) and Microsoft emphasize MFA because they +indicate that mandated password changes make user accounts less secure. + +### 2.8.1 Policy + +- User passwords SHALL NOT expire. + +### 2.8.2 Resources + +- [Password policy recommendations - Microsoft 365 admin \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/admin/misc/password-policy-recommendations?view=o365-worldwide#password-expiration-requirements-for-users) + +- [Eliminate bad passwords using Azure Active Directory Password + Protection](https://docs.microsoft.com/en-us/azure/active-directory/authentication/concept-password-ban-bad) + +- [NIST Special Publication 800-63B - Digital Identity + Guidelines](https://pages.nist.gov/800-63-3/sp800-63b.html) + +### 2.8.3 License Requirements + +- N/A + +### 2.8.4 Implementation + +[Follow the instructions at this +link](https://docs.microsoft.com/en-us/microsoft-365/admin/manage/set-password-expiration-policy?view=o365-worldwide#set-password-expiration-policy) +to configure the password expiration policy. + +## 2.9 Session Length SHALL Be Limited + +To reduce the risk of credential theft during user sessions, configure +the sign-in frequency to a limited period of time. + +### 2.9.1 Policy + +- Sign-in frequency SHALL be configured to 12 hours. + +### 2.9.2 Resources + +- [Configure authentication session management with Conditional + Access](https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/howto-conditional-access-session-lifetime) + +- [NIST Special Publication 800-63B - Digital Identity + Guidelines](https://pages.nist.gov/800-63-3/sp800-63b.html) + +### 2.9.3 License Requirements + +- N/A + +### 2.9.4 Implementation + +[Follow the instructions at this +link](https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/howto-conditional-access-session-lifetime#policy-1-sign-in-frequency-control) +to implement the conditional access policy that configures the sign-in +frequency, + +1. Set the **Users** or **workload identities** to include **All + users.** + +2. Set the **Cloud apps or actions** to include **All cloud apps.** + +3. Set the **Access Controls** -\> **Session** -\> **Sign-in + frequency** to a value of “12 hours”. + +## 2.10 Browser Sessions SHALL NOT Be Persistent + +To reduce the risk of credential theft during user sessions, disallow +persistent browser sessions. + +### 2.10.1 Policy + +- Browser sessions SHALL not be persistent. + +### 2.10.2 Resources + +- [Configure authentication session management with Conditional Access](https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/howto-conditional-access-session-lifetime) + +- [NIST Special Publication 800-63B - Digital Identity Guidelines](https://pages.nist.gov/800-63-3/sp800-63b.html) + +### 2.10.3 License Requirements + +- N/A + +### 2.10.4 Implementation + +[Follow the instructions at this link](https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/howto-conditional-access-session-lifetime#policy-2-persistent-browser-session) +to implement the conditional access policy that prevents persistent +browser sessions. + +1. Set the **Users or workload identities** to **include All users.** + +2. Set the **Cloud apps or actions** to include **All cloud apps.** + +3. Set the **Access Controls -**\> **Session** -\> **Persistent browser + session** to **Never persistent.** + +## 2.11 The Number of Users with the Highest Privilege Roles SHALL Be Limited + +Global Administrator is the highest privileged role in Azure AD because +it provides unfettered access to the tenant. Therefore, if a user’s +credential with these permissions were to be compromised, it would +present grave risks to the security of the tenant. Limit the number of +users that are assigned the role of Global Administrator. Assign users +to finer-grained administrative roles that they need to perform their +duties instead of being assigned the Global Administrator role. + +### 2.11.1 Policy + +- A minimum of two users and a maximum of four users SHALL be + provisioned with the Global Administrator role. + +### 2.11.2 Resources + +- [Best practices for Azure AD roles (Limit number of Global + Administrators to less than 5)](https://docs.microsoft.com/en-us/azure/active-directory/roles/best-practices#5-limit-the-number-of-global-administrators-to-less-than-5) + +- [About admin roles](https://docs.microsoft.com/en-us/microsoft-365/admin/add-users/about-admin-roles?view=o365-worldwide) + +### 2.11.3 License Requirements + +- N/A + +### 2.11.4 Implementation + +**Policy bullet \#1:** + +1. In the **Azure Portal**, navigate to **Azure Active Directory.** + + + +2. Select **Roles and administrators.** + +3. Select the **Global administrator role.** + +4. Under **Manage**, select **Assignments.** + +5. Validate that between two to four users are listed. + + + +6. For those who have Azure AD PIM, they will need to check both the + **Eligible assignments** and **Active assignments** tabs. There + should be a total of two to four users across both of these tabs + (not individually). + +7. If any groups are listed, need to check how many users are members + of each group and include that in the total count. + +**Policy bullet \#2:** + +1. In the **Azure Portal**, navigate to **Azure Active Directory.** + + + +2. Select **Security.** + +3. Under **Manage**, select **Identity Secure Score.** + +4. Click the **Columns** button and ensure that all the available + columns are selected to display and click **Apply.** + +5. Review the score for the action named **Use limited administrative + roles.** + +6. Ensure that the maximum score was achieved, and that the status is + **Completed.** + +7. If the maximum score was not achieved, click the improvement action + and Microsoft provides a pop-up page with detailed instructions on + how to address the weakness. In short, to address the weakness, + assign users to finer grained roles (e.g., SharePoint Administrator, + Exchange Administrator) instead of Global Administrator. Only the + minimum number of users necessary should be assigned to Global + Administrator. Once the roles are reassigned according to the + guidance, check the score again after 48 hours to ensure compliance. + +## 2.12 Highly Privileged User Accounts SHALL Be Cloud-Only + +Assign users that need to perform highly privileged tasks to cloud-only +Azure AD accounts to minimize the collateral damage of an on-premises +identity compromise.[^1] + +### 2.12.1 Policy + +- Users that need to be assigned to highly privileged Azure AD roles + SHALL be provisioned cloud-only accounts that are separate from the + on-premises directory or other federated identity providers. + +- The following built-in Azure AD roles are considered highly privileged + at a minimum. Additional built-in roles that are considered highly + privileged in the agency’s environment can be added to this list: + + - Global Administrator + + - Privileged Role Administrator + + - User Administrator + + - SharePoint Administrator + + - Exchange Administrator + + - Hybrid Identity Administrator + + - Application Administrator + + - Cloud Application Administrator + +### 2.12.2 Resources + +- [Securing privileged access for hybrid and cloud deployments in Azure AD](https://docs.microsoft.com/en-us/azure/active-directory/roles/security-planning#ensure-separate-user-accounts-and-mail-forwarding-for-global-administrator-accounts) + +### 2.12.3 License Requirements + +- N/A + +### 2.12.4 Implementation + +Review [these](https://docs.microsoft.com/en-us/azure/active-directory/roles/view-assignments) +instructions to identify users assigned to highly privileged roles and +verify the account does not exist outside Azure AD. + +## 2.13 Multifactor Authentication SHALL Be Required for Highly Privileged Roles + +Require users to perform MFA to access highly privileged roles. This +configuration provides a backup policy to enforce MFA for highly +privileged users in case the main conditional access policy—which +requires MFA for all users—is disabled or misconfigured. + +### 2.13.1 Policy + +- MFA SHALL be required for user access to highly privileged roles. + + + +- Refer to the baseline statement [Highly Privileged User Accounts SHALL + be Cloud-Only](#2.12.1 Policy) + for a recommended minimum list of Azure AD built-in roles that are + considered highly privileged. It is also possible to designate + additional built-in roles that are considered highly privileged in the + agency’s environment based on its risk tolerance. + +### 2.13.2 Resources + +- [Five steps to securing your identity infrastructure](https://docs.microsoft.com/en-us/azure/security/fundamentals/steps-secure-identity) + +- [M-22-09 Federal Zero Trust Strategy](https://www.whitehouse.gov/wp-content/uploads/2022/01/M-22-09.pdf) + +### 2.13.3 License Requirements + +- N/A + +### 2.13.4 Implementation + +[Follow these instructions](https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/howto-conditional-access-policy-all-users-mfa) +to create a conditional access policy requiring MFA for access, but +under **Assignments,** use the following tailored steps to scope the +policy to privileged roles. + +1. Under **Assignments**, select **Users and groups.** + + + +2. Under **Include**, choose **Select users and groups**, then click + the **Directory roles** checkbox. Select each of the roles listed in + the baseline statement, [Highly Privileged User Accounts SHALL be Cloud-Only](#2121-Policy). + +3. Under **Exclude**, follow Microsoft’s guidance from the previously + provided instructions link. + +## 2.14 Users Assigned to Highly Privileged Roles SHALL NOT Have Permanent Permissions + +Do not assign users to highly privileged roles using permanent active +role assignments. Instead, assign users to eligible role assignments in +a PAM system and provide an expiration period for active assignments +requiring privileged users to reactivate their highly privileged roles +upon expiration. + +**Note**: Although Azure AD PIM is referenced in the implementation +instructions, an equivalent third-party PAM service may be used instead. + +### 2.14.1 Policy + +- Permanent active role assignments SHALL NOT be allowed for highly + privileged roles. Active assignments SHALL have an expiration period. + - Refer to the baseline statement, [Highly Privileged User Accounts SHALL be Cloud-Only](#2121-Policy), + for a recommended minimum list of Azure AD built-in roles that are + considered highly privileged. It is also possible to designate + additional built-in roles that are considered highly privileged in the + agency’s environment based on its risk tolerance. + + + +- Provisioning of users to highly privileged roles SHALL NOT occur + outside of a PAM system, such as the Azure AD PIM service, because + this bypasses the controls the PAM system provides. + +### 2.14.2 Resources + +- [Assign Azure AD roles in Privileged Identity Management](https://docs.microsoft.com/en-us/azure/active-directory/privileged-identity-management/pim-how-to-add-role-to-user) + +### 2.14.3 License Requirements + +- Use of an Azure AD PIM or an equivalent third-party PAM service. + +- Azure AD PIM requires an AAD P2 license + +### 2.14.4 Implementation + +Note: Any parts of the following implementation instructions that +reference the Azure AD PIM service will vary if using a third-party PAM +system. + +1. In the **Azure Portal**, navigate to **Azure AD Privileged Identity + Management (PIM).** + + + +2. Under **Manage**, select **Azure AD roles**. + +3. Under **Manage**, select **Roles**. This should bring up a list of + all the Azure AD roles managed by the PIM service. + +4. **Note**: This step is specific to the first policy bullet. + Repeat this step and step 5 for each highly privileged role + referenced in the policy section. The role “Global Administrator” is + used as an example in these instructions. + + + 1. Click the **Global Administrator** role in the list. + 2. Click **Settings**. + 3. Click **Edit.** + 4. Select the **Assignment** tab. + 5. De-select the option named **Allow permanent active assignment.** + 6. Under **Expire active assignments after**, select **15 days**. + 7. Click **Update.** + + + +5. Note: This step is specific to the second policy bullet. + + + + 1. While on the **Assignments** page for the role, select the **Active + Assignments** tab. + 2. Review the assignments list. If any of the assignments show a + **Start time** of “-” (i.e., empty start time) and of **End time** + of **Permanent**, then those role assignments were made outside of + the PIM service and therefore are out of compliance with the policy. + 3. Delete the non-compliant role assignments and then recreate them + using the PIM service. + + + +6. In addition to checking for permanent assignments using the PIM + Assignments page in step \#5, PIM also provides a report that lists + all role assignments that were performed outside of PIM so that + those assignments can be deleted and properly recreated using PIM. + + + + 1. From the **PIM landing page**, under **Manage**, select **Azure AD + roles.** + 2. Under **Manage**, select **Alerts.** + 3. Click the **Scan** button and wait for the scan to complete. + 4. If there were any roles assigned outside of PIM, the report will + display an alert named, **Roles are being assigned outside of + Privileged Identity Management**; Click that alert. + 5. PIM displays a list of users, their associated roles, and the + date/time that they were assigned a role outside of PIM: Delete the + non-compliant role assignments and then recreate them using the PIM + service. + +## 2.15 Activation of Highly Privileged Roles SHOULD Require Approval + +Require approval for a user to activate a highly privileged role, such +as Global Administrator. This makes it more challenging for an attacker +to leverage the stolen credentials of highly privileged users and +ensures that privileged access is monitored closely. + +**Note**: Although Azure AD PIM is referenced in the implementation +instructions, an equivalent third-party PAM service may be used instead. + +### 2.15.1 Policy + +- Activation of highly privileged roles SHOULD require approval + + + +- Refer to the baseline statement [Highly Privileged User Accounts SHALL be Cloud-Only](#2121-Policy) + for a list of Azure AD built-in roles that are considered highly + privileged. It is also possible to configure additional built-in roles + that are considered highly privileged in the agency’s environment + based on its risk tolerance. + +### 2.15.2 Resources + +- [Approve or deny requests for Azure AD roles in Privileged Identity Management](https://docs.microsoft.com/en-us/azure/active-directory/privileged-identity-management/azure-ad-pim-approval-workflow) + +### 2.15.3 License Requirements + +- Use an Azure AD PIM or an equivalent third-party PAM service + +- Azure AD PIM requires an AAD P2 license + +### 2.15.4 Implementation + +**Note**: Any parts of the following implementation instructions that +reference the Azure AD PIM service will vary if using a third-party PAM +system. + +1. In the **Azure Portal**, navigate to **Azure AD** and create a new + group named “Privileged Escalation Approvers.” This group will + contain users that will receive role activation approval requests + and approve or deny them. Users in this group must, at least, have + the permissions provided to the Privileged Role Administrators role + to adjudicate requests. + +2. In the **Azure Portal**, navigate to **Azure AD Privileged Identity + Management (PIM).** + + + +3. Under **Manage**, select **Azure AD roles**. + +4. Under **Manage**, select **Roles**. This should bring up a list of + all the Azure AD roles managed by the PIM service. + +5. Repeat this step for the Privileged Role Administrator role, User + Administrator role, and other roles that the agency has designated + as highly privileged. + + + + 1. Click the **Global Administrator** role in the list. + 2. Click **Settings.** + 3. Click **Edit**. + 4. Select the **Require approval to activate** option. + 5. Click **Select approver**s, select the group **Privileged Escalation + Approvers**, and then click **Select**. + 6. Click **Update**. + +## 2.16 Highly Privileged Role Assignment and Activation SHALL Be Monitored + +Since many cyber attacks leverage privileged access, it is imperative to +closely monitor the assignment and activation of the highest privileged +roles for signs of compromise. Create alerts to trigger when a highly +privileged role is assigned to a user and when a user activates a highly +privileged role. + +Note: Although Azure AD PIM is referenced in the implementation +instructions, an equivalent third-party PAM service may be used instead. + +### 2.16.1 Policy + +- Eligible and Active highly privileged role assignments SHALL trigger + an alert. + + + + - Refer to the baseline statement [Highly Privileged User Accounts SHALL be Cloud-Only](#2121-Policy) + for a recommended minimum list of Azure AD built-in roles that are + considered highly privileged. It is also possible to designate + additional built-in roles that are considered highly privileged in the + agency’s environment based on its risk tolerance. + + + +- User activation of the Global Administrator role SHALL trigger an + alert. + +- User activation of other highly privileged roles SHOULD trigger an + alert. + + + + - Note: Alerts can be configured for user activation of other highly + privileged roles as well but note that if users activate these other + roles frequently, it can prompt a significant number of alerts. + Therefore, for those other roles, it might be prudent to set up a + separate monitoring mailbox from the one configured for the alerts + associated with the Global Administrator role. This separate mailbox + would be designed to store alerts for “review as necessary” purposes + versus the mailbox configured for the Global Administrator role, which + should be monitored closely since that role is sensitive. + +### 2.16.2 Resources + +- [Assign Azure AD roles in Privileged Identity Management](https://docs.microsoft.com/en-us/azure/active-directory/privileged-identity-management/pim-how-to-add-role-to-user) + +### 2.16.3 License Requirements + +- Use an Azure AD PIM or an equivalent third-party PAM service. + +- Azure AD PIM requires an AAD P2 license + +### 2.16.4 Implementation + +Note: Any parts of the following implementation instructions that +reference the Azure AD PIM service will vary if using a third-party PAM +system. + +1. In the **Azure Portal**, navigate to **Azure AD Privileged Identity + Management (PIM).** + + + +2. Under **Manage**, select A**zure AD roles.** + +3. Under **Manage**, select **Roles**. This should bring up a list of + all the Azure AD roles managed by the PIM service. + +4. Click the **Global Administrator** role. + +5. Click **Settings** and then click **Edit.** + +6. Click the **Notification** tab. + +7. Under **Send notifications when members are assigned as eligible to + this role**, in the **Role assignment alert** -\> **Additional + recipients** textbox, enter the email address of the mailbox + configured to receive the alerts for this role. + +8. Under S**end notifications when members are assigned as active to + this role**, in the **Role assignment alert** -\> **Additional + recipients** textbox, enter the email address of the mailbox + configured to receive the alerts for this role. + +9. Under **Send notifications when eligible members activate this + role**, in the **Role activation alert** -\> **Additional + recipients** textbox, enter the email address of the mailbox + configured to receive the alerts for this role. + +10. Click **Update**. + +11. Repeat steps 4 through 10 for each of the other highly privileged + roles referenced in the policy section above, with one modification: + + + + 1. When configuring the **Send notifications when eligible members + activate this role** for these other roles, enter an email address + of a mailbox that is different from the one used to monitor Global + Administrator activations. + +## 2.17 Managed Devices SHOULD Be Required for Authentication + +Require that users connect to M365 from a device that is managed using +conditional access. Agencies that are implementing a hybrid Azure AD +environment will likely use the conditional access control option named +**Hybrid Azure AD joined**, whereas agencies that are using devices that +connect directly to the cloud and do not join an on-premises AD will use +the conditional access control option named, **Require device to be +marked as compliant**. + +**Guest user access note**: This conditional access policy will impact +guest access to the tenant because guest users will be required to +authenticate from a managed device similar to regular Azure AD users. +For guest users, the organization that manages their home tenant is +responsible for managing their devices and the resource tenant must be +configured to trust the device claims from the home tenant, otherwise +guest users will be blocked by the policy. [This link](https://docs.microsoft.com/en-us/azure/active-directory/external-identities/authentication-conditional-access) describes the +detailed authentication flow for guest users and how conditional access +related to devices is applied. +The implementation section describes the cross-tenant settings that must +be configured in both the home and the resource tenants to facilitate +guest access with managed devices. + +### 2.17.1 Policy + +- Managed devices SHOULD be required for authentication. + +### 2.17.2 Resources + +- [Configure hybrid Azure AD join](https://docs.microsoft.com/en-us/azure/active-directory/devices/howto-hybrid-azure-ad-join) + +- [Azure AD joined devices](https://docs.microsoft.com/en-us/azure/active-directory/devices/concept-azure-ad-join) + +- [Set up enrollment for Windows devices (for Intune)](https://docs.microsoft.com/en-us/mem/intune/enrollment/windows-enroll) + +### 2.17.3 License Requirements + +- Use Microsoft Intune (if implementing the requirement for the device + to be compliant). + +### 2.17.4 Implementation + +[Follow these instructions](https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/howto-conditional-access-policy-compliant-device#create-a-conditional-access-policy) +to create a conditional access policy that requires the device to be +either hybrid Azure AD joined or compliant during authentication. + +Use the following instructions to facilitate guest access with managed +devices. Although the agency implementing this baseline only controls +the resource tenant and does not have control over the home tenant, CISA +provides our recommended security configuration for the home tenant in +this section. + +Reference [this link](https://docs.microsoft.com/en-us/azure/active-directory/external-identities/cross-tenant-access-overview) +for a general description of cross-tenant access settings to become +familiar with the terminology and configurations. + +For the resource tenant, use the following steps (for demonstration +purposes, the home tenant domain is named “home.onmicrosoft.com” — +replace this name with the actual name of the tenant): + + + +1. Navigate to **Azure AD** -\> **External Identities** -\> + **Cross-tenant access settings.** + + + +2. In **Organizational Settings**, add a new organization – + “home.onmicrosoft.com”. + +3. Open the **Inbound access** settings for the newly added + organization. + +4. Click the **B2B collaboration** tab. Under **External users and + Groups** -\> **Access status**, select **Allow access.** + +5. Under **External users and Groups** -\> **Applies to**, select **All + external users and groups.** + +6. Click the **Trust settings** tab. Under **Customize settings** -\> + select **Trust multi-factor authentication from Azure AD tenants**, + **Trust compliant devices,** and **Trust hybrid Azure AD joined + devices** + + + +For the home tenant, use the following steps (for demonstration +purposes the resource tenant domain is named +“resource.onmicrosoft.com” — replace this name with the actual name +of the tenant): + + + +1. Navigate to **Azure AD** -\> **External Identities** -\> + **Cross-tenant access settings.** + + + +2. In **Organizational Settings**, Add a new organization – + “resource.onmicrosoft.com”. + +3. Open the **Outbound access** settings for the newly added + organization. + +4. Click the **B2B collaboration** tab. Under **Users and Groups** -\> + **Access status**, select **Allow access.** + +5. Under **Users and Groups** -\> **Applies to**, select **All users.** + +## 2.18 Guest User Access SHOULD Be Restricted + +Ensure that only users with specific privileges can invite guest users +to the tenant and that invites can only be sent to specific external +domains. Also ensure that guest users have limited access to Azure AD +directory objects. + +### 2.18.1 Policy + +- Only users with the Guest Inviter role SHOULD be able to invite guest + users. + +- Guest invites SHOULD only be allowed to specific external domains that + have been authorized by the agency for legitimate business purposes. + +- Guest users SHOULD have limited access to Azure AD directory objects. + +### 2.18.2 Resources + +- [Configure external collaboration settings](https://docs.microsoft.com/en-us/azure/active-directory/external-identities/external-collaboration-settings-configure) + +### 2.18.3 License Requirements + +- N/A + +### 2.18.4 Implementation + +[Follow these instructions](https://docs.microsoft.com/en-us/azure/active-directory/external-identities/external-collaboration-settings-configure#configure-settings-in-the-portal) +to configure the Azure AD **External collaboration settings**. + +1. Under **Guest user access**, select **Guest users have limited + access to properties and memberships of directory objects.** + +2. Under **Guest invite settings**, select **Only users assigned to + specific admin roles can invite guest users**. + +3. Under **Collaboration restrictions**, select **Allow invitations + only to the specified domains (most restrictive)**. + + + +4. Select **Target domains** and enter the names of the external + domains that have been authorized by the agency for guest user + access. + +# Acknowledgements + +In addition to acknowledging the important contributions of a diverse +team of Cybersecurity and Infrastructure Security Agency (CISA) experts, +CISA thanks the following federal agencies and private sector +organizations that provided input during the development of the Secure +Business Cloud Application’s security configuration baselines in +response to Section 3 of [Executive Order (EO) 14028, *Improving the +Nation’s +Cybersecurity*](https://www.federalregister.gov/documents/2021/05/17/2021-10460/improving-the-nations-cybersecurity): + +- Consumer Financial Protection Bureau (CFPB) + +- Department of the Interior (DOI) + +- National Aeronautics and Space Administration (NASA) + +- Sandia National Laboratories (Sandia) + +- U.S. Census Bureau (USCB) + +- U.S. Geological Survey (USGS) + +- U.S. Office of Personnel Management (OPM) + +- U.S. Small Business Administration (SBA) + +The cross-agency collaboration and partnerships developed during this +initiative serve as an example for solving complex problems faced by the +federal government. + +**Cybersecurity Innovation Tiger Team (CITT) Leadership** + +Beau Houser (USCB), Sanjay Gupta (SBA), Michael Witt (NASA), James +Saunders (OPM), Han Lin (Sandia), Andrew Havely (DOI). + +**CITT Authors** + +Trafenia Salzman (SBA), Benjamin McChesney (OPM), Robert Collier (USCB), +Matthew Snitchler (Sandia), Darryl Purdy (USCB), Brandon Frankens +(NASA), Brandon Goss (NASA), Nicole Bogeajis (DOI/USGS), Kevin Kelly +(DOI), Adnan Ehsan (CFPB), Michael Griffin (CFPB), Vincent Urias +(Sandia), Angela Calabaza (Sandia). + +**CITT Contributors** + +Dr. Mukesh Rohatgi (MITRE), Lee Szilagyi (MITRE), Nanda Katikaneni +(MITRE), Ted Kolovos (MITRE), Thomas Comeau (MITRE), Karen Caraway +(MITRE), Jackie Whieldon (MITRE), Jeanne Firey (MITRE), Kenneth Myers +(General Services Administration). + +# Appendix A: Hybrid Azure AD Guidance + +The majority of this document does not focus on securing hybrid Azure AD +environments. CISA is working on a separate document that addresses the +unique implementation requirements of hybrid Azure AD infrastructure, +including the on-premises components. Meanwhile, the following limited +set of hybrid Azure AD policies that include on-premises components are +provided: + +- Azure AD Password Protection SHOULD be implemented for the on-premises + directory. + + + +- [Enforce on-premises Azure AD Password Protection for Active Directory Domain Services](https://docs.microsoft.com/en-us/azure/active-directory/authentication/concept-password-ban-bad-on-premises) + +- [Plan and deploy on-premises Azure Active Directory Password Protection](https://docs.microsoft.com/en-us/azure/active-directory/authentication/howto-password-ban-bad-on-premises-deploy) + + + +- Password hash synchronization with the on-premises directory SHOULD be + implemented. + + + +- [Implement password hash synchronization with Azure AD Connect sync](https://docs.microsoft.com/en-us/azure/active-directory/hybrid/how-to-connect-password-hash-synchronization) + + + +- Service accounts created in Azure AD to support the integration of + Azure AD Connect SHOULD be restricted to originate from the IP address + space of the network hosting the on-premises AD. This can be + implemented via a conditional access policy that is applied to the + Azure AD Connect service accounts and blocks access except from a + specific Azure AD Named Location that is configured with respective + on-premises IP address range. + + + +- [Using the location condition in a Conditional Access policy](https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/location-condition) + +- [Azure AD Connect: Accounts and permissions](https://docs.microsoft.com/en-us/azure/active-directory/hybrid/reference-connect-accounts-permissions) + +[^1]: “Cloud-only” user accounts have no ties to the on-premises AD and + are not federated – they are local to Azure AD only. diff --git a/baselines/defender.md b/baselines/defender.md new file mode 100644 index 0000000000..86f03a7d1a --- /dev/null +++ b/baselines/defender.md @@ -0,0 +1,912 @@ +# 1. Introduction + +Microsoft 365 Defender is a cloud-based enterprise defense suite that +coordinates prevention, detection, investigation, and response. This set +of tools and features are used to detect many types of attacks. + +This baseline focuses on the features of Defender for Office 365 and +some settings are in fact configured in the [Microsoft 365 +compliance](https://compliance.microsoft.com) admin center. However, for +simplicity, both the Microsoft 365 Defender and Microsoft 365 compliance +admin center items are contained in this baseline. + +Generally, use of Microsoft Defender is not required by the baselines of +the core M365 products (Exchange Online, Teams, etc.). However, some of +the controls in the core baselines require the use of a dedicated +security tool, such as Defender. This baseline should not be a +requirement to use Defender, but instead, as guidance for how these +requirements could be met using Defender, should an agency elect to use +Defender as their tool of choice. + +In addition to these controls, agencies should consider using a Cloud +Access Security Broker to secure their environments as they adopt zero +trust principles. + +## 1.1 Assumptions + +The **License Requirements** sections of this document assume the +organization is using an [M365 +E3](https://www.microsoft.com/en-us/microsoft-365/compare-microsoft-365-enterprise-plans) +or [G3](https://www.microsoft.com/en-us/microsoft-365/government) +license level. Therefore, only licenses not included in E3/G3 are +listed. + +## 1.2 Resources + +**License Compliance and Copyright** + +Portions of this document are adapted from documents in Microsoft’s +[Microsoft +365](https://github.com/MicrosoftDocs/microsoft-365-docs/blob/public/LICENSE) +and +[Azure](https://github.com/MicrosoftDocs/azure-docs/blob/main/LICENSE) +GitHub repositories. The respective documents are subject to copyright +and are adapted under the terms of the Creative Commons Attribution 4.0 +International license. Source documents are linked throughout this +document. The United States Government has adapted selections of these +documents to develop innovative and scalable configuration standards to +strengthen the security of widely used cloud-based software services. + +# 2. Baseline + +## 2.1 Preset Security Profiles SHOULD NOT Be Used + +Microsoft Defender defines two [preset security +profiles](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/preset-security-policies?view=o365-worldwide): +standard and strict. While most of the settings in this baseline mirror +the settings of the standard profile, this baseline recommends against +the use of the preset profiles. Instead, it enumerates all relevant +settings, as the preset security profiles are inflexible and take +precedence over all other present policies. + +### 2.1.1 Policy + +- Preset security profiles SHOULD NOT be used. + +### 2.1.2 Resources + +- [Recommended settings for EOP and Microsoft Defender for Office 365 + security \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365?view=o365-worldwide#eop-anti-spam-policy-settings) + +### 2.1.3 License Requirements + +- N/A + +## 2.2 Data Loss Prevention SHALL Be Enabled + +There are multiple, different ways to secure sensitive information, such +as warning users, encryption, or blocking attempts to share. The +agency’s data loss prevention (DLP) policy will dictate what agency +information is sensitive and how that information is handled. + +### 2.2.1 Policy + +- A custom policy SHALL be configured to protect PII and sensitive + information, as defined by the agency. At a minimum, credit card + numbers, Taxpayer Identification Numbers (TIN), and Social Security + Numbers (SSN) SHALL be blocked. + +- The custom policy SHOULD be applied in Exchange, OneDrive, Teams Chat, + and Microsoft Defender. + +- The action for the DLP policy SHOULD be set to block sharing sensitive + information with everyone when DLP conditions are met. + +- Notifications to inform users and help educate them on the proper use + of sensitive information SHOULD be enabled. + +- A list of apps that are not allowed to access files protected by DLP + policy SHOULD be defined. + +- A list of browsers that are not allowed to access files protected by + DLP policy SHOULD be defined. + +### 2.2.2 Resources + +- [Plan for data loss prevention (DLP) \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/compliance/dlp-overview-plan-for-dlp?view=o365-worldwide) + +- [Data loss prevention in Exchange Online \| Microsoft + Docs](https://docs.microsoft.com/en-us/exchange/security-and-compliance/data-loss-prevention/data-loss-prevention) + +- [Personally identifiable information (PII) \| + NIST](https://csrc.nist.gov/glossary/term/personally_identifiable_information#:~:text=NISTIR%208259,2%20under%20PII%20from%20EGovAct) + +- [Sensitive information \| + NIST](https://csrc.nist.gov/glossary/term/sensitive_information) + +### 2.2.3 License Requirements + +- DLP for Teams requires an E5 or G5 license. See [Information + Protection: Data Loss Prevention for Teams \| Microsoft + Docs](https://docs.microsoft.com/en-us/office365/servicedescriptions/microsoft-365-service-descriptions/microsoft-365-tenantlevel-services-licensing-guidance/microsoft-365-security-compliance-licensing-guidance#information-protection-data-loss-prevention-for-teams) + for more information. + +- DLP for Endpoint requires an E5 or G5 license. See [Get started with + Endpoint data loss prevention - Microsoft Purview (compliance) \| + Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/compliance/endpoint-dlp-getting-started?view=o365-worldwide) + for more information. + +### 2.2.4 Implementation + +1. Sign in to the [Microsoft 365 + compliance](https://compliance.microsoft.com) admin center. + +2. Under **Solutions**, select **Data loss prevention**. + +3. Select **Policies** from the top of the page. + +4. Select **Default Office 365 DLP policy**. + +5. Select **Edit policy**. + +6. Edit the name and description of the policy if desired, then click + **Next**.  + +7. Under **Locations to apply the policy**, set **Status** to **On** + for all products except Power BI (preview). + +8. Click **Create rule**. Assign the rule an appropriate name and + description. + +9. Click **Add condition**, then **Content contains**. + +10. Click **Add**, then **Sensitive info types**. + +11. Create policies that protect information that is sensitive to the + agency. At a minimum, the agency should protect: + + - Credit card numbers + + - U.S. Individual Taxpayer Identification Numbers (TIN) + + - U.S. Social Security Numbers (SSN) + + - All agency defined PII and sensitive information + +12. Click **Add**. + +13. Under **Actions**, click **Add an action**. + +14. Click **Restrict access of encrypt the content in Microsoft 365 + locations**. + +15. Check **Restrict Access or encrypt the content in Microsoft 365 + locations**. + +16. Select **Block Everyone**. + +17. Turn on **Use notifications to inform your users and help educate + them on the proper use of sensitive info**. + +18. Click **Save**, then **Next**. + +19. Select **Turn it on right away**, then click **Next**. + +20. Click **Submit**. + +21. Go to **Endpoint DLP Settings.** + + 1. Go to **Unallowed Apps.** + + 2. Click **Add** or **Edit Unallowed Apps.** + + 3. Enter an app and executable name to disallow said app from accessing + protected files and to log the incident. + + 4. Return and click **Unallowed Bluetooth Apps**. + + 5. Enter an app and executable name to disallow said app from accessing + protected files and to log the incident. + + 6. Return and click **Browser and domain restrictions to sensitive + data**. + + 7. Under **Unallowed Browsers**, enter and select needed browsers to + prevent that browser from accessing protected files. + + 8. Switch **Always audit file activity for devices** to **ON**. + +## 2.3 Common Attachments Filter SHALL Be Enabled + +Filtering emails by attachment file types will flag emails as malware if +the file type has been put in a predefined list of disallowed file +types. The Common Attachments Filter also attempts to look beyond just +the file extension and automatically detect the file type using true +typing. + +### 2.3.1 Policy + +- The common attachments filter SHALL be enabled in the default + anti-malware policy and in all existing policies. + +- Disallowed file types SHALL be determined and set. At a minimum, + click-to-run files SHOULD be blocked (e.g., .exe, .cmd, and .vbe). + +### 2.3.2 Resources + +- [Configure anti-malware policies in EOP \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/configure-anti-malware-policies?view=o365-worldwide) + +- [Anti-malware policies \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/anti-malware-protection?view=o365-worldwide#anti-malware-policies) + +### 2.3.3 License Requirements + +- Requires Defender for Office 365 Plan 1 or 2. These are included with + E5 and G5 and are available as add-ons for E3 and G3. + +### 2.3.4 Implementation + +To enable common attachments filter in the default policy: + +1. Sign in to [Microsoft 365 + Defender](https://security.microsoft.com/). + +2. Under **Email & collaboration**, select **Policies & rules**. + +3. Select **Threat policies**. + +4. Under **Policies**, select **Anti-malware**. + +5. Select the **Default (Default)** policy. + +6. Click **Edit protection settings**. + +7. Check **Enable the common attachments filter**. + +8. Click **Customize file types** as needed. + +9. Click **Save**. + +To create a new, custom policy, follow the instructions on [Use the +Microsoft 365 Defender portal to create anti-malware +policies](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/configure-anti-malware-policies?view=o365-worldwide#use-the-microsoft-365-defender-portal-to-create-anti-malware-policies). + +## 2.4 Zero-Hour Auto Purge for Malware SHOULD Be Enabled + +This setting determines whether emails can be quarantined automatically +after delivery to a user’s mailbox (e.g., in the case of a match with an +updated malware classification rule). + +### 2.4.1 Policy + +- Zero-hour Auto Purge (ZAP) for malware SHOULD be enabled in the + default anti-malware policy and in all existing custom policies. + +### 2.4.2 Resources + +- [Configure anti-malware policies in EOP \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/configure-anti-malware-policies?view=o365-worldwide) + +- [Anti-malware policies \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/anti-malware-protection?view=o365-worldwide#anti-malware-policies) + +### 2.4.3 License Requirements + +- Requires Defender for Office 365 Plan 1 or 2. These are included with + E5 and G5 and are available as add-ons for E3 and G3. + +### 2.4.4 Implementation + +To enable ZAP: + +1. Sign in to [Microsoft 365 + Defender](https://security.microsoft.com/). + +2. Under **Email & collaboration**, select **Policies & rules**. + +3. Select **Threat policies**. + +4. Under **Policies**, select **Anti-malware**. + +5. Select the **Default (Default)** policy. + +6. Click **Edit protection settings**. + +7. Check **Enable zero-hour auto purge for malware (Recommended)**. + +8. Click **Save**. + +## 2.5 Phishing Protections SHOULD Be Enabled + +There are multiple ways to protect against phishing, including +impersonation protection, mailbox intelligence and safety tips. +Impersonation protection checks incoming emails to see if the sender +address is similar to the users or domains on an agency-defined list. If +the sender address is significantly similar, as to indicate an +impersonation attempt, the email is quarantined. Mailbox intelligence is +an AI-based tool for identifying potential impersonation attempts. + +### 2.5.1 Policy + +- User impersonation protection SHOULD be enabled for key agency + leaders. + +- Domain impersonation protection SHOULD be enabled for domains owned by + the agency. + +- Domain impersonation protection SHOULD be added for frequent partners. + +- Trusted senders and domains MAY be added in the event of false + positives. + +- Intelligence for impersonation protection SHALL be enabled. + +- Message action SHALL be set to quarantine if the message is detected + as impersonated. + +- Mail classified as spoofed SHALL be quarantined. + +- All safety tips SHALL be enabled, including: + + - first contact, + + - user impersonation, + + - domain impersonation, + + - user impersonation unusual characters, + + - “?” for unauthenticated senders for spoof, and + + - “via” tag. + +- The above configurations SHALL be set in the default policy and SHOULD + be set in all existing custom policies. + +### 2.5.2 Resources + +- [Configure anti-phishing policies in EOP \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/configure-anti-phishing-policies-eop?view=o365-worldwide) + +- [EOP anti-phishing policy settings \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365?view=o365-worldwide#eop-anti-phishing-policy-settings) + +### 2.5.3 License Requirements + +- Impersonation protection and advanced phishing thresholds require + Defender for Office 365 Plan 1 or 2. These are included with E5 and G5 + and are available as add-ons for E3 and G3. As of September 1, 2022 + anti-phishing for user and domain impersonation and spoof intelligence + are not yet available in GCC High and DoD (see [Platform features \| + Microsoft + Docs](https://docs.microsoft.com/en-us/office365/servicedescriptions/office-365-platform-service-description/office-365-us-government/office-365-us-government#platform-features) + for current offerings). + +### 2.5.4 Implementation + +1. Sign in to [Microsoft 365 + Defender](https://security.microsoft.com/). + +2. Under **Email & collaboration**, select **Policies & rules**. + +3. Select **Threat policies**. + +4. Under **Policies**, select **Anti-phishing**. + +5. Select the **Office365 AntiPhish Default (Default)** policy. + +6. Click **Edit protection settings**. + +7. Check **Enable users to protect**. + +8. Click **Manage sender(s)**, then add users that merit impersonation + protection. + +9. Check **Enable domains to protect**. + +10. Check **Include domains I own**. + +11. Check **Include custom domains**. + +12. Click **Manage custom domains(s)** to add the domains of frequent + partners. + +13. Check **Enable mailbox intelligence (Recommended)**. + +14. Check **Enable Intelligence for impersonation protection + (Recommended)**. + +15. Click **Save**. + +16. Click **Edit actions**. + +17. Set **If message is detected as an impersonated user** to + **Quarantine the message**. + +18. Set **If message is detected as an impersonated domain** to + **Quarantine the message**. + +19. Set **If Mailbox Intelligence detects an impersonated user** to + **Quarantine the message**. + +20. Set **If message is detected as spoof** to **Quarantine the + message**. + +21. Under **Safety tips & indicators**, check: + +1. **Show first contact safety tip (Recommended)** + +2. **Show user impersonation safety tip** + +3. **Show domain impersonation safety tip** + +4. **Show user impersonation unusual characters safety tip** + +5. **Show (?) for unauthenticated senders for spoof** + +6. **Show “via” tag** + +22. Click **Save**. + +## 2.6 Inbound Anti-Spam Protections SHALL Be Enabled + +There are several features that protect against inbound spam. Bulk +compliant level, quarantines, safety tips, and zero-hour auto purge. + +### 2.6.1 Policy + +- The bulk complaint level (BCL) threshold SHOULD be set to six or + lower. + +- Spam and high confidence spam SHALL be moved to either the junk email + folder or the quarantine folder. + +- Phishing and high confidence phishing SHALL be quarantined. + +- Bulk email SHOULD be moved to either the junk email folder or the + quarantine folder. + +- Spam in quarantine SHOULD be retained for at least 30 days. + +- Spam safety tips SHOULD be turned on. + +- Zero-hour auto purge (ZAP) SHALL be enabled for both phishing and spam + messages. + +- Allowed senders MAY be added but allowed domains SHALL NOT be added. + +- The previously listed configurations SHALL be set in the default + policy and SHOULD be set in all existing custom policies. + +### 2.6.2 Resources + +- [Bulk complaint level (BCL) in EOP \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/bulk-complaint-level-values?view=o365-worldwide) + +- [EOP anti-spam policy settings \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365?view=o365-worldwide#eop-anti-spam-policy-settings) + +- [Configure anti-spam policies in EOP \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/configure-your-spam-filter-policies?view=o365-worldwide) + +### 2.6.3 License Requirements + +- N/A + +### 2.6.4 Implementation + +1. Sign in to [Microsoft 365 + Defender](https://security.microsoft.com/). + +2. Under **Email & collaboration**, select **Policies & rules**. + +3. Select **Threat policies**. + +4. Under Policies, select **Anti-spam**. + +5. Select **Anti-spam inbound policy (Default)**. + +6. Under **Bulk email threshold & spam properties**, click **Edit spam + threshold and properties**. + +7. Set **Bulk email threshold** to six or lower. + +8. Click **Save**. + +9. Under **Actions**, click **Edit actions**. + +10. In the **Message actions** section: + +1. For **Spam, High confidence spam**, and **Bulk**, set the action to + either **Move message to Junk Email folder** or **Quarantine + message**. + +2. Set the action for both **Phishin**g and **High confidence + phishing** to **Quarantine message**. + +3. Set **Retain spam in quarantine for this many days** to “30.” + +4. Check **Enable spam safety tips**. + +5. Check **Enable zero-hour auto purge (ZAP)**, **Enable for phishing + messages,** and **Enable for spam messages**. + +11. Click **Save.** + +## 2.7 Safe Link Policies SHOULD Be Enabled + +When enabled, URLs in emails are rewritten by prepending + +`https://*.safelinks.protection.outlook.com/?url=` + +to the original URL. This change can only be seen by either clicking the +URL or copying and pasting it; the end-user, even when hovering over the +URL in their email, will still only see the original URL. By prepending +the safe links URL, Microsoft can proxy the initial URL through their +scanning service. Their proxy can perform the following: + +- Compare the URL will a block list. + +- Compare the URL with a list of know malicious sites. + +- If the URL points to a downloadable file, apply real-time file + scanning. + +If all checks pass, the user is redirected to the original URL. + +### 2.7.1 Policy + +- The Safe Links Policy SHALL include all agency domains—and by + extension—all users. + +- URL rewriting and malicious link click checking SHALL be enabled. + +- Malicious link click checking SHALL be enabled with Microsoft Teams. + +- Real-time suspicious URL and file-link scanning SHALL be enabled. + +- URLs SHALL be scanned completely before message delivery. + +- Internal agency email messages SHALL have safe links enabled. + +- User click tracking SHALL be enabled. + +- Safe Links in Office 365 apps SHALL be turned on. + +- Users SHALL NOT be enabled to click through to the original URL. + +### 2.7.2 Resources + +- [Safe Links in Microsoft Defender for Office 365 \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/safe-links?view=o365-worldwide) + +- [Set up Safe Links policies in Microsoft Defender for Office 365 \| + Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/set-up-safe-links-policies?view=o365-worldwide) + +### 2.7.3 License Requirements + +- Requires Defender for Office 365 Plan 1 or 2. These are included with + E5 and G5 and are available as add-ons for E3 and G3. + +### 2.7.4 Implementation + +For more information about recommended Safe Links settings, see +[Safe](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365?view=o365-worldwide#safe-links-settings) +Links settings. + +1. Sign in to [Microsoft 365 + Defender](https://security.microsoft.com/). + +2. Under **Email & collaboration**, select **Policies & rules**. + +3. Select **Threat policies**. + +4. Under **Policies**, select **Safe Links**. + +5. Create a Safe Links Policy. + +1. Assign the new policy an appropriate name and description. + +2. Include all tenant domains. All users under those domains will be + added. + +3. On the **URL & click protection settings** page: + + +1. Select **On: Safe Links cehcks a list of known, malicious links when +users click links in email. URLs are rewritten by default.** + +2. Select **Apply Safe Links to email messages sent within the +organization** + +3. Select **Apply real-time URL scanning for suspicious links and links +that point to files.** + +4. Select **Wait for URL scanning to complete before delivering the +message.** + + +1. On the **URL & click protection settings** page, under **Teams**, + select **On: Safe Links checks a list of known, malicious links when + users click links in Microsoft Teams. URLs are not rewritten**. + +2. On the **URL & click protection settings** page, under **Office 365 + Apps**, select **On: Safe Links checks a list of known, malicious + links when users click links in Microsoft Office Apps. URLs are not + rewritten**. + +3. On the **URL & click protestion settings** page, under **Click + protestion settings**: + + 1. Select **Track User Clicks** + + 2. Do not select **Let users click through to the original URL**. + + +5. Review the new policy, then click **Submit**. + +## 2.8 Safe-Attachments SHALL Be Enabled + +The Safe Attachments will scan messages for attachments with malicious +content. It routes all messages and attachments that do not have a +virus/malware signature to a special environment. It then uses machine +learning and analysis techniques to detect malicious intent. Enabling +this feature may slow down message delivery to the user due to the +scanning. + +### 2.8.1 Policy + +- At least one Safe Attachments Policy SHALL include all agency + domains—and by extension—all users. + +- The action for malware in email attachments SHALL be set to block. + +- Redirect emails with detected attachments to an agency-specified email + SHOULD be enabled. + +- Safe attachments SHOULD be enabled for SharePoint, OneDrive, and + Microsoft Teams. + +### 2.8.2 Resources + +- [Safe Attachments in Microsoft Defender for Office 365 \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/safe-attachments?view=o365-worldwide#safe-attachments-policy-settings) + +- [Safe Attachments Policy Settings \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/safe-attachments?view=o365-worldwide#safe-attachments-policy-settings) + +- [Use the Microsoft 365 Defender portal to create Safe Attachments + policies \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/set-up-safe-attachments-policies?view=o365-worldwide#use-the-microsoft-365-defender-portal-to-create-safe-attachments-policies) + +- [Turn on Safe Attachments for SharePoint, OneDrive, and Microsoft + Teams \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/turn-on-mdo-for-spo-odb-and-teams?view=o365-worldwide) + +### 2.8.3 License Requirements + +- Requires Defender for Office 365 Plan 1 or 2. These are included with + E5 and G5 and are available as add-ons for E3 and G3. + +### 2.8.4 Implementation + +To configure safe attachments for Exchange Online, follow the +instructions listed on [Use the Microsoft 365 Defender portal to create +Safe Attachments +policies](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/set-up-safe-attachments-policies?view=o365-worldwide#use-the-microsoft-365-defender-portal-to-create-safe-attachments-policies). + +1. Sign in to [Microsoft 365 + Defender](https://security.microsoft.com/). + +2. Under **Email & collaboration**, select **Policies & rules**. + +3. Select **Threat policies**. + +4. Under **Policies**, select **Safe Attachments**. + +5. Click **Create** to start a new policy. + +6. Give the new policy an appropriate name and description. + +7. Under domains, enter all agency tenant domains. All users under + these domains will be added to the policy. + +8. Under **Safe Attachments unknown malware response**, select + **Block**. + +9. Set the **Quarantine policy** to **AdminOnlyAccessPolicy**. + +10. Click **Next**, then **Submit**. + +To enable Safe Attachments for SharePoint, OneDrive, and Microsoft +Teams, follow the instructions listed at [Turn on Safe Attachments for +SharePoint, OneDrive, and Microsoft Teams \| Microsoft +Docs](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/turn-on-mdo-for-spo-odb-and-teams?view=o365-worldwide). + +1. Sign in to [Microsoft 365 + Defender](https://security.microsoft.com/). + +2. Under **Email & collaboration**, select **Policies & rules**. + +3. Select **Threat policies**. + +4. Under **Policies**, select **Safe Attachments**. + +5. Select **Global settings**. + +6. Set **Turn on Defender for Office 365 for SharePoint, OneDrive, and + Microsoft Teams** to on. + +## 2.9 Alerts SHALL Be Enabled + +There are several pre-built alert policies available pertaining to +various apps in the M365 suite. These alerts give admins better +real-time insight into possible security incidents. + +### 2.9.1 Policy + +- At a minimum, the alerts required by the *Exchange Online Minimum + Viable Secure Configuration Baseline* SHALL be enabled. + +- The alerts SHOULD be sent to a monitored address or incorporated into + a SIEM. + +### 2.9.2 Resources + +- [Alert policies in Microsoft 365 \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/compliance/alert-policies?view=o365-worldwide) + +### 2.9.3 License Requirements + +- N/A + +### 2.9.4 Implementation + +1. Sign in to [Microsoft 365 + Defender](https://security.microsoft.com/). + +2. Under **Email & collaboration**, select **Policies & rules**. + +3. Select **Alert Policy**. + +4. Click the policy name. + +5. Ensure **Status** is set to **On**. + +6. Ensure **Email recipients** includes at least one monitored address. + +## 2.10 Unified Audit Logging SHALL Be Enabled + +Unified audit logging generates logs of user activity in M365 services. +These logs are essential for conducting incident response and threat detection activity. + +By default, Microsoft retains the audit logs for only 90 days. Activity +by users with E5 licenses is logged for one year. + +However, per OMB M-21-31, Microsoft 365 audit logs are to be retained at least 12 months in +active storage and an additional 18 months in cold storage. This can be +accomplished either by offloading the logs out of the cloud environment +or natively through Microsoft by creating an [audit log retention +policy](https://docs.microsoft.com/en-us/microsoft-365/compliance/audit-log-retention-policies?view=o365-worldwide#create-an-audit-log-retention-policy). + +OMB M-21-13 also requires Advanced Audit be configured in M365. Advanced Audit adds additional event types to the Unified Audit Log. + +### 2.10.1 Policy + +- Unified audit logging SHALL be enabled. + +- Advanced audit SHALL be enabled. + +- Audit logs SHALL be maintained for at least the minimum duration + dictated by OMB M-21-31. + +### 2.10.2 Resources + +- [OMB M-21-31 \| Office of Management and + Budget](https://www.whitehouse.gov/wp-content/uploads/2021/08/M-21-31-Improving-the-Federal-Governments-Investigative-and-Remediation-Capabilities-Related-to-Cybersecurity-Incidents.pdf) + +- [Turn auditing on or off \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/compliance/turn-audit-log-search-on-or-off?view=o365-worldwide)  + +- [Create an audit log retention policy \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/compliance/audit-log-retention-policies?view=o365-worldwide#create-an-audit-log-retention-policy) + +- [Search the audit log in the compliance center \| Microsoft + Docs ](https://docs.microsoft.com/en-us/microsoft-365/compliance/search-the-audit-log-in-security-and-compliance?view=o365-worldwide)  + +- [Audited Activities \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/compliance/search-the-audit-log-in-security-and-compliance?view=o365-worldwide#audited-activities) + +### 2.10.3 License Requirements + +- Advanced audit capabilities, including the creation of a custom audit + log retention policy, requires E5/G5 licenses or E3/G3 licenses with + add-on compliance licenses. + +- Additionally, maintaining logs in the M365 environment for longer than + one year requires an add-on license. For more information, see + [Licensing requirements \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/compliance/auditing-solutions-overview?view=o365-worldwide#licensing-requirements). + +### 2.10.4 Implementation + +Auditing can be enabled from the Microsoft 365 compliance admin center +and the Exchange Online PowerShell. Follow the instructions listed on +[Turn on +auditing](https://docs.microsoft.com/en-us/microsoft-365/compliance/turn-audit-log-search-on-or-off?view=o365-worldwide#turn-on-auditing). + +1. Sign in to the [Microsoft 365 + compliance](https://compliance.microsoft.com) admin center. + +2. Under **Solutions**, select **Audit**. + +3. If auditing is not enabled, a banner displays and prompts that the + user and admin activity start being recorded. + +4. Click the **Start recording user and admin activity banner**. + +To set up advanced audit, see [Set up Advanced Audit in Microsoft 365 \| +Microsoft +Docs.](https://docs.microsoft.com/en-us/microsoft-365/compliance/set-up-advanced-audit?view=o365-worldwide) + +To create an audit retention policy, follow the instructions listed on +[Create an audit log retention policy](https://docs.microsoft.com/en-us/microsoft-365/compliance/audit-log-retention-policies?view=o365-worldwide#create-an-audit-log-retention-policy). + +To check the current logging status via PowerShell + +1. Connect to the Exchange Online PowerShell. + +2. Run the following command: `Get-AdminAuditLogConfig | FL UnifiedAuditLogIngestionEnabled` + +To enable logging via PowerShell: + +1. Connect to the Exchange Online PowerShell. + +2. Run the following command: `Set-AdminAuditLogConfig -UnifiedAuditLogIngestionEnabled $true` + + +# Acknowledgements + +In addition to acknowledging the important contributions of a diverse +team of Cybersecurity and Infrastructure Security Agency (CISA) experts, +CISA thanks the following federal agencies and private sector +organizations that provided input during the development of the Secure +Business Cloud Application’s security configuration baselines in +response to Section 3 of [Executive Order (EO) 14028, *Improving the +Nation’s +Cybersecurity*](https://www.federalregister.gov/documents/2021/05/17/2021-10460/improving-the-nations-cybersecurity): + +- Consumer Financial Protection Bureau (CFPB) + +- Department of the Interior (DOI) + +- National Aeronautics and Space Administration (NASA) + +- Sandia National Laboratories (Sandia) + +- U.S. Census Bureau (USCB) + +- U.S. Geological Survey (USGS) + +- U.S. Office of Personnel Management (OPM) + +- U.S. Small Business Administration (SBA) + +The cross-agency collaboration and partnerships developed during this +initiative serve as an example for solving complex problems faced by the +federal government. + +**Cybersecurity Innovation Tiger Team (CITT) Leadership** + +Beau Houser (USCB), Sanjay Gupta (SBA), Michael Witt (NASA), James +Saunders (OPM), Han Lin (Sandia), Andrew Havely (DOI). + +**CITT Authors** + +Trafenia Salzman (SBA), Benjamin McChesney (OPM), Robert Collier (USCB), +Matthew Snitchler (Sandia), Darryl Purdy (USCB), Brandon Frankens +(NASA), Brandon Goss (NASA), Nicole Bogeajis (DOI/USGS), Kevin Kelly +(DOI), Adnan Ehsan (CFPB), Michael Griffin (CFPB), Vincent Urias +(Sandia), Angela Calabaza (Sandia). + +**CITT Contributors** + +Dr. Mukesh Rohatgi (MITRE), Lee Szilagyi (MITRE), Nanda Katikaneni +(MITRE), Ted Kolovos (MITRE), Thomas Comeau (MITRE), Karen Caraway +(MITRE), Jackie Whieldon (MITRE), Jeanne Firey (MITRE), Kenneth Myers +(General Services Administration). diff --git a/baselines/exchange.md b/baselines/exchange.md new file mode 100644 index 0000000000..863dddcf65 --- /dev/null +++ b/baselines/exchange.md @@ -0,0 +1,825 @@ +# 1. Introduction + +Microsoft Exchange Online provides users easy access to their email and +supports organizational meetings, contacts, and calendars. + +Many admin controls for Exchange Online are found in the [Exchange admin +center](https://admin.exchange.microsoft.com/). However, several of the +security features for Exchange Online are shared between Microsoft +products and are configured in either the [Microsoft 365 +Defender](https://security.microsoft.com/) or the [Microsoft 365 +compliance](https://compliance.microsoft.com) admin centers. Generally +speaking, the use of Microsoft Defender is not strictly required for +this baseline. When noted, alternative products may be used in lieu of +Defender, on the condition that they fulfill these required baseline +settings. + +## 1.1 Assumptions + +The **License Requirements** sections of this document assume the +organization is using an [M365 +E3](https://www.microsoft.com/en-us/microsoft-365/compare-microsoft-365-enterprise-plans) +or [G3](https://www.microsoft.com/en-us/microsoft-365/government) +license level. Therefore, only licenses not included in E3/G3 are +listed. + +## 1.2 Resources + +**License Compliance and Copyright** + +Portions of this document are adapted from documents in Microsoft’s +[Microsoft +365](https://github.com/MicrosoftDocs/microsoft-365-docs/blob/public/LICENSE) +and +[Azure](https://github.com/MicrosoftDocs/azure-docs/blob/main/LICENSE) +GitHub repositories. The respective documents are subject to copyright +and are adapted under the terms of the Creative Commons Attribution 4.0 +International license. Source documents are linked throughout this +document. The United States Government has adapted selections of these +documents to develop innovative and scalable configuration standards to +strengthen the security of widely used cloud-based software services. + +# 2. Baseline + +## 2.1 Automatic Forwarding to External Domains SHALL Be Disabled + +This control is intended to prevent bad actors from using client-side +forwarding rules to exfiltrate data to external recipients. + +### 2.1.1 Policy + +- Automatic forwarding to external domains SHALL be disabled. + +### 2.1.2 Resources + +- [Reducing or increasing information flow to another company \| + Microsoft + Docs](https://docs.microsoft.com/en-us/exchange/mail-flow-best-practices/remote-domains/remote-domains#reducing-or-increasing-information-flow-to-another-company) + +### 2.1.3 License Requirements + +- N/A + +### 2.1.4 Implementation + +To disallow automatic forwarding to external domains: + +1. Sign in to the [Exchange admin + center](https://admin.exchange.microsoft.com/). + +2. Select **Mail flow,** then **Remote domains**. + +3. Select **Default**. + +4. Under **Email reply types**, select **Edit reply types**. + +5. Clear the checkbox next to **Allow automatic forwarding**, then + click **Save**. + +## 2.2 Sender Policy Framework SHALL Be Enabled + +The Sender Policy Framework (SPF) is a mechanism that allows domain +administrators to specify which IP addresses are explicitly approved to +send email on behalf of the domain, facilitating detection of spoofed +emails. SPF isn’t configured through the Exchange admin center, but +rather via DNS records hosted by the agency’s domain. Thus, the exact +steps needed to set up SPF varies from agency to agency, but Microsoft’s +documentation provides some helpful starting points. + +### 2.2.1 Policy + +- A list of approved IP addresses for sending mail SHALL be maintained. + +- An SPF policy(s) that designates only these addresses as approved + senders SHALL be published. + +### 2.2.2 Resources + +- [Binding Operational Directive 18-01 - Enhance Email and Web Security + \| DHS](https://cyber.dhs.gov/bod/18-01/) + +- [Trustworthy Email \| NIST 800-177 Rev. + 1](https://csrc.nist.gov/publications/detail/sp/800-177/rev-1/final) + +- [Set up SPF to help prevent spoofing \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/set-up-spf-in-office-365-to-help-prevent-spoofing?view=o365-worldwide) + +- [How Microsoft 365 uses Sender Policy Framework (SPF) to prevent + spoofing \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/how-office-365-uses-spf-to-prevent-spoofing?view=o365-worldwide) + +### 2.2.3 License Requirements + +- N/A + +### 2.2.4 Implementation + +SPF is not configured through the Exchange admin center, but rather via +DNS records hosted by the agency’s domain. Thus, the exact steps needed +to set up SPF varies from agency to agency. + +To test your SPF configuration, SPF records can be requested using the +PowerShell tool Resolve-DnsName. For example: + +`Resolve-DnsName example.com txt` + +## 2.3 DomainKeys Identified Mail SHOULD Be Enabled + +DomainKeys Identified Mail (DKIM) allows digital signatures to be added +to email messages in the message header, providing a layer of both +authenticity and integrity to emails. As with SPF, DKIM relies on Domain +Name Service (DNS) records, thus, its deployment depends on how an +agency manages its DNS. DKIM is enabled for your tenant's default domain +(e.g., onmicrosoft.com domains), but it must be manually enabled for +custom domains. + +### 2.3.1 Policy + +- DKIM SHOULD be enabled for any custom domain. + +### 2.3.2 Resources + +- [Binding Operational Directive 18-01 - Enhance Email and Web Security + \| DHS](https://cyber.dhs.gov/bod/18-01/) + +- [Trustworthy Email \| NIST 800-177 Rev. + 1](https://csrc.nist.gov/publications/detail/sp/800-177/rev-1/final) + +- [Use DKIM to validate outbound email sent from your custom domain \| + Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/use-dkim-to-validate-outbound-email?view=o365-worldwide) + +- [Support for validation of DKIM signed messages \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/support-for-validation-of-dkim-signed-messages?view=o365-worldwide) + +- [What is EOP? \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/eop-general-faq?view=o365-worldwide#what-is-eop-) + +### 2.3.3 License Requirements + +- DKIM signing is included with Exchange Online Protection (EOP), which + in turn is included in all Microsoft 365 subscriptions that contain + Exchange Online mailboxes. + +### 2.3.4 Implementation + +To enable DKIM, follow the instructions listed on [Steps to Create, +enable and disable DKIM from Microsoft 365 Defender portal \| Microsoft +Docs](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/use-dkim-to-validate-outbound-email?view=o365-worldwide#steps-to-create-enable-and-disable-dkim-from-microsoft-365-defender-portal). + +1. Navigate to the [Microsoft 365 + Defender](http://security.microsoft.com) admin center. + +2. Go to Policies & Rules. + +3. Go to Threat Policies. + +4. Select **DKIM**. + +5. Select your domain. + +6. Switch **Sign messages for this domain with DKIM signatures** to + **Enabled**. + +7. If you are enabling DKIM for the first time, a pop-up window listing + Canonical Name (CNAME) records displays. Publish these records to + your DNS service provider. + +8. Return to the DKIM page on the Defender admin center to finish + enabling DKIM. + +## 2.4 Domain-Based Message Authentication, Reporting, and Conformance SHALL Be Enabled + +Domain-based Message Authentication, Reporting, and Conformance (DMARC) +works with SPF and DKIM to authenticate mail senders and ensure that +destination email systems can validate messages sent from your domain. +DMARC helps receiving mail systems determine what to do with messages +sent from your domain that fail SPF or DKIM checks. + +### 2.4.1 Policy + +- A DMARC policy SHALL be published for every second-level domain. + +- The DMARC message rejection option SHALL be “p=reject”. + +- The DMARC point of contact for aggregate reports SHALL include + . + +- An agency point of contact SHOULD be included for aggregate and/or + failure reports. + +### 2.4.2 Resources + +- [Binding Operational Directive 18-01 - Enhance Email and Web Security + \| DHS](https://cyber.dhs.gov/bod/18-01/) + +- [Trustworthy Email \| NIST 800-177 Rev. + 1](https://csrc.nist.gov/publications/detail/sp/800-177/rev-1/final) + +- [Domain-based Message Authentication, Reporting, and Conformance + (DMARC) \| RFC 7489](https://datatracker.ietf.org/doc/html/rfc7489) + +- [Best practices for implementing DMARC in Office 365 \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/use-dmarc-to-validate-email?view=o365-worldwide#best-practices-for-implementing-dmarc-in-microsoft-365) + +- [How Office 365 handles outbound email that fails DMARC \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/use-dmarc-to-validate-email?view=o365-worldwide#how-microsoft-365-handles-inbound-email-that-fails-dmarc) + +### 2.4.3 License Requirements + +- N/A + +### 2.4.4 Implementation + +DMARC implementation varies depending on how an agency manages its DNS +records. See [Form the DMARC TXT record for your domain \| Microsoft +Docs](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/use-dmarc-to-validate-email?view=o365-worldwide#step-4-form-the-dmarc-txt-record-for-your-domain) +for Microsoft guidance. + +DMARC records can be requested using the Powershell tool +Resolve-DnsName. For example: + +`Resolve-DnsName _dmarc.example.com txt` + +Replace “example.com” in the example with the domain(s) used for your +agency’s emails. Ensure that (1) the DNS record exists, (2) “p=reject;” +is included in the policy returned from the query, and that (3) + is included as a point for contact for +aggregate feedback. + +## 2.5 Simple Mail Transfer Protocol Authentication SHALL Be Disabled + +Modern email clients that connect to Exchange Online mailboxes—including +Outlook, Outlook on the web, iOS Mail, and Outlook for iOS and +Android—do not use Simple Mail Transfer Protocol Authentication (SMTP +AUTH) to send email messages. SMTP AUTH is only needed for applications +outside of Outlook that send email messages. + +### 2.5.1 Policy + +- SMTP AUTH SHALL be disabled in Exchange Online. + +- SMTP AUTH MAY be enabled on a per-mailbox basis as needed. + +### 2.5.2 Resources + +- [Enable or disable authenticated client SMTP submission (SMTP AUTH) in + Exchange Online \| Microsoft + Docs](https://docs.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission) + +- [Use the Microsoft 365 admin center to enable or disable SMTP AUTH on + specific mailboxes \| Microsoft + Docs](https://docs.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission#use-the-microsoft-365-admin-center-to-enable-or-disable-smtp-auth-on-specific-mailboxes) + +### 2.5.3 License Requirements + +- N/A + +### 2.5.4 Implementation + +SMTP AUTH can only be disabled tenant-wide using Exchange Online +PowerShell. Follow the instructions listed at [Disable SMTP AUTH in your +organization \| Microsoft +Docs](https://docs.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission#disable-smtp-auth-in-your-organization). + +To enable SMTP AUTH on a per-mailbox basis, follow the instructions +listed at [Use the Microsoft 365 admin center to enable or disable SMTP +AUTH on specific mailboxes \| Microsoft +Docs](https://docs.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission#use-the-microsoft-365-admin-center-to-enable-or-disable-smtp-auth-on-specific-mailboxes). + +## 2.6 Calendar and Contact Sharing SHALL Be Restricted + +Exchange Online allows the creation of sharing polices that soften +default restrictions on contact and calendar details sharing. These +policies should only be enabled with caution and must comply with the +following policies. + +### 2.6.1 Policy + +- Contact folders SHALL NOT be shared with all domains, although they + MAY be shared with specific domains. + +- Calendar details SHALL NOT be shared with all domains, although they + MAY be shared with specific domains. + +### 2.6.2 Resources + +- [Sharing in Exchange Online \| Microsoft + Docs](https://docs.microsoft.com/en-us/exchange/sharing/sharing) + +- [Organization relationships in Exchange Online \| Microsoft + Docs](https://docs.microsoft.com/en-us/exchange/sharing/organization-relationships/organization-relationships) + +- [Sharing policies in Exchange Online \| Microsoft + Docs](https://docs.microsoft.com/en-us/exchange/sharing/sharing-policies/sharing-policies) + +### 2.6.3 License Requirements + +- N/A + +### 2.6.4 Implementation + +To restrict sharing with all domains: + +1. Sign in to the [Exchange admin + center](https://admin.exchange.microsoft.com). + +2. Under **Organization**, select **Sharing**. + +3. Under **Individual Sharing**, for all existing policies, ensure that + for all sharing rules, **Sharing with all domains** is not selected. + +## 2.7 External Sender Warnings SHALL Be Implemented + +Mail flow rules allow the modification of incoming mail, such that mail +from external users can be easily identified, for example by prepending +the subject line with “\[External\].” + +### 2.7.1 Policy + +- External sender warnings SHALL be implemented. + +### 2.7.2 Resources + +- [Mail flow rules (transport rules) in Exchange Online \| Microsoft + Docs](https://docs.microsoft.com/en-us/exchange/security-and-compliance/mail-flow-rules/mail-flow-rules) + +- [Capacity Enhancement Guide: Counter-Phishing Recommendations for + Federal Agencies \| + CISA](https://www.cisa.gov/sites/default/files/publications/Capacity_Enhancement_Guide-Counter-Phishing_Recommendations_for_Federal_Agencies.pdf) + +- [Actions To Counter Email-Based Attacks On Election-Related Entities + \| + Cisa](https://www.cisa.gov/sites/default/files/publications/CISA_Insights_Actions_to_Counter_Email-Based_Attacks_on_Election-Related_S508C.pdf) + +### 2.7.3 License Requirements + +- N/A + +### 2.7.4 Implementation + +To enable external sender warnings: + +1. Sign in to the [Exchange admin + center](https://admin.exchange.microsoft.com). + +2. Under **Mail flow**, select **Rules**. + +3. Click the plus (**+**) button to create a new rule. + +4. Select **Modify messages…**. + +5. Give the rule an appropriate name. + +6. Under **Apply this rule if…,** select **The sender is located…** + +7. Under **select sender location**, select **Outside the + organization**, then click **OK.** + +8. Under **Do the following…,** select **Prepend the subject of the + message with…**. + +9. Under **specify subject prefix**, enter a message such as + “\[External\]” (without the quotation marks), then click **OK**. + +10. Under **Choose a mode for this rule**, select **Enforce**. + +11. Click **Save**. + +## 2.8 Data Loss Prevention Solutions SHALL Be Enabled + +Data loss prevention (DLP) helps prevent both accidental leakage of +sensitive information as well as intentional exfiltration of data. DLP +forms an integral part of securing Microsoft Exchange Online. There a +several commercial DLP solutions available that document support for +M365. Agencies may select any service that fits their needs and meets +the requirements outlined in this baseline setting. + +Microsoft offers DLP services, controlled within the [Microsoft 365 +compliance](https://compliance.microsoft.com) admin center. Though use +of Microsoft’s DLP solution is not strictly required, guidance for +configuring Microsoft’s DLP solution can be found in the “Data Loss +Prevention SHALL Be Enabled” section of the *Defender for Office 365 +Minimum Viable Secure Configuration Baseline*. The DLP solution selected +by an agency should offer services comparable to those offered by +Microsoft. + +### 2.8.1 Policy + +- A DLP solution SHALL be used. The selected DLP solution SHOULD offer + services comparable to the native DLP solution offered by Microsoft. + +- 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. + +### 2.8.2 Resources + +- The “Data Loss Prevention SHALL Be Enabled” section of the *Defender + for Office 365 Minimum Viable Secure Configuration Baseline*. + +## 2.9 Emails SHALL Be Filtered by Attachment File Type + +For some types of files (e.g., executable files), the dangers of +allowing them to be sent over email outweigh any potential benefits. +Some services, such as the Common Attachment Filter of Microsoft +Defender, filter emails based on the attachment file types. Use of +Microsoft Defender for this purpose is not strictly required; instead, +equivalent products that fulfill the requirements outlined in this +baseline setting may be used. + +Though use of Microsoft Defender’s solution is not strictly required for +this purpose, guidance for configuring the Common Attachment Filter in +Microsoft Defender can be found in the “Common Attachments Filter SHALL +Be Enabled” section of the Defender for Office 365 Minimum Viable Secure +Configuration Baseline. The solution selected by an agency should offer +services comparable to those offered by Microsoft. + +### 2.9.1 Policy + +- 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. + +- The attachment filter SHOULD attempt to determine the true file type + and assess the file extension. + +- Disallowed file types SHALL be determined and set. At a minimum, + click-to-run files SHOULD be blocked (e.g., .exe, .cmd, and .vbe). + +### 2.9.2 Resources + +- The “Common Attachments Filter SHALL Be Enabled” section of the + *Defender for Office 365 Minimum Viable Secure Configuration + Baseline*. + +## 2.10 Emails SHALL Be Scanned for Malware + +Though any product that fills the requirements outlined in this baseline +setting may be used, for guidance on implementing malware scanning using +Microsoft Defender, see the following sections of the *Defender for +Office 365 Minimum Viable Secure Configuration Baseline*: + +- The “Safe-Attachments SHALL Be Enabled” + +- “Zero-hour Auto Purge for Malware SHALL Be Enabled” + +### 2.10.1 Policy + +- Emails SHALL be scanned for malware. + +- Emails identified as containing malware SHALL be quarantined or + dropped. + +- Email scanning SHOULD be capable of reviewing emails after delivery. + +### 2.10.2 Resources + +- The “Safe-Attachments SHALL Be Enabled” section of the *Defender for + Office 365 Minimum Viable Secure Configuration Baseline.* + +- The “Zero-hour Auto Purge for Malware SHALL Be Enabled” section of the + *Defender for Office 365 Minimum Viable Secure Configuration + Baseline.* + +## 2.11 Phishing Protections SHOULD Be Enabled + +Several techniques exist for protecting against phishing attacks, +including the following techniques: + +- Impersonation protection checks, wherein a tool compares the sender’s + address to the addresses of known senders to flag look-alike + addresses, like and + +- User warnings, such as displaying a notice the first time a user + receives an email from a new sender + +- AI-based tools + +Microsoft Defender has capabilities for all of these phishing +protections. And except for impersonation protection, these features are +available with EOP, which is included in all Microsoft 365 subscriptions +that contain Exchange Online mailboxes. For more guidance on configuring +phishing protections with Microsoft’s native solutions, see the +“Phishing Protections SHOULD Be Enabled,” section of the *Defender for +Office 365 Minimum Viable Secure Configuration Baseline*. + +### 2.11.1 Policy + +- Impersonation protection checks SHOULD be used. + +- User warnings, comparable to the user safety tips included with EOP, + SHOULD be displayed. + +- The phishing protection solution SHOULD include an AI-based phishing + detection tool comparable to EOP Mailbox Intelligence. + +### 2.11.2 Resources + +- The “Phishing Protections SHOULD Be Enabled” section of the *Defender + for Office 365 Minimum Viable Secure Configuration Baseline.* + +## 2.12 IP Allow Lists SHOULD NOT be Implemented + +Microsoft Defender supports the creations of IP “allow lists,” intended +to ensure that emails from *specific* senders are not blocked. However, +as a result, emails from these senders bypass important security +mechanisms, such as spam filtering, SPF, DKIM, DMARC, and [FROM address +enforcement](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/how-office-365-validates-the-from-address?view=o365-worldwide#override-from-address-enforcement). + +IP “block lists” ensure that mail from these IP addresses is always +blocked. Although we have no specific guidance on which IP addresses to +add, block lists can be used to block mail from known spammers. + +IP “safe lists” is a dynamic list of “known, good senders,” which +Microsoft sources from various third-party subscriptions. As with +senders in the allow list, emails from these senders bypass important +security mechanisms. + +### 2.12.1 Policy + +- IP allow lists SHOULD NOT be created. + +- Safe lists SHOULD NOT be enabled. + +- A connection filter MAY be implemented to create an IP “Block list.” + +### 2.12.2 Resources + +- [Use the IP Allow List \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/create-safe-sender-lists-in-office-365?view=o365-worldwide#use-the-ip-allow-list) + +- [Configure connection filtering \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/configure-the-connection-filter-policy?view=o365-worldwide) + +- [Use the Microsoft 365 Defender portal to modify the default + connection filter policy \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/configure-the-connection-filter-policy?view=o365-worldwide#use-the-microsoft-365-defender-portal-to-modify-the-default-connection-filter-policy) + +### 2.12.3 License Requirements + +- Exchange Online Protection + +### 2.12.4 Implementation + +To modify the connection filters, follow the instructions found on [Use +the Microsoft 365 Defender portal to modify the default connection +filter +policy](https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/configure-the-connection-filter-policy?view=o365-worldwide#use-the-microsoft-365-defender-portal-to-modify-the-default-connection-filter-policy). + +1. Sign in to [Microsoft 365 + Defender](https://security.microsoft.com/threatpolicy)**.** + +2. Under **Email & collaboration**, select **Policies & rules**. + +3. Under **Policies**, select **Anti-spam.** + +4. Select **Connection filter policy (Default).** + +5. Click **Edit connection filter policy.** + +6. Ensure no addresses are specified under **Always allow messages from + the following IP addresses or address range**. + +7. Enter addresses under **Always block messages from the following IP + addresses or address range** as needed. + +8. Ensure **Turn on safe list** is not selected. + +## 2.13 Mailbox Auditing SHALL Be Enabled + +Mailbox auditing helps users investigate compromised accounts or +discover illicit access to Exchange Online. Some actions performed by +administrators, delegates, and owners are logged automatically. While +mailbox auditing is enabled by default, agencies should ensure that it +has not been inadvertently disabled. + +### 2.13.1 Policy + +- Mailbox auditing SHALL be enabled. + +### 2.13.2 Resources + +- [Manage mailbox auditing in Office 365 \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/compliance/enable-mailbox-auditing?view=o365-worldwide) + +- [Supported mailbox types \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/compliance/enable-mailbox-auditing?view=o365-worldwide%22%20\l%20%22supported-mailbox-types) + +- [Microsoft Compliance Manager - Microsoft 365 Compliance \|Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/compliance/compliance-manager?view=o365-worldwide) + +### 2.13.3 License Requirements + +- N/A + +### 2.13.4 Implementation + +Mailbox auditing can be enabled from the Exchange Online PowerShell. +Follow the instructions listed on [Manage mailbox auditing in Office +365](https://docs.microsoft.com/en-us/microsoft-365/compliance/enable-mailbox-auditing?view=o365-worldwide). + +To check the current mailbox auditing status via PowerShell: + +1. Connect to the Exchange Online PowerShell. + +2. Run the following command: + +`Get-OrganizationConfig | Format-List AuditDisabled` + +To enable mailbox auditing via PowerShell: + +1. Connect to the Exchange Online PowerShell + +2. Run the following command: + +`Set-OrganizationConfig –AuditDisabled $false` + +## 2.14 Inbound Anti-Spam Protections SHALL Be Enabled + +Microsoft Defender includes several capabilities for protecting against +inbound spam emails. Use of Microsoft Defender is not strictly required +for this purpose; any product that fulfills the requirements outlined in +this baseline setting may be used. See the “Inbound Anti-Spam +Protections SHALL Be Enabled” section of the *Defender for Office 365 +Minimum Viable Secure Configuration Baseline* for additional guidance. + +### 2.14.1 Policy + +- A spam filter SHALL be enabled. The filtering solution selected SHOULD + offer services comparable to the native spam filtering offered by + Microsoft. + +- Spam and high confidence spam SHALL be moved to either the junk email + folder or the quarantine folder. + +- Allowed senders MAY be added, but allowed domains SHALL NOT be added. + +### 2.14.2 Resources + +- The “Inbound Anti-Spam Protections SHALL Be Enabled” section of the + *Defender for Office 365 Minimum Viable Secure Configuration + Baseline*. + +## 2.15 Link Protection SHOULD Be Enabled + +Several technologies exist for protecting users from malicious links +included in emails. For example, Microsoft Defender accomplishes this by +prepending + +`https://*.safelinks.protection.outlook.com/?url=` + +to any URLs included in emails. By prepending the safe links URL, +Microsoft can proxy the initial URL through their scanning service. +Their proxy can perform the following actions: + +- Compare the URL with a block list, + +- Compare the URL with a list of know malicious sites, and + +- If the URL points to a downloadable file, apply real-time file + scanning. + +If all checks pass, the user is redirected to the original URL. + +Though Defender’s use is not strictly required for this purpose, +guidance for enabling link scanning using Microsoft Defender is included +in the “Safe Links Policies SHALL Be Enabled” section of the *Defender for Office 365 +Minimum Viable Secure Configuration Baseline.* + +### 2.15.1 Policy + +- URL comparison with a block-list SHOULD be enabled. + +- Direct download links SHOULD be scanned for malware. + +- User click tracking SHOULD be enabled. + +### 2.15.2 Resources + +- The “Safe Links Policies SHOULD Be Enabled” section of the *Defender + for Office 365 Minimum Viable Secure Configuration Baseline*. + +## 2.16 Alerts SHALL Be Enabled + +Microsoft Defender includes several prebuilt alert policies, many of +which pertain to Exchange Online. These alerts give admins better +real-time insight into possible security incidents. Guidance for +configuring alerts in Microsoft Defender is given in the “Alerts SHALL +Be Enabled” section of the *Defender for Office 365 Minimum Viable +Secure Configuration Baseline*. + +### 2.16.1 Policy + +- At a minimum, the following alerts SHALL be enabled: + + - Suspicious email sending patterns detected. + + - Suspicious Connector Activity. + + - Suspicious Email Forwarding Activity. + + - Unusual increase in email reported as phish. + + - Messages have been delayed. + + - Tenant restricted from sending unprovisioned email. + + - Tenant restricted from sending email. + + - Malware campaign detected after delivery. + + - A potentially malicious URL click was detected. + +- The alerts SHOULD be sent to a monitored address or incorporated into + a SIEM. + +### 2.16.2 Resources + +- The “Alerts SHALL Be Enabled” section of the *Defender for Office 365 + Minimum Viable Secure Configuration Baseline*. + +## 2.17 Unified Audit Logging SHALL Be Enabled + +Unified audit logging generates logs of user activity in M365 services. +These logs are essential for conducting incident response and threat detection activity. + +By default, Microsoft retains the audit logs for only 90 days. Activity by users with E5 licenses is logged for one year. + +However, per OMB M-21-31, Microsoft 365 audit logs are to be retained at least 12 months in active storage and an additional 18 months in cold storage. +This can be accomplished either by offloading the logs out of the cloud environment or natively through Microsoft by creating an audit log retention policy. + +OMB M-21-13 also requires Advanced Audit be configured in M365. Advanced Audit adds additional event types to the Unified Audit Log. + +Audit logging is managed from the Microsoft compliance center. For +guidance configuring audit logging, see the “Audit Logging SHALL Be +Enabled” section of the *Defender for Office 365 Minimum Viable Secure +Configuration Baseline*. + +### 2.17.1 Policy + +- Unified audit logging SHALL be enabled. + +- Advanced audit SHALL be enabled. + +- Audit logs SHALL be maintained for at least the minimum duration + dictated by [OMB M-21-31 (Appendix + C)](https://www.whitehouse.gov/wp-content/uploads/2021/08/M-21-31-Improving-the-Federal-Governments-Investigative-and-Remediation-Capabilities-Related-to-Cybersecurity-Incidents.pdf). + +### 2.17.2 Resources + +- The “Unified Audit Logging SHALL Be Enabled” section of the *Defender for + Office 365 Minimum Viable Secure Configuration Baseline*. + + +# Acknowledgements + +In addition to acknowledging the important contributions of a diverse +team of Cybersecurity and Infrastructure Security Agency (CISA) experts, +CISA thanks the following federal agencies and private sector +organizations that provided input during the development of the Secure +Business Cloud Application’s security configuration baselines in +response to Section 3 of [Executive Order (EO) 14028, *Improving the +Nation’s +Cybersecurity*](https://www.federalregister.gov/documents/2021/05/17/2021-10460/improving-the-nations-cybersecurity): + +- Consumer Financial Protection Bureau (CFPB) + +- Department of the Interior (DOI) + +- National Aeronautics and Space Administration (NASA) + +- Sandia National Laboratories (Sandia) + +- U.S. Census Bureau (USCB) + +- U.S. Geological Survey (USGS) + +- U.S. Office of Personnel Management (OPM) + +- U.S. Small Business Administration (SBA) + +The cross-agency collaboration and partnerships developed during this +initiative serve as an example for solving complex problems faced by the +federal government. + +**Cybersecurity Innovation Tiger Team (CITT) Leadership** + +Beau Houser (USCB), Sanjay Gupta (SBA), Michael Witt (NASA), James +Saunders (OPM), Han Lin (Sandia), Andrew Havely (DOI). + +**CITT Authors** + +Trafenia Salzman (SBA), Benjamin McChesney (OPM), Robert Collier (USCB), +Matthew Snitchler (Sandia), Darryl Purdy (USCB), Brandon Frankens +(NASA), Brandon Goss (NASA), Nicole Bogeajis (DOI/USGS), Kevin Kelly +(DOI), Adnan Ehsan (CFPB), Michael Griffin (CFPB), Vincent Urias +(Sandia), Angela Calabaza (Sandia). + +**CITT Contributors** + +Dr. Mukesh Rohatgi (MITRE), Lee Szilagyi (MITRE), Nanda Katikaneni +(MITRE), Ted Kolovos (MITRE), Thomas Comeau (MITRE), Karen Caraway +(MITRE), Jackie Whieldon (MITRE), Jeanne Firey (MITRE), Kenneth Myers +(General Services Administration). diff --git a/baselines/onedrive.md b/baselines/onedrive.md new file mode 100644 index 0000000000..b54c4fa911 --- /dev/null +++ b/baselines/onedrive.md @@ -0,0 +1,404 @@ +# 1. Introduction + +OneDrive for Business is a cloud-based file storage system with online +editing and collaboration tools for Microsoft Office documents and is +part of Office 365. OneDrive for Business facilitates synchronization +across multiple devices and enables secure, compliant, and intelligent +collaboration with multiple people. + +This security baseline applies guidance from industry benchmarks on how +to secure cloud solutions on Azure. + +## 1.1 Assumptions + +These baseline specifications assume that the agency is using OneDrive +for Business, not personal or school versions, and allowing access using +both OneDrive application sync and the browser-based client. + +It is also assumed that the agency will use Azure Active Directory to +authenticate accounts and authorize applications. + +The **License Requirements** sections of this document assume the +organization is using an [M365 +E3](https://www.microsoft.com/en-us/microsoft-365/compare-microsoft-365-enterprise-plans) +or [G3](https://www.microsoft.com/en-us/microsoft-365/government) +license level. Therefore, only licenses not included in E3/G3 are +listed. + +## 1.2 Resources + +**License Compliance and Copyright** + +Portions of this document are adapted from documents in Microsoft’s +[Microsoft +365](https://github.com/MicrosoftDocs/microsoft-365-docs/blob/public/LICENSE) +and +[Azure](https://github.com/MicrosoftDocs/azure-docs/blob/main/LICENSE) +GitHub repositories. The respective documents are subject to copyright +and are adapted under the terms of the Creative Commons Attribution 4.0 +International license. Source documents are linked throughout this +document. The United States Government has adapted selections of these +documents to develop innovative and scalable configuration standards to +strengthen the security of widely used cloud-based software services. + +# 2. Baseline + +## 2.1 Anyone Links SHOULD Be Turned Off + +Unauthenticated sharing (Anyone links) is used to share data without +authentication and users are free to pass it on to others outside the +agency. To prevent users from unauthenticated sharing of content, turn +off Anyone sharing for users outside the tenant when accessing content +in SharePoint, Groups, or Teams. + +### 2.1.1 Policy + +- Anyone links SHOULD be disabled. + +### 2.1.2 Resources + +- [Limit accidental exposure \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/solutions/share-limit-accidental-exposure?view=o365-worldwide) + +### 2.1.3 License Requirements + +- N/A + +### 2.1.4 Implementation + +**Note**: OneDrive settings can be more restrictive than the SharePoint +setting, but not more permissive. + +To turn off Anyone links for the agency: + +1. Open the **SharePoint admin center**. + +2. In the left-hand navigation pane, expand **Policies,** then select + [**Sharing**](https://go.microsoft.com/fwlink/?linkid=2185222). + +3. Set the SharePoint external sharing settings to **New and existing + guests**, then set OneDrive to **New and existing guests**. + +4. Click **Save**. + +To turn off Anyone links for a site: + +1. In the **SharePoint admin center** left navigation pane, expand + **Sites,** and select [**Active + sites**](https://go.microsoft.com/fwlink/?linkid=2185220). + +2. Select the site to configure. + +3. In the ribbon, select **Sharing**. + +4. Ensure that **Sharing** is set to **New and existing guests**. + +5. Click **Save**. + +## 2.2 Expiration Date SHOULD Be Set for Anyone Links + +Files that are stored in SharePoint sites, Groups, and Teams for months +and years could lead to unexpected modifications to files if shared with +unauthenticated people. Configuring expiration times for Anyone links +can help avoid unwanted changes. If Anyone links are enabled, the +expiration date SHOULD be set to thirty days or as determined by mission +needs or agency policy. + +### 2.2.1 Policy + +- An expiration date SHOULD be set for Anyone links. + +- Expiration date SHOULD be set to thirty days. + +### 2.2.2 Resources + +- [Best practices for unauthenticated sharing \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/solutions/best-practices-anonymous-sharing?view=o365-worldwide) + +### 2.2.3 License Requirements + +- N/A + +### 2.2.4 Implementation + +To set an expiration date for Anyone links across the agency (**Note**: +Anyone links must be enabled). + +1. Open the **SharePoint admin center.** + +2. In the left-hand navigation pane, expand **Policies**, and then + select + [**Sharing**](https://go.microsoft.com/fwlink/?linkid=2185222). + +3. Under **Choose expiration and permissions options for Anyone + links**, select the **These links must expire within this many + days** check box. + +4. Enter the number of days in the box, and then click **Save**. + +To set an expiration date for Anyone links on a specific site: + +1. Open the **SharePoint admin center**, expand **Sites**, and then + select [**Active + sites**](https://go.microsoft.com/fwlink/?linkid=2185220). + +2. Select the site to change, and then select **Sharing**. + +3. Under **Advanced settings for Anyone links**, under **Expiration of + Anyone links**, clear the **Same as organization-level setting** + check box. + +4. Select the **These links must expire within this many days** option + and enter a number of days in the box. + +5. Click **Save**. + +## 2.3 Link Permissions SHOULD Be Set to Enabled Anyone Links to View + +The Anyone links default to allow people to edit files, as well as edit +and view files and upload new files to folders. To allow unauthenticated +sharing but keep unauthenticated people from modifying the agency's +content, consider setting the file and folder permissions to **View**. + +### 2.3.1 Policy + +- Anyone link permissions SHOULD be limited to View. + +### 2.3.2 Resources + +- [Set link permissions \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoft-365/solutions/best-practices-anonymous-sharing?view=o365-worldwide#set-link-permissions) + +### 2.3.3 License Requirements + +- N/A + +### 2.3.4 Implementation + +1. Open the **SharePoint admin center**. + +2. In the left-hand navigation pane, expand **Policies**, then select + **Sharing**. + +3. Under **Advanced settings for Anyone links**, set the file and + folder permissions to **View**. + +## 2.4 OneDrive Client SHALL Be Restricted to Windows for Agency-Defined Domain(s) + +Configuring OneDrive to sync only to agency-defined domains ensures that +users can only sync to agency-managed computers. + +### 2.4.1 Policy + +- OneDrive Client for Windows SHALL be restricted to agency-Defined + Domain(s). + +### 2.4.2 Resources + +- [Allow syncing only on computers joined to specific domains – OneDrive + \| Microsoft + Docs](https://docs.microsoft.com/en-us/onedrive/allow-syncing-only-on-specific-domains) + +### 2.4.3 License Requirements + +- N/A + +### 2.4.4 Implementation + +1. Open the **SharePoint admin center.** + +2. In the left-hand navigation pane, select **Settings** and sign in + with an account that has [admin + permissions](https://docs.microsoft.com/en-us/sharepoint/sharepoint-admin-role) + for the agency. + +3. Select **Sync**. + +4. Select the **Allow syncing only on computers joined to specific + domains** check box. + +5. Add the [Globally Unique Identifier (GUID) of each + domain](https://docs.microsoft.com/en-us/powershell/module/activedirectory/get-addomain?view=windowsserver2022-ps) for + the member computers that the agency wants to be able to sync. + +**Note:** Add the domain GUID of the computer domain membership. If +users are in a separate domain, only the domain GUID that the computer +account is joined to is required. + +**Important:** This setting is only applicable to Active Directory +domains. It does not apply to Azure Active Directory (AAD) domains. If +agency devices are only Azure AD joined, consider using a [Conditional +Access Policy](https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/overview) +instead. + +6. Click **Save**. + +## 2.5 OneDrive Client SHALL Be Restricted to Sync with Mac for Agency-Defined Devices + +Set restrictions on whether users can sync items to non-domain joined +machines, control the list of allowed domains, and manage whether Mac +clients (which do not support domain join) can sync. + +### 2.5.1 Policy + +- OneDrive Client Sync SHALL only be allowed only within the local + domain. + +### 2.5.2 Resources + +- [Set-SPOTenantSyncClientRestriction (SharePointOnlinePowerShell) \| + Microsoft + Docs](https://docs.microsoft.com/en-us/powershell/module/sharepoint-online/set-spotenantsyncclientrestriction?view=sharepoint-ps#:~:text=In%20order%20to%20explicitly%20block%20Microsoft%20OneDrive%20client,cmdlet%20with%20the%20BlockMacSync%20parameter%20set%20to%20true.?msclkid=f80f95c5c4c611ecac7de0980370f33c) + +### 2.5.3 License Requirements + +- N/A + +### 2.5.4 Implementation + +The `Set-SPOTenantSyncClientRestriction` cmdlet can be used to enable +the feature for tenancy and set the domain GUIDs in the safe recipients +list. When this feature is enabled, it can take up to 24 hours for the +change to take effect. However, any changes to the safe domains list are +reflected within five minutes. + +`Set-SPOTenantSyncClientRestriction -Enable -DomainGuids +"786548DD-877B-4760-A749-6B1EFBC1190A; +877564FF-877B-4760-A749-6B1EFBC1190A" -BlockMacSync:$false` + +## 2.6 OneDrive Client Sync SHALL Only Be Allowed Within the Local Domain + +Configuring OneDrive to sync only to agency-defined domains ensures that +users can only sync to agency-managed computers. + +### 2.6.1 Policy + +- OneDrive Client Sync SHALL be restricted to the local domain. + +### 2.6.2 Resources + +- [Allow syncing only on computers joined to specific domains \| + Microsoft + Documents](https://docs.microsoft.com/en-us/onedrive/allow-syncing-only-on-specific-domains) + +### 2.6.3 License Requirements + +- N/A + +### 2.6.4 Implementation + +1. Open the **SharePoint admin center**. + +2. In the left-hand navigation pane, select **Settings**. + +3. Next to **OneDrive**, click **Sync** to display synchronization + settings. + +4. On the **Sync settings** page, confirm that **Allow syncing only on + computers joined to specific domains** is checked, and that a domain + GUID displays in the box below it. + +## 2.7 Legacy Authentication SHALL Be Blocked + +Modern authentication, based on Active Directory Authentication Library +(ADAL) and Open Authorization 2 (OAuth2), is a critical component of +security in Office 365. It provides the device authentication and +authorization capability of Office 365, which is a foundational security +component. If modern authentication is not required, this creates a +loophole that could allow unauthorized devices to connect to OneDrive +and download/exfiltrate enterprise data. For this reason, it is +important to make sure that only apps that support modern authentication +are allowed to connect, assuring that only authorized devices are +allowed to access enterprise data. + +### 2.7.1 Policy + +- Legacy Authentication SHALL be blocked. + +### 2.7.2 Resources + +- [Control access from unmanaged devices \| Microsoft + Documents](https://docs.microsoft.com/en-us/sharepoint/control-access-from-unmanaged-devices) + +### 2.7.3 License Requirements + +- N/A + +### 2.7.4 Implementation + +1. Open the **SharePoint admin center**. + +2. In the left-hand navigation pane, click **Policies** \> **Access + Control** \> **Device access**. + +3. Click **Apps that don’t use modern authentication** to display the + device access settings. + +4. On the **Apps that don’t use modern authentication** page, select + the **Block access** option. + +# Acknowledgements + +In addition to acknowledging the important contributions of a diverse +team of Cybersecurity and Infrastructure Security Agency (CISA) experts, +CISA thanks the following federal agencies and private sector +organizations that provided input during the development of the Secure +Business Cloud Application’s security configuration baselines in +response to Section 3 of [Executive Order (EO) 14028, *Improving the +Nation’s +Cybersecurity*](https://www.federalregister.gov/documents/2021/05/17/2021-10460/improving-the-nations-cybersecurity): + +- Consumer Financial Protection Bureau (CFPB) + +- Department of the Interior (DOI) + +- National Aeronautics and Space Administration (NASA) + +- Sandia National Laboratories (Sandia) + +- U.S. Census Bureau (USCB) + +- U.S. Geological Survey (USGS) + +- U.S. Office of Personnel Management (OPM) + +- U.S. Small Business Administration (SBA) + +The cross-agency collaboration and partnerships developed during this +initiative serve as an example for solving complex problems faced by the +federal government. + +**Cybersecurity Innovation Tiger Team (CITT) Leadership** + +Beau Houser (USCB), Sanjay Gupta (SBA), Michael Witt (NASA), James +Saunders (OPM), Han Lin (Sandia), Andrew Havely (DOI). + +**CITT Authors** + +Trafenia Salzman (SBA), Benjamin McChesney (OPM), Robert Collier (USCB), +Matthew Snitchler (Sandia), Darryl Purdy (USCB), Brandon Frankens +(NASA), Brandon Goss (NASA), Nicole Bogeajis (DOI/USGS), Kevin Kelly +(DOI), Adnan Ehsan (CFPB), Michael Griffin (CFPB), Vincent Urias +(Sandia), Angela Calabaza (Sandia). + +**CITT Contributors** + +Dr. Mukesh Rohatgi (MITRE), Lee Szilagyi (MITRE), Nanda Katikaneni +(MITRE), Ted Kolovos (MITRE), Thomas Comeau (MITRE), Karen Caraway +(MITRE), Jackie Whieldon (MITRE), Jeanne Firey (MITRE), Kenneth Myers +(General Services Administration). + +# Configuring On-Premises Devices + +##### Limit Syncing to Agency-defined Equipment within the Agency (Tenants) + +OneDrive includes a sync client that allows users to synchronize their +files from the OneDrive cloud service to their desktop/laptop computer. +This allows them to interact with a local copy of the files in a way +that is very similar to working with regular local files on their +computer. + +**Resources** + +[Use OneDrive policies to control sync settings - OneDrive \| Microsoft +Docs](https://docs.microsoft.com/en-us/onedrive/use-group-policy#allow-syncing-onedrive-accounts-for-only-specific-organizations) diff --git a/baselines/powerbi.md b/baselines/powerbi.md new file mode 100644 index 0000000000..0259e6770d --- /dev/null +++ b/baselines/powerbi.md @@ -0,0 +1,1022 @@ +# 1. Introduction + +Power BI is a Software as a Service (SaaS) offering from Microsoft that +facilitates self-service business intelligence dashboards, reports, +datasets, and visualizations. Power BI can connect to multiple, +different data sources, combine and shape data from those connections, +then create reports and dashboards to share with others. + +The Power BI service is built on Azure. The Power BI service +architecture is based on two clusters: the Web Front End (WFE) cluster +and the Back-End cluster. End users neither control nor have visibility +into these underlying clusters, as they are part of the underlying SaaS +architecture. The WFE cluster manages the initial connection and +authentication to the Power BI service, and once authenticated, the +Back-End handles all subsequent user interactions. Power BI uses Azure +Active Directory (AAD) to store and manage user identities and manages +the storage of data and metadata using Azure Binary Large Object (BLOB) +and Azure Structured Query Language (SQL) Database, respectively. (For +additional detail, please refer to the [Power BI +Security](https://docs.microsoft.com/en-us/power-bi/enterprise/service-admin-power-bi-security) +documentation page.) + +## 1.1 Scope + +This baseline focuses on the Power BI SaaS service that comes integrated +with Microsoft 365, noting that there is also a desktop version of Power +BI that can be installed locally. Users who are developing business +intelligence products and analytics in Power BI desktop can push content +to either the Power BI Report Server or to the Power BI SaaS service +previously described. If required for a given environment or use case, a +separate Power BI desktop baseline with tailored security requirements +and considerations should be developed by security and end user +operations staff. + +## 1.2 Resources + +**License Compliance and Copyright** + +Portions of this document are adapted from documents in Microsoft’s +[Microsoft +365](https://github.com/MicrosoftDocs/microsoft-365-docs/blob/public/LICENSE) +and +[Azure](https://github.com/MicrosoftDocs/azure-docs/blob/main/LICENSE) +GitHub repositories. The respective documents are subject to copyright +and are adapted under the terms of the Creative Commons Attribution 4.0 +International license. Source documents are linked throughout this +document. The United States Government has adapted selections of these +documents to develop innovative and scalable configuration standards to +strengthen the security of widely used cloud-based software services. + +## 1.3 Assumptions + +Agencies using Power BI have a data classification scheme in place for + the data entering Power BI. + +- Agencies may connect more than one data source to their Power BI + tenant. + +- All data sources use a secure connection for data transfer to and from + the Power BI tenant; the agency disallows non-secure connections. + +- The **License Requirements** sections of this document assume the + organization is using an [M365 + E3](https://www.microsoft.com/en-us/microsoft-365/compare-microsoft-365-enterprise-plans) + or [G3](https://www.microsoft.com/en-us/microsoft-365/government) + license level. Therefore, only licenses not included in E3/G3 are + listed. Additionally, M365 G5 is required for Power BI Pro. Power BI Premium is available as an add-on to G5 and provides + dedicated capacity-based BI, self-service data prep for big data, and + simplification of data management and access at enterprise scale. + + +# 2. Baseline + +## 2.1 External Sharing SHOULD be Disabled + +External sharing can represent a potential security risk, therefore, +disabling it is a best practice unless specific, approved use cases make +it a necessity. As with the other collaboration capabilities within the +Power BI tenant, the agency must evaluate whether its use case requires +allowing external sharing. + +When sharing with users outside an agency, the external users receive an +email with a link to the shared report or dashboard. The recipient must +sign into Power BI to view the shared content. + +After the shared-to user signs into the Power BI service, they see the +shared report or dashboard in its own browser window, not in the usual +Power BI portal. + +People outside the agency can't edit content in the shared report or +dashboard. They can interact with the charts and change filters or +slicers, but changes are not saved. + +Only direct recipients see the shared report or dashboard. For example, +if a sharing invite is sent to powerbiuser1@contoso.com, only +powerbiuser1 sees the dashboard. No other user can see the dashboard, +even if powerbiuser1 forwards them the link. Powerbiuser1 must use the +same email address to access it: if powerbiuser1 signs in with any other +email address, they will not have access to the dashboard. + +People outside the tenant agency cannot see any data if role or +row-level security is implemented on on-premises Analysis Services +tabular models. + +### 2.1.1 Policy + +- External sharing SHOULD be disabled unless the agency mission requires + the capability. + +- If external sharing is deemed appropriate, the agency SHOULD limit the + sharing ability to a security group instead of the entire agency. + +### 2.1.2 Resources + +- [Power BI Tenant settings \| Microsoft + Docs](https://docs.microsoft.com/en-us/power-bi/guidance/admin-tenant-settings) + +### 2.1.3 License Requirements + +- N/A + +### 2.1.4 Implementation + +1. In the **Power BI tenant admin portal**, go to **Export and Sharing + Settings.** + +2. Disable the **External sharing** toggle. + +3. If the agency approves external sharing, select a specific security + group that includes users who should be able to share data and + reports externally. + +## 2.2 Publish to Web SHOULD be Disabled + +Power BI has a capability to publish reports and content to the web. +This capability creates a publicly accessible web URL that does not +require authentication or status as an AAD user to view it. While this +may be needed for a specific use case or collaboration scenario, it is a +best practice to keep this setting off by default to prevent unintended +and potentially sensitive data exposure. + +If it is deemed necessary to make an exception and enable the feature, +admins should limit the ability to publish to the web to only specific +security groups, instead of allowing the entire agency to publish data +to the web. + +### 2.2.1 Policy + +- The Publish to Web feature SHOULD be disabled unless the agency + mission requires the capability. + +### 2.2.2 Resources + +- [Power BI Tenant settings \| Microsoft + Docs](https://docs.microsoft.com/en-us/power-bi/guidance/admin-tenant-settings) + +- [Power BI Security Baseline v2.0 \| Microsoft benchmarks GitHub + repo](https://github.com/MicrosoftDocs/SecurityBenchmarks/blob/master/Azure%20Offer%20Security%20Baselines/2.0/power-bi-security-baseline-v2.0.xlsx) + +### 2.2.3 License Requirements + +- N/A + +### 2.2.4 Implementation + +***Confirm Publish to web is disabled*** + +The **Publish to web** setting in the admin portal gives options for +users to create embed codes. It is recommended that agencies disallow +publishing to the web pending further justification reviews by +information security. + +1. Administrators can set **Publish to web** to **Disabled.** + +2. However, if **Publish to web** is set to **enabled**, admins + can **Choose how embed codes work** to **Allow only existing embed + codes**. In that case, users can create embed codes, but they must + contact the tenant’s Power BI admin to allow them to do so. + +## 2.3 Power BI Guest Access SHOULD be Disabled + +A best practice is to disallow guest user access. Disallowing guest +access also aligns with zero trust principles. The agency with +potentially shareable Power BI resources and data in its tenant must +evaluate its unique sharing requirements and whether an exception should +be granted to allow external guests to access content in the agency’s +tenant. + +Enabling this setting allows AAD Business-to-Business (AAD B2B) guest +users to access Power BI. If this setting is disabled, guest users +receive an error when trying to access Power BI. Disabling this setting +for the entire agency also prevents users from inviting guests to the +agency. Using the specific security groups option allows admins to +control which guest users can access Power BI. + +The types of users are defined as follows (**Note**: these terms vary in +use across Microsoft documentation): + +- **Internal users**: members of the agency’s M365 tenant. + +- **External users**: members of a different M365 tenant. + +- **Business to Business (B2B) guest users**: external users that are + formally invited to view and/or edit Power BI workspace content and + are added to the agency’s AAD as guest users. These users authenticate + with their home organization/tenant and are granted access to Power BI + content by virtue of being listed as guest users in the tenant’s AAD. + +**Note**: Guest users are subject to restrictions to their experience +that are controlled by the AAD administrator. If the Power BI tenant’s +guest users will need to own and share Power BI content with others and +manage workspaces as Power BI workspace Admins, Microsoft recommends +changing the **Guest user permissions are limited** setting in AAD to +allow these users to use people pickers within the Power BI UX. Since +Power BI integrates natively with AAD, the AAD Baseline should be +consulted for additional guidance on managing guest users. + +### 2.3.1 Policy + +- Guest user access to the Power BI tenant SHOULD be disabled unless the + agency mission requires the capability. + +### 2.3.2 Resources + +- [Power BI Tenant settings \| Microsoft + Docs](https://docs.microsoft.com/en-us/power-bi/guidance/admin-tenant-settings) + +- [Power BI Security Baseline v2.0 \| Microsoft benchmarks GitHub + repo](https://github.com/MicrosoftDocs/SecurityBenchmarks/blob/master/Azure%20Offer%20Security%20Baselines/2.0/power-bi-security-baseline-v2.0.xlsx) + +### 2.3.3 License Requirements + +- N/A + +### 2.3.4 Implementation + +1. In the **tenant admin portal**, go to **Export and Sharing + Settings**. + +2. Disable the **Allow Azure Active Directory guest users to access + Power BI** toggle. + +## 2.4 External Invitations SHOULD be Disabled + +This setting controls whether Power BI allows inviting external users to +the agency’s organization through Power BI’s sharing workflows and +experiences. After an external user accepts the invite, they become an +AAD B2B guest user in the organization. They will then appear in user +pickers throughout the Power BI user experience. + +If this setting is disabled: + +- Existing guest users in the tenant organization continue to have + access to any items they already had access to and continue to be + listed in user picker experiences. + +- An external user who is not already a guest user in the agency cannot + be added to the agency through Power BI. + +For maintaining least privilege, a best practice is to disable this +setting unless dictated by the mission need. + +**Note**: To invite external users to the tenant, a user also needs the +AAD Guest Inviter role. The setting in this baseline statement only +controls the ability to invite guest users through Power BI. See the +*AAD Minimum Viable Secure Configuration Baseline* for more information +on roles. + +### 2.4.1 Policy + +- The **Invite external users to your organization** feature SHOULD be + disabled unless agency mission requires the capability. + +### 2.4.2 Resources + +- [Power BI Tenant settings \| Microsoft + Docs](https://docs.microsoft.com/en-us/power-bi/guidance/admin-tenant-settings) + +- [Distribute Power BI content to external guest users with AAD B2B \| + Microsoft + Docs](https://docs.microsoft.com/en-us/power-bi/enterprise/service-admin-azure-ad-b2b) + +- [Power BI Security Baseline v2.0 \| Microsoft benchmarks GitHub + repo](https://github.com/MicrosoftDocs/SecurityBenchmarks/blob/master/Azure%20Offer%20Security%20Baselines/2.0/power-bi-security-baseline-v2.0.xlsx) + +### 2.4.3 License Requirements + +- N/A + +### 2.4.4 Implementation + +1. In the tenant admin portal, go to **Export and Sharing Settings**. + +2. Disable the **Invite external users to your organization** toggle. + +## 2.5 The External Editing Capability SHOULD be Disabled + +It is possible to give external guest users the ability to edit and +manage Power BI content; however, this could have considerable data +security implications. + +Microsoft notes that Power BI comes with this setting disabled. + +If there is a mission need to allow external users to edit and manage +Power BI content, the recommended best practice is to assign these +entities to a security group. + +### 2.5.1 Policy + +- The external editing capability SHOULD be disabled unless agency + mission requires the capability. + +### 2.5.2 Resources + +- [Power BI Tenant settings \| Microsoft + Docs](https://docs.microsoft.com/en-us/power-bi/guidance/admin-tenant-settings) + +- [Azure AD B2B Guest users can now edit and manage content in Power BI + to collaborate better across organizations \| Microsoft + Docs](https://powerbi.microsoft.com/en-us/blog/azure-ad-b2b-guest-users-can-now-edit-and-manage-content-in-power-bi-to-collaborate-better-across-organizations/) + +- [Power BI Security Baseline v2.0 \| Microsoft benchmarks GitHub + repo](https://github.com/MicrosoftDocs/SecurityBenchmarks/blob/master/Azure%20Offer%20Security%20Baselines/2.0/power-bi-security-baseline-v2.0.xlsx) + +### 2.5.3 License Requirements + +If this setting is enabled, an AAD B2B guest user must have a Power BI +Pro license in a workspace other than the “My workspace” area to edit +and manage content within the inviting organization’s Power BI tenant. + +### 2.5.4 Implementation + +1. In the tenant admin portal, go to **Export and Sharing Settings**. + +2. Disable the toggle labeled **Allow Azure Active Directory guest + users to edit and manage content in the organization**. + +## 2.6 Service Principals SHALL be Allowed to be Used to Securely Manage Application Identities + +Power BI supports the use of service principals to manage application +identities. Service principals can use application programming +interfaces (APIs) to access tenant-level features, which are controlled +by Power BI service admins and enabled for the entire agency or for +agency security groups. Access of service principals can be controlled +by creating dedicated security groups for them and using these groups in +any Power BI tenant level-settings. If service principals are employed +for Power BI, it is recommended that service principal credentials used +for encrypting or accessing Power BI be stored in a Key Vault, with +properly assigned access policies and regularly reviewed access +permissions. + +**Several high-level use cases for service principals:** + +- Power BI interactions with data sources. There will be some cases + where a service principal is not possible from Power BI to a data + source (e.g., Azure Table Storage). + +- A user’s service principal for accessing the Power BI Service (e.g., + app.powerbi.com, app.powerbigov.us). + +- Power BI Embedded and other users of the Power BI REST APIs to + interact with PBI content. + +**Best Practices for Service Principals:** + +- Evaluate whether certificates or secrets are a more secure option for + the implementation. Note that Microsoft recommends certificates over + secrets. + +- Use the principle of least privilege in implementing service + principals; only provide the ability to create app registrations to + entities that require it. + +- Instead of enabling service principals for the entire agency, + implement for a dedicated security group. + +### 2.6.1 Policy + +- Service Principals SHOULD be allowed for Power BI where applicable. + +- Service Principal credentials used for encrypting or accessing Power + BI SHALL NOT be stored in scripts or config files and SHALL be stored + in a secure vault such as Azure Key Vault. + +### 2.6.2 Resources + +- [Automate Premium workspace and dataset tasks with service principal + \| Microsoft + Docs](https://docs.microsoft.com/en-us/power-bi/admin/service-premium-service-principal) + +- [Embed Power BI content with service principal and an application + secret \| Microsoft + Docs](https://docs.microsoft.com/en-us/power-bi/developer/embedded/embed-service-principal) + +- [Embed Power BI content with service principal and a certificate \| + Microsoft + Docs](https://docs.microsoft.com/en-us/power-bi/developer/embedded/embed-service-principal-certificate) + +- [Enable service principal authentication for read-only admin APIs \| + Microsoft + Docs](https://docs.microsoft.com/en-us/power-bi/admin/read-only-apis-service-principal-authentication) + +- [Microsoft Power BI Embedded Developer Code Samples \| Microsoft + GitHub](https://github.com/microsoft/PowerBI-Developer-Samples/blob/master/Python/Encrypt%20credentials/README.md) + +- [Microsoft Power BI Security Baseline, Baseline Statement IM-2 \| + Microsoft + Docs](https://docs.microsoft.com/en-us/security/benchmark/azure/baselines/power-bi-security-baseline) + +### 2.6.3 License Requirements + +- N/A + +### 2.6.4 Implementation + +Standardize on a single authoritative identity and access management +source (note that AAD integrates natively for Power BI). + +1. In the **tenant settings** + +2. Confirm that service principals are enabled in the **Allow service principals to use Power BI APIs** option. + +3. Confirm that the service principal is restricted to a security group +related to Power BI, rather than open to the entire agency. (Note: +Service principals have access to any tenant settings for which +they are enabled. Depending on the agency’s admin settings, this +includes specific security groups or the entire agency.) + +To restrict service principal access to specific tenant settings, it is +recommended to allow access only to specific security groups. +Alternatively, one can create a dedicated security group for service +principals and exclude it from the desired tenant settings. + +## 2.7 ResourceKey Authentication SHOULD be Blocked + +This setting pertains to the security and development of Power BI +embedded content. The Power BI tenant states that “for extra security, +block using resource key-based authentication.” This baseline statement +recommends, but does not mandate, setting ResourceKey-based +authentication to the blocked state. + +For streaming datasets created using the Power BI service user +interface, the dataset owner receives a URL that includes a resource +key. This key authorizes the requestor to push data into the dataset +without using an AAD OAuth bearer token, so please keep in mind the +implications of having a secret key in the URL when working with this +type of dataset and method. + +This setting applies to streaming and PUSH datasets. If Resource +Key-based authentication is blocked, users will not be allowed send data +to streaming and PUSH datasets using the API with a resource key. +However, if developers have an approved need to leverage this feature, +an exception to the policy can be investigated. + +### 2.7.1 Policy + +- ResourceKey Authentication SHOULD be blocked unless a specific use + case (e.g., streaming and/or PUSH datasets) merits its use. + +### 2.7.2 Resources + +- [Power BI Tenant settings \| Microsoft + Docs](https://docs.microsoft.com/en-us/power-bi/guidance/admin-tenant-settings) + +- [Real-time streaming in Power BI \| Microsoft + Docs](https://docs.microsoft.com/en-us/power-bi/connect-data/service-real-time-streaming) + +### 2.7.3 License Requirements + +- N/A + +### 2.7.4 Implementation + +1. Under **Developer Settings** in the Power BI tenant admin portal, + toggle **Block ResourceKey Authentication** to an enabled state. + +## 2.8 Python and R Visual Sharing SHOULD be Disabled + +Power BI can interact with Python and R scripts to integrate +visualizations from these languages. Python visuals are created from +Python scripts, which could contain code with security or privacy risks. +When attempting to view or interact with a Python visual for the first +time, a user is presented with a security warning message. Python and R +visuals should only be enabled if the author and source are trusted, or +after a code review of the Python/R script(s) in question is conducted +and deems the scripts free of security risks. + +### 2.8.1 Policy + +- R and Python interactions SHOULD be disabled. + +### 2.8.2 Resources + +- [Power BI Visuals and Python \| Microsoft + Docs](https://docs.microsoft.com/en-us/power-bi/connect-data/desktop-python-visuals) + +### 2.8.3 License Requirements + +- N/A + +### 2.8.4 Implementation + +1. In the **Power BI tenant**, go to **R and Python Visuals Settings**. + +2. Toggle off the **Interact with and share R and Python visuals** + option. + +## 2.9 Data Stewards and Power BI Admins SHOULD Discover, Classify, and Label Sensitive Data + +There are multiple ways to secure sensitive information, such as warning +users, encryption, or blocking attempts to share. Use Microsoft +Information Protection sensitivity labels on Power BI reports, +dashboards, datasets, and dataflows to guard sensitive content against +unauthorized data access and leakage. This can also guard against +unwanted aggregation and commingling. + +**Note**: At this baseline’s time of writing, data loss prevention (DLP) +profiles are in preview status for Power BI. Once released for general +availability and government, DLP profiles represent another available +tool for securing power Power BI datasets. Refer to the *Defender for +Office 365 Minimum Viable Secure Configuration Baseline* for more on +DLP. + +### 2.9.1 Policy + +- Sensitivity labels SHOULD be enabled for Power BI and employed for + sensitive data per enterprise data protection policies. + +### 2.9.2 Resources + +- [Enable sensitivity labels in Power BI \| Microsoft + Docs](https://docs.microsoft.com/en-us/power-bi/admin/service-security-enable-data-sensitivity-labels) + +- [Data loss prevention policies for Power BI + (preview)](https://docs.microsoft.com/en-us/power-bi/enterprise/service-security-dlp-policies-for-power-bi) + +- [Data Protection in Power + BI](https://docs.microsoft.com/en-us/power-bi/enterprise/service-security-data-protection-overview) + +- [Power BI Security Baseline v2.0 \| Microsoft benchmarks GitHub + repo](https://github.com/MicrosoftDocs/SecurityBenchmarks/blob/master/Azure%20Offer%20Security%20Baselines/2.0/power-bi-security-baseline-v2.0.xlsx) + +### 2.9.3 License Requirements + +- An Azure Information Protection Premium P1 or Premium P2 license is + required to apply or view Microsoft Information Protection sensitivity + labels in Power BI. Azure Information Protection can be purchased + either standalone or through one of the Microsoft licensing suites. + See [Azure Information Protection + pricing](https://azure.microsoft.com/services/information-protection/) for + detail. + +- Azure Information Protection sensitivity labels need to be migrated to + the Microsoft Information Protection Unified Labeling platform to be + used in Power BI. + +- To be able to apply labels to Power BI content and files, a user must + have a Power BI Pro or Premium Per User (PPU) license in addition to + one of the previously mentioned Azure Information Protection licenses. + +- Before enabling sensitivity labels on the agency’s tenant, make sure + that sensitivity labels have been defined and published for relevant + users and groups. See [Create and configure sensitivity labels and + their + policies](https://docs.microsoft.com/en-us/microsoft-365/compliance/create-sensitivity-labels) + for detail. + +### 2.9.4 Implementation + +**Enable Sensitivity Labels in Power BI:** + +Sensitivity labels must be enabled on the tenant before they can be used +in both the Power BI service and in Desktop. This section describes how +to enable them in the tenant settings. + +To enable sensitivity labels on the tenant: + +1. Navigate to the **Power BI Admin portal**-\>**Tenant + settings** pane-\> **Information protection** section. + +2. In the **Information Protection** section, perform the following + steps: + +3. Open **Allow users to apply sensitivity labels for Power BI + content**. + +4. Enable the toggle. + +5. Define who can apply and change sensitivity labels in Power BI + assets. By default, everyone in the agency will be able to apply + sensitivity labels; however, one can choose to enable setting + sensitivity labels only for specific users or security groups. With + either the entire agency or specific security groups selected, one + can exclude specific subsets of users or security groups. + +6. When sensitivity labels are enabled for the entire agency, + exceptions are typically security groups. + +7. When sensitivity labels are enabled only for specific users or + security groups, exceptions are typically specific users. + +This approach makes it possible to prevent certain users from applying +sensitivity labels in Power BI, even if they belong to a group that +has permissions to do so. + +8. Click **Apply**. + +## 2.10 Audit Logs SHALL be Enabled in Power BI Tenant + +The Power BI tenant has a setting for audit log generation to monitor +internal activity and compliance. Users within the agency can use +auditing to monitor actions taken in Power BI by other users in the +agency. Power BI audit logs are always available for tenants that have +enabled recording user and admin activity in the Office 365 Admin +Portal, in which case this setting appears enabled but greyed out in the +tenant settings. + +### 2.10.1 Policy + +- Power BI audit log generation SHALL be enabled in the Power BI tenant. + +### 2.10.2 Resources + +- [Power BI Tenant settings \| Microsoft + Docs](https://docs.microsoft.com/en-us/power-bi/guidance/admin-tenant-settings) + +- [National Cybersecurity Protection System-Cloud Interface Reference + Architecture Volume + 1](https://www.cisa.gov/sites/default/files/publications/NCPS%20Cloud%20Interface%20RA%20Volume%20One%20%282021-05-14%29.pdf) + +- [National Cybersecurity Protection System - Cloud Interface Reference + Architecture Volume + 2](https://www.cisa.gov/sites/default/files/publications/NCPS%20Cloud%20Interface%20RA%20Volume%20Two%202021-06-11%20%28508%20COMPLIANT%29.pdf) + +- [Power BI Security Baseline v2.0 \| Microsoft benchmarks GitHub + repo](https://github.com/MicrosoftDocs/SecurityBenchmarks/blob/master/Azure%20Offer%20Security%20Baselines/2.0/power-bi-security-baseline-v2.0.xlsx) + +### 2.10.3 License Requirements + +- N/A + +### 2.10.4 Implementation + +1. In the **Power BI tenant**, go to **Audit and Usage Settings.** + +2. Enable **Create audit logs for internal activity auditing and + compliance**. This setting may already be enabled and greyed out if + audit recording has been turned on in the M365 admin portal. + + +# Acknowledgements + +In addition to acknowledging the important contributions of a diverse +team of Cybersecurity and Infrastructure Security Agency (CISA) experts, +CISA thanks the following federal agencies and private sector +organizations that provided input during the development of the Secure +Business Cloud Application’s security configuration baselines in +response to Section 3 of [Executive Order (EO) 14028, *Improving the +Nation’s +Cybersecurity*](https://www.federalregister.gov/documents/2021/05/17/2021-10460/improving-the-nations-cybersecurity): + +- Consumer Financial Protection Bureau (CFPB) + +- Department of the Interior (DOI) + +- National Aeronautics and Space Administration (NASA) + +- Sandia National Laboratories (Sandia) + +- U.S. Census Bureau (USCB) + +- U.S. Geological Survey (USGS) + +- U.S. Office of Personnel Management (OPM) + +- U.S. Small Business Administration (SBA) + +The cross-agency collaboration and partnerships developed during this +initiative serve as an example for solving complex problems faced by the +federal government. + +**Cybersecurity Innovation Tiger Team (CITT) Leadership** + +Beau Houser (USCB), Sanjay Gupta (SBA), Michael Witt (NASA), James +Saunders (OPM), Han Lin (Sandia), Andrew Havely (DOI). + +**CITT Authors** + +Trafenia Salzman (SBA), Benjamin McChesney (OPM), Robert Collier (USCB), +Matthew Snitchler (Sandia), Darryl Purdy (USCB), Brandon Frankens +(NASA), Brandon Goss (NASA), Nicole Bogeajis (DOI/USGS), Kevin Kelly +(DOI), Adnan Ehsan (CFPB), Michael Griffin (CFPB), Vincent Urias +(Sandia), Angela Calabaza (Sandia). + +**CITT Contributors** + +Dr. Mukesh Rohatgi (MITRE), Lee Szilagyi (MITRE), Nanda Katikaneni +(MITRE), Ted Kolovos (MITRE), Thomas Comeau (MITRE), Karen Caraway +(MITRE), Jackie Whieldon (MITRE), Jeanne Firey (MITRE), Kenneth Myers +(General Services Administration). + +# Implementation Considerations + +## Information Protection Considerations + +Several best practices and approaches are available to protect sensitive +data in Power BI. + +- Leverage sensitivity labels via Microsoft Information Protection. + +- Power BI allows service users to bring their own key to protect data + at rest. + +- Customers have the option to keep data sources on-premises and + leverage Direct Query or Live Connect with an on-premises data gateway + to minimize data exposure to the cloud service. + +- Implement Row Level Security in Power BI datasets. + +**Implementation Steps:** + +**Apply sensitivity labels from data sources to their data in Power BI** + +When this setting is enabled, Power BI datasets that connect to +sensitivity-labeled data in supported data sources can inherit those +labels, so the data remains classified and secure when brought into +Power BI. For details about sensitivity label inheritance from data +sources, see Sensitivity label inheritance from data sources (preview). + +***To enable sensitivity label inheritance from data sources:*** + +1. Navigate to the Power BI tenant settings. + +2. Select **Information protection** -\> **Apply sensitivity labels + from data sources to their data in Power BI (preview).** + +3. Enable **Restrict content with protected labels from being shared + via link with everyone in your agency**. + +When this setting is enabled, users can't generate a sharing link for +people in the agency for content with protection settings in the +sensitivity label. + +Sensitivity labels with protection settings include encryption or +content markings. For example, the agency may have a "Highly +Confidential" label that includes encryption and applies a "Highly +Confidential" watermark to content with this label. Therefore, when this +tenant setting is enabled and a report has a sensitivity label with +protection settings, then users can't create sharing links for people in +the agency. + +**Information Protection Prerequisites Specific to Power BI** + +- An Azure Information Protection Premium P1 or Premium P2 license is + required to apply or view Microsoft Information Protection sensitivity + labels in Power BI. Azure Information Protection can be purchased + either standalone or through one of the Microsoft licensing suites. + See [Azure Information Protection + pricing](https://azure.microsoft.com/services/information-protection/) for + detail. + +- Azure Information Protection sensitivity labels need to be migrated to + the Microsoft Information Protection Unified Labeling platform in + order for them to be used in Power BI. + +- To be able to apply labels to Power BI content and files, a user must + have a Power BI Pro or Premium Per User (PPU) license in addition to + one of the previously mentioned Azure Information Protection licenses. + +- Before enabling sensitivity labels on the agency’s tenant, make sure + that sensitivity labels have been defined and published for relevant + users and groups. See [Create and configure sensitivity labels and + their + policies](https://docs.microsoft.com/en-us/microsoft-365/compliance/create-sensitivity-labels) + for detail. + +**High-Level Steps to Use Bring Your Own Key (BYOK) Feature in Power +BI** + +First, confirm having the latest Power BI Management cmdlet. Install the +latest version by running Install-Module -Name MicrosoftPowerBIMgmt. +More information about the Power BI cmdlet and its parameters is +available in [Power BI PowerShell cmdlet +module](https://docs.microsoft.com/en-us/powershell/power-bi/overview?view=powerbi-ps). + +Follow steps in Bring Your Own (encryption) Keys for Power BI \| +Microsoft Docs. + +**Row Level Security Implementation** + +Row Level Security (RLS) involves several configuration steps, which +should be completed in the following order. + +1. Create a report in Microsoft Power BI Desktop. + +2. Import the data. + +3. Confirm the data model between both tables. + +4. Create the report visuals. + +5. Create RLS roles in Power BI Desktop by using DAX. + +6. Test the roles in Power BI Desktop. + +7. Deploy the report to Microsoft Power BI service. + +8. Add members to the role in Power BI service. + +9. Test the roles in Power BI service. + +- Reference Microsoft Power BI documentation for additional detail on + [Row Level Security + configuration](https://docs.microsoft.com/en-us/power-bi/enterprise/service-admin-rls). + +**Related Resources** + +- [Sensitivity labels in Power BI \| Microsoft + Docs](https://docs.microsoft.com/en-us/power-bi/admin/service-security-sensitivity-label-overview) + +- [Bring your own encryption keys for Power BI \| Microsoft + Docs](https://docs.microsoft.com/en-us/power-bi/admin/service-encryption-byok) + +- [What is an on-premises data gateway? \| Microsoft + Docs](https://docs.microsoft.com/en-us/data-integration/gateway/service-gateway-onprem) + +- [Row-level security (RLS) with Power BI \| Microsoft + Docs](https://docs.microsoft.com/en-us/power-bi/admin/service-admin-rls) + +- [Power BI PowerShell cmdlets and modules references \| Microsoft + Docs](https://docs.microsoft.com/en-us/powershell/power-bi/overview?view=powerbi-ps) + +## Source Code and Credential Security Considerations + +Exposure of secrets via collaboration spaces is a security concern when +using Power BI. + +For Power BI embedded applications, it is recommended to implement a +source code scanning solution to identify credentials within the code of +any app housing embedded Power BI report(s). A source code scanner can +also encourage moving discovered credentials to more secure locations, +such as Azure Key Vault. + +Store encryption keys or service principal credentials used for +encrypting or accessing Power BI in a Key Vault, assign proper access +policies to the vault and regularly review access permissions. + +For regulatory or other compliance reasons, some agencies may need to +bring their own keys (BYOK), which is supported by Power BI. By default, +Power BI uses Microsoft-managed keys to encrypt the data. In Power BI +Premium, users can use their own keys for data at-rest that is imported +into a dataset (see [Data source and storage +considerations](https://docs.microsoft.com/en-us/power-bi/enterprise/service-encryption-byok#data-source-and-storage-considerations) +for more information). + +- For Power BI embedded applications, a best practice is to implement a + source code scanning solution to identify credentials within the code + of the app housing the embedded Power BI report(s). + +- If required under specific regulations, agencies need a strategy for + maintaining control and governance of their keys. The bring your own + key (BYOK) functionality is one option. + +**Prerequisites** + +- Implementers must do their own due diligence in selecting a source + code scanner that integrates with their specific environment. + Microsoft documentation [provides a + link](https://owasp.org/www-community/Source_Code_Analysis_Tools) to + third-party scanners at the Open Web Application Security Project + (OWASP). This baseline does not endorse or advise on the selection or + use of any specific third-party tool. + +- If BYOK is deemed to be a requirement: + +- Power BI Premium is required for BYOK. + +- To use BYOK, the Power BI tenant admin must upload data to the Power + BI service from a Power BI Desktop (PBIX) file. + +- RSA keys must be 4096-bit. + +- Enable BYOK in the tenant. + +**BYOK Implementation High-Level Steps** + +Enable BYOK at the tenant level via PowerShell by first introducing the +encryption keys created and stored in Azure Key Vault to the Power BI +tenant. + +Then assign these encryption keys per Premium capacity for encrypting +content in the capacity. + +To enable bringing the agency’s key for Power BI, the high-level +configuration steps are as follows: + +1. Add the Power BI service as a service principal for the key vault, + with wrap and unwrap permissions. + +2. Create an RSA key with a 4096-bit length (or use an existing key of + this type), with wrap and unwrap permissions. + +3. To turn on BYOK, Power BI Tenant administrators must use a set of + Power BI [Admin PowerShell + Cmdlets](https://docs.microsoft.com/en-us/powershell/module/microsoftpowerbimgmt.admin/?view=powerbi-ps) + added to the Power BI Admin Cmdlets. + + Follow [detailed + steps](https://docs.microsoft.com/en-us/power-bi/enterprise/service-encryption-byok) + from Microsoft. + +**Related Resources:** + +- [Bring your own encryption keys for Power BI \| Microsoft + Docs](https://docs.microsoft.com/en-us/power-bi/enterprise/service-encryption-byok) + +- [Microsoft Source Code Analysis Developer Frequently Asked + Questions](https://docs.microsoft.com/en-us/azure/security/develop/security-code-analysis-faq) + +- For GitHub, the agency can use the native secret scanning feature to + identify credentials or other form of secrets within code at [About + secret scanning \| GitHub + docs](https://docs.github.com/github/administering-a-repository/about-secret-scanning) + +- [Announcing General Availability of Bring Your Own Key (BYOK) for + Power BI + Premium](https://powerbi.microsoft.com/en-us/blog/announcing-general-availability-of-bring-your-own-key-byok-for-power-bi-premium/) + +## File Export and Visual Artifact Considerations + +Exporting data from Power BI to image files and comma-separated value +(.csv) file format has data security implications. For example, if +row-level security (RLS) features are in use in Power BI, an export to +image or .csv could allow a user to inadvertently decouple that setting +and expose data to a party who does not have permissions or a need to +know that previously secured data. A similar scenario applies for +information protection sensitivity labels. + +A message regarding this condition is provided in the Power BI tenant +settings for the particular types of exports. + +In contrast to this, Power BI applies these protection settings (RLS, +sensitivity labels) when the report data leaves Power BI via a supported +export method, such as export to Excel, PowerPoint, or PDF, download to +.pbix, and Save (Desktop). In this case, only authorized users will be +able to open protected files. + +**Copy and Paste Visuals:** + +Power BI can allow users to copy and paste visuals from Power BI reports +as static images into external applications. This could represent a data +security risk in some contexts. The agency must evaluate whether this +represents risk for its data artifacts and whether to turn this off in +the Export and Sharing Settings. + +**Related Resources:** + +- [Sensitivity labels in Power BI \| Microsoft + Docs](https://docs.microsoft.com/en-us/power-bi/enterprise/service-security-sensitivity-label-overview) + +- [Say No to Export Data, Yes to Analyze in + Excel](https://radacad.com/say-no-to-export-data-yes-to-analyze-in-excel-power-bi-and-excel-can-talk) + +- [Power BI Governance – Why you should consider disabling Export to + Excel](https://data-marc.com/2020/04/13/power-bi-governance-why-you-should-consider-to-disable-export-to-excel/) + +**Implementation settings:** + +1. In the **Power BI tenant** settings, under **Export and sharing + settings**, admins can opt to toggle off both **Export reports as + image files** and **Export to .csv**. + +2. In the **Power BI tenant** settings, under **Export and sharing + settings**, admins can opt to toggle off **Copy and paste visuals**. + +####### Establishing Private Network Access Connections Using Azure Private Link + +When connecting to Azure services intended to supply Power BI datasets, +agencies should consider connecting their Power BI tenant to an Azure +Private Link endpoint and disable public internet access. + +In this configuration, Azure Private Link and Azure Networking private +endpoints are used to send data traffic privately using Microsoft's +backbone network infrastructure. The data travels the Microsoft private +network backbone instead of going across the Internet. + +Using private endpoints with Power BI ensures that traffic will flow +over the Azure backbone to a private endpoint for Azure cloud-based +resources. + +Within this configuration, there is also the capability to disable +public access to Power BI datasets. + +**High-Level Implementation Steps** + +Note: It is imperative that the VNET and VM are configured before +disabling public internet access. + +1. Enable private endpoints for Power BI. + +2. Create a Power BI resource in the Azure portal. + +3. Create a virtual network. + +4. Create a virtual machine (VM). + +5. Create a private endpoint. + +6. Connect to a VM using Remote Desktop (RDP). + +7. Access Power BI privately from the virtual machine. + +8. Disable public access for Power BI. + +**Related Resources:** + +- [Private endpoints for accessing Power BI \| Microsoft + Docs](https://docs.microsoft.com/en-us/power-bi/enterprise/service-security-private-links) + +- [Microsoft Power BI Security Baseline, Baseline Statement NS-3 \| + Microsoft + Docs](https://docs.microsoft.com/en-us/security/benchmark/azure/baselines/power-bi-security-baseline) diff --git a/baselines/powerplatform.md b/baselines/powerplatform.md new file mode 100644 index 0000000000..3afb419317 --- /dev/null +++ b/baselines/powerplatform.md @@ -0,0 +1,416 @@ +# 1. Introduction + +The Microsoft Power Platform is a group of applications involving +low-code application development, business intelligence, a custom chat +bot creator, and app connectivity software. The following summarizes the +Power Platform applications and other applications frequently used by +Power Platform applications. + +**Power Apps**: This is a low-code application development software used +to create custom business applications. The apps can be used as desktop, +mobile, and web apps. Three different types of Power Apps can be +created: + +1. **Canvas Apps**: These are drag and drop style developed apps, where + users drag and add User Interface (UI) components to the screen. + Users can then connect the components to data sources to display + data in the canvas app. + +2. **Model-Driven Apps**: These apps are developed from an existing + data source. They can be thought of as the inverse of a Canvas App. + For those familiar with the Model-View-Controller design pattern, + Model-Driven apps revolve around building the view and controller on + top of the model. + +3. **Portals**: These apps are created to be websites. + +**Power Automate**: This is an online tool within the Microsoft 365 +applications and add-ins used to create automated workflows between apps +and services to synchronize files, get notifications and collect data. + +**Power Virtual Agents**: These are custom chat bots for use in the +stand-alone Power Virtual Agents web app or in a Microsoft Teams +channel. + +**Connectors**: These are a proxy or a wrapper around an API that allows +the underlying service to be accessed from Power Automate workflows, a +Power App or Azure Logic Apps. + +**Microsoft Dataverse**: This is a cloud database management system most +often used to store data in SQL-like tables. A Power App would then use +a connector to connect to the Dataverse table and perform create, read, +update and delete (CRUD) operations. + +## 1.1 Assumptions + +The **License Requirements** sections of this document assume the +organization is using an [M365 +E3](https://www.microsoft.com/en-us/microsoft-365/compare-microsoft-365-enterprise-plans) +or [G3](https://www.microsoft.com/en-us/microsoft-365/government) +license level. Therefore, only licenses not included in E3/G3 are +listed. + +## 1.2 Resources + +**License Compliance and Copyright** + +Portions of this document are adapted from documents in Microsoft’s +[Microsoft +365](https://github.com/MicrosoftDocs/microsoft-365-docs/blob/public/LICENSE) +and +[Azure](https://github.com/MicrosoftDocs/azure-docs/blob/main/LICENSE) +GitHub repositories. The respective documents are subject to copyright +and are adapted under the terms of the Creative Commons Attribution 4.0 +International license. Source documents are linked throughout this +document. The United States Government has adapted selections of these +documents to develop innovative and scalable configuration standards to +strengthen the security of widely used cloud-based software services. + +# 2. Baseline + +Baselines in this section are for administrative controls that apply to +all Power Platform applications at the Power Platform tenant and +environment level. Additional Power Platform security settings would be +implemented at the app level, connector level, or Dataverse table level. +Refer to Microsoft documentation for those additional controls. + +## 2.1 Creation of Power Platform Environments SHALL Be Restricted + +Power Platform environments are used to group together, manage, and +store Power Apps and Power Virtual Agents. By default, any user in the +Azure AD Tenant can create additional environments. Enabling this +control will restrict the creation of new environments to users with the +following admin roles: Global admins, Dynamics 365 admins, and Power +Platform admins. + +### 2.1.1 Policy + +- The ability to create additional environments SHALL be restricted to + admins. + +### 2.1.2 Resources + +- [Control who can create and manage environments in the Power Platform + admin center \| Microsoft + Documents](https://docs.microsoft.com/en-us/power-platform/admin/control-environment-creation) + +- [Environment Administrator \| Digital Transformation Agency of + Australia](https://desktop.gov.au/blueprint/office-365.html#power-platform) + +- [Microsoft Technical Documentation \| Power + Apps](https://docs.microsoft.com/en-us/power-apps/) + +### 2.1.3 License Requirements + +- N/A + +### 2.1.4 Implementation + +1. Sign in to the [Power Platform admin + center](https://admin.powerplatform.microsoft.com/) or for GCC + environments sign in to the [GCC Power Platform Admin + Center](https://gcc.admin.powerplatform.microsoft.us/). + +2. In the upper-right corner of the Microsoft Power Platform site, + select the **Gear icon** (Settings icon). + +3. Select **Power Platform settings**. + +4. Under **Who can create production and sandbox environments**, select + **Only specific admins.** + +5. Under **Who can create trial environments**, select **Only specific + admins.** + +Now only Global admins, Dynamics 365 service admins, Power Platform +Service admins, and Delegated admins can create environments. + +## 2.2 Data Loss Prevention Policies for Power Platform Environments SHALL Be Created + +To secure Power Platform environments Data Loss Prevention (DLP) +policies can be created to restrict the connectors that can be used with +Power Apps created in an environment. A DLP policy can be created to +affect all or some environments or exclude certain environments. The +narrower policy will be enforced when there is a clash. + +Connectors can be separated by creating a DLP policy that assigns them +to one of three groups: Business, Non-Business, and Blocked. Connectors +in different groups cannot be used in the same Power App. Connectors in +the Blocked group cannot be used at all. Note that some M365 connectors +cannot be blocked (e.g., Teams and SharePoint connectors). + +In the DLP policy, connectors can also be configured to restrict read +and write permissions to the data source/service. Connectors that cannot +be blocked, also cannot be configured. Agencies should evaluate the +connectors and configure them to fit with agency needs and security +requirements. The agency should then create a DLP policy to only allow +those connectors to be used in the Power Platform. + +When the Azure AD tenant is created, by default, a Power Platform +environment is created in Power Platform. This Power Platform +environment will bear the name of the tenant. There is no way to +restrict users in the Azure AD tenant from creating Power Apps in the +default Power Platform environment. Admins can restrict users from +creating apps in all other created environments. + +### 2.2.1 Policy + +- A DLP policy SHALL be created to restrict connector access in the + default Power Platform environment. + +- Non-default environments SHOULD have at least one DLP policy that + affects them. + +- All connectors except those listed below SHOULD be added to the + Blocked category in the default environment policy: + + - Approvals + + - Dynamics 365 Customer Voice + + - Excel Online (Business) + + - Microsoft Dataverse + + - Microsoft Dataverse (legacy) + + - Microsoft Teams + + - Microsoft To-Do (Business) + + - Office 365 Groups + + - Office 365 Outlook + + - Office 365 Users + + - OneDrive for Business + + - OneNote (Business) + + - Planner + + - Power Apps Notification + + - Power BI + + - SharePoint + + - Shifts for Microsoft Teams + + - Yammer + +### 2.2.2 Resources + +- [Data Policies for Power Automate and Power Apps \| Digital + Transformation Agency of + Australia](https://desktop.gov.au/blueprint/office-365.html#power-apps-and-power-automate) + +- [Create a data loss prevention (DLP) policy \| Microsoft + Docs](https://docs.microsoft.com/en-us/power-platform/admin/create-dlp-policy) + +### 2.2.3 License Requirements + +- N/A + +### 2.2.4 Implementation + +1. Sign in to the [Power Platform admin + center](https://admin.powerplatform.microsoft.com/) (for GCC + environments sign in to the [GCC Power Platform Admin + Center](https://gcc.admin.powerplatform.microsoft.us/)). + +2. On the left pane, select **Policies** then **Data Policies.** + +3. Select the **+ New Policy** icon to create a new policy. + +4. Give the policy a suitable agency name and click **Next.** + +5. At the **Prebuilt connectors** section, select the connectors that + fit the agency’s needs. + +6. Select a connector and click **Move to Business.** + +7. If necessary (and possible) for the connector, click **Configure +connector** at the top of the screen to change connector +permissions. + +8. Refer to Table 1 for those connectors to move which **Business/Non-Business** Category. + +9. For the default environment, move all connectors that cannot be +blocked to the **Blocked** category. + +10. At the bottom of the screen, select **Next** to move on. + +11. Add a customer connector pattern that fit the agency’s needs. Click +**Next**. + +12. Define the scope of the policy. For the default environment select +**Add multiple environments** and add the default environment. + +13. Select the environments over which to add the policy and click **Add + to policy** at the top. + +14. Select **Next**-\> **Create Policy** to finish. + +## 2.3 Tenant Isolation SHALL Be Enabled to Prevent Cross Tenant Access of Power Platform environments + +Power Platform tenant isolation is different from Azure AD-wide tenant +restriction. It does not impact Azure AD-based access outside of Power +Platform. Power Platform tenant isolation only works for connectors +using Azure AD-based authentication, such as Office 365 Outlook or +SharePoint. The default configuration in Power Platform is with tenant +isolation set to **Off**, which allows for cross-tenant connections to +be established. A user from tenant A using a Power App with a connector +can seamlessly establish a connection to tenant B if using appropriate +Azure AD credentials. + +If admins want to allow only a select set of tenants to establish +connections to or from their tenant, they can turn on tenant isolation. +Once tenant isolation is turned on, inbound (connections to the tenant +from external tenants) and outbound (connections from the tenant to +external tenants) cross-tenant connections are blocked by Power Platform +even if the user presents valid credentials to the Azure AD-secured data +source. + +### 2.3.1 Policy + +- Power Platform tenant isolation SHALL be enabled. + +- An inbound/outbound connection allowlist SHOULD be configured. + +- The allowlist MAY be empty. + +### 2.3.2 Resources + +- [Enable tenant isolation and configure allowlist \| Microsoft + Docs](https://docs.microsoft.com/en-us/power-platform/admin/cross-tenant-restrictions#enable-tenant-isolation-and-configure-allowlist) + +### 2.3.3 License Requirements + +- N/A + +### 2.3.4 Implementation + +1. Sign in to the [Power Platform admin + center](https://admin.powerplatform.microsoft.com/) or for GCC + environments sign in to the [GCC Power Platform Admin + Center](https://gcc.admin.powerplatform.microsoft.us/). + +2. On the left pane, select **Policies -\> Tenant Isolation**. + +3. The tenant isolation allowlist can be configured by using **New + tenant rule** on the Tenant Isolation page. + +4. If Tenant Isolation is switched **Off**, add or edit the rules in + the allowlist. However, these rules won't be enforced until tenant + isolation is turned **On**. + +## 2.4 Content Security Policy SHALL Be Enabled + +Content Security Policy (CSP) is an added layer of security that helps +to detect and mitigate certain types of attacks, including Cross-Site +Scripting (XSS) and data injection attacks. These attacks are used for +everything from data theft to site defacement to malware distribution. +Currently, there is no UI for editing these attributes, but there is a +plan to expose this control in the Power Platform admin center. In the +meantime, use this +[script](https://docs.microsoft.com/en-us/power-platform/admin/content-security-policy) +to apply this setting. Also, there is no current way to implement this +setting for Canvas Apps. When enabled, this setting will apply to all +current Model-driven apps at only the environment level. + +### 2.4.1 Policy + +- Content security policies for model-driven Power Apps SHALL be + enabled. + +### 2.4.2 Resources + +- [Content Security Policy \| Microsoft + Docs](https://docs.microsoft.com/en-us/power-platform/admin/content-security-policy) + +### 2.4.3 License Requirements + +- N/A + +### 2.4.4 Implementation + +1. Sign in to [Make Power Apps](https://make.powerapps.com) (for GCC + environments sign in to the [GCC Make Power + Apps](https://make.gov.powerapps.us) center). + +2. On the left-hand pane select **Apps** and select one of the + Model-Driven Apps (if there is no available Model-Driven app, create + one). + +3. While in the model-driven app menu with an account with entity + update permissions, such as System Administrator or Power Platform + Administrator, open the browser dev tools. + +4. How to open the browser dev tools depends on which browser is used. + For a chromium-based browser, right click -\> **inspect** -\> + **console**). + +5. Paste the JavaScript code found +[here](https://docs.microsoft.com/en-us/power-platform/admin/content-security-policy) +into the console. + +6. To simply enable CSP, pass the default configuration, i.e., call the + function like so - `enableFrameAncestors(["'self'"])`. + +7. As an example of enabling additional origins to embed the app - + `enableFrameAncestors(["*.powerapps.com", "'self'", "abcxyz"])`. + +8. To disable CSP call - `disableCSP()`. + +# Acknowledgements + +In addition to acknowledging the important contributions of a diverse +team of Cybersecurity and Infrastructure Security Agency (CISA) experts, +CISA thanks the following federal agencies and private sector +organizations that provided input during the development of the Secure +Business Cloud Application’s security configuration baselines in +response to Section 3 of [Executive Order (EO) 14028, *Improving the +Nation’s +Cybersecurity*](https://www.federalregister.gov/documents/2021/05/17/2021-10460/improving-the-nations-cybersecurity): + +- Consumer Financial Protection Bureau (CFPB) + +- Department of the Interior (DOI) + +- National Aeronautics and Space Administration (NASA) + +- Sandia National Laboratories (Sandia) + +- U.S. Census Bureau (USCB) + +- U.S. Geological Survey (USGS) + +- U.S. Office of Personnel Management (OPM) + +- U.S. Small Business Administration (SBA) + +The cross-agency collaboration and partnerships developed during this +initiative serve as an example for solving complex problems faced by the +federal government. + +**Cybersecurity Innovation Tiger Team (CITT) Leadership** + +Beau Houser (USCB), Sanjay Gupta (SBA), Michael Witt (NASA), James +Saunders (OPM), Han Lin (Sandia), Andrew Havely (DOI). + +**CITT Authors** + +Trafenia Salzman (SBA), Benjamin McChesney (OPM), Robert Collier (USCB), +Matthew Snitchler (Sandia), Darryl Purdy (USCB), Brandon Frankens +(NASA), Brandon Goss (NASA), Nicole Bogeajis (DOI/USGS), Kevin Kelly +(DOI), Adnan Ehsan (CFPB), Michael Griffin (CFPB), Vincent Urias +(Sandia), Angela Calabaza (Sandia). + +**CITT Contributors** + +Dr. Mukesh Rohatgi (MITRE), Lee Szilagyi (MITRE), Nanda Katikaneni +(MITRE), Ted Kolovos (MITRE), Thomas Comeau (MITRE), Karen Caraway +(MITRE), Jackie Whieldon (MITRE), Jeanne Firey (MITRE), Kenneth Myers +(General Services Administration). diff --git a/baselines/sharepoint.md b/baselines/sharepoint.md new file mode 100644 index 0000000000..c7f06858af --- /dev/null +++ b/baselines/sharepoint.md @@ -0,0 +1,328 @@ +# 1. Introduction + +SharePoint Online is a web-based collaboration and document management +platform. Though highly flexible, it is primarily used to store +documents and communicate information across organizations. +Organizations can create sites, pages, document libraries, lists and +custom applications. + +In this baseline, the types of SharePoint Online users are defined as +follows (Note: these terms vary in use across Microsoft documentation +and within the context of different M365 workloads (e.g., Teams vs +SharePoint Online): + +1. **Internal users**: members of the agency’s M365 organization. + +2. **External users**: members of a different M365 organization. + +3. **Business to Business (B2B) guest users**: external users that are + formally invited to collaborate and added to the agency’s Azure + Active Directory (AAD) as guest users. These users authenticate with + their home organization/tenant and are granted access by virtue of + being listed as guest users on the tenant’s AAD. + +4. **Unmanaged users**: users that are not members of any M365 tenant + or organization (e.g., recipients of an “Anyone link.”) + + External sharing within SharePoint Online is defined as users within + an organization sharing content with people outside the organization + (external users, B2B guest users, or unmanaged users), or with + licensed users on multiple Microsoft 365 subscriptions if the agency + has more than one subscription. SharePoint has external sharing + settings at both the organizational level and the site level + (previously called the "site collection" level). To allow external + sharing on any site, it must be allowed at the organization level. + Then external sharing can be restricted for other sites. If a site's + external sharing option and the organization-level sharing option + don't match, the most restrictive value will always be applied. + + Source: + + +## 1.1 Resources + +**License Compliance and Copyright** + +Portions of this document are adapted from documents in Microsoft’s +[Microsoft +365](https://github.com/MicrosoftDocs/microsoft-365-docs/blob/public/LICENSE) +and +[Azure](https://github.com/MicrosoftDocs/azure-docs/blob/main/LICENSE) +GitHub repositories. The respective documents are subject to copyright +and are adapted under the terms of the Creative Commons Attribution 4.0 +International license. Source documents are linked throughout this +document. The United States Government has adapted selections of these +documents to develop innovative and scalable configuration standards to +strengthen the security of widely used cloud-based software services. + +## 1.2 Assumptions + +The **License Requirements** sections of this document assume the +organization is using an [M365 +E3](https://www.microsoft.com/en-us/microsoft-365/compare-microsoft-365-enterprise-plans) +or [G3](https://www.microsoft.com/en-us/microsoft-365/government) +license level. Therefore, only licenses not included in E3/G3 are +listed. + +# 2. Baselines + +## 2.1 File and Folder Links Default Sharing Settings SHALL Be Set to "Specific People (Only the People the User Specifies)" + +This policy ensures that when sharing files in SharePoint, there are +several possible scopes, including agency-wide or “anyone with the +link.” + +### 2.1.1 Policy + +- File and folder links default sharing setting SHALL be set to + “Specific People (Only the People the User Specifies)”. + +### 2.1.2 Resources + +- [File and folder links \| Microsoft + Documents](https://docs.microsoft.com/en-us/sharepoint/turn-external-sharing-on-or-off#file-and-folder-links) + +### 2.1.3 License Requirements + +- N/A + +### 2.1.4 Implementation + +In the **SharePoint admin** **center**: + +1. In the left-hand navigation bar, click **Policies** -\> **Sharing** + to display sharing settings. + +2. Under **File and folder links**, ensure that the default link type + is set to **Specific people (only the people the user specifies)**. + +## 2.2 External Sharing SHOULD be Set to “New and Existing Guests” and Managed Through Approved Domains and/or Security Groups Per Interagency Collaboration Needs. + +SharePoint allows sharing with users who are outside the agency, which +is convenient but may pose a data loss or other information security +risk. This working group recommends allowlisting by domains and security +groups per interagency collaboration needs. + +**Note**: Adjusting this setting will adjust external sharing +for OneDrive and Teams to the same, selected level. OneDrive and Teams +can be less permissive (not more permissive) than SharePoint Online. + +Adding approved domains and/or security groups will also be reflected in +OneDrive external sharing settings. + +External access may be granted on a per-domain basis. This may be +desirable in some cases, e.g., for agency-to-agency collaboration (see +the CIO Council's [Interagency Collaboration +Program](https://community.max.gov/display/Egov/Interagency+Collaboration+Program)’s +OMB Max Site for a list of .gov domains for sharing). + +### 2.2.1 Policy + +- External sharing SHOULD be limited to approved domains and security + groups per interagency collaboration needs. + +### 2.2.2 Resources + +- [Manage sharing settings \| Microsoft + Documents](https://docs.microsoft.com/en-us/sharepoint/turn-external-sharing-on-or-off) + +### 2.2.3 License Requirements + +- N/A + +### 2.2.4 Implementation + +To adjust sharing settings, in the **SharePoint admin center**: + +1. Select **Policies** -\> **Sharing**. + +2. Adjust external sharing slider to **New and Existing Guests.** + +3. Expand **More external sharing settings.** + +4. Select **Limit external sharing by domain**. + +5. Select **Add domains.** + +6. Add domains. + +7. Select **Save.** + +8. Select **Allow only users in specific security groups to share + externally**. + +9. Select **Manage security groups.** + +10. Add security groups. + +11. Select **Save.** + +## 2.3 Sensitive SharePoint Sites SHOULD Adjust Their Default Sharing Settings to Those Best Aligning to Their Sensitivity Level + +SharePoint allows sharing with users who are outside the agency, which +is convenient but may pose a data loss or other information security +risk. This working group recommends outside of the default +organizational settings agencies should evaluate each created site and +adjust sharing settings best aligned to their respective sensitivity +level. + +### 2.3.1 Policy + +- Sharing settings for specific SharePoint sites SHOULD align to their + sensitivity level. + +### 2.3.2 Resources + +- [Managing SharePoint Online Security: A Team Effort \| Microsoft + Build](https://docs.microsoft.com/en-us/microsoft-365/community/sharepoint-security-a-team-effort) + +### 2.3.3 License Requirements + +- N/A + +### 2.3.4 Implementation + +To limit external sharing by domain, in the **SharePoint admin center**: + +1. Select **Sites.** + +2. Select **Active sites.** + +3. Select **Site name.** + +4. Select **Add domains.** + +5. Select **Policies.** + +6. Under **external sharing**, select **Edit**. + +7. Select permissions aligning to the risk posture associated with the + sensitivity of the SharePoint site. + +8. Select **Save.** + +## 2.4 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. + +SharePoint allows sharing with users who are outside the agency, which +is convenient but may pose a data loss or other information security +risk. This working group recommends setting an expiration time for guest +access to the site or OneDrive. + +**Note**: Adjusting this setting will adjust external sharing +for OneDrive and Teams to the same, specified expiration times. + +### 2.4.1 Policy + +- Expiration timers for ‘guest access to a site or OneDrive’ and ‘people + who use a verification code’ SHOULD be set. + +- Expiration timers SHOULD be set to 30 days. + +### 2.4.2 License Requirements + +- N/A + +### 2.4.3 Resources + +- [Managing SharePoint Online Security: A Team Effort \| Microsoft + Build](https://docs.microsoft.com/en-us/microsoft-365/community/sharepoint-security-a-team-effort) + +### 2.4.4 Implementation + +To limit external sharing by domain, in the **SharePoint admin center**: + +1. Select **Policies** -\> **Sharing**. + +2. Expand **More external sharing settings.** + +3. Select **Guest access to a site or OneDrive will expire + automatically after this many days.** + +4. Enter “30” days. + +5. Select **People who use a verification code must reauthenticate + after this many days**. + +6. Enter “30 days”. + +## 2.5 Users SHALL Be Prevented from Running Custom Scripts + +Allowing users to run custom scripts can potentially allow malicious +scripts to run in a trusted environment. For this reason, running custom +scripts should not be allowed. + +### 2.5.1 Policy + +- Users SHALL be prevented from running custom scripts. + +### 2.5.2 Resources + +- [Allow or prevent custom script \| Microsoft + Documents](https://docs.microsoft.com/en-us/sharepoint/allow-or-prevent-custom-script) + +### 2.5.3 License Requirements + +- N/A + +### 2.5.4 Implementation + +In the **SharePoint Classic admin center**: + +1. Scroll to the **Custom Script** setting and select both of the + following: + +2. **Prevent users from running custom script on personal sites.** + +3. **Prevent users from running custom script on self-service created + sites.** + +# Acknowledgements + +In addition to acknowledging the important contributions of a diverse +team of Cybersecurity and Infrastructure Security Agency (CISA) experts, +CISA thanks the following federal agencies and private sector +organizations that provided input during the development of the Secure +Business Cloud Application’s security configuration baselines in +response to Section 3 of [Executive Order (EO) 14028, *Improving the +Nation’s +Cybersecurity*](https://www.federalregister.gov/documents/2021/05/17/2021-10460/improving-the-nations-cybersecurity): + +- Consumer Financial Protection Bureau (CFPB) + +- Department of the Interior (DOI) + +- National Aeronautics and Space Administration (NASA) + +- Sandia National Laboratories (Sandia) + +- U.S. Census Bureau (USCB) + +- U.S. Geological Survey (USGS) + +- U.S. Office of Personnel Management (OPM) + +- U.S. Small Business Administration (SBA) + +The cross-agency collaboration and partnerships developed during this +initiative serve as an example for solving complex problems faced by the +federal government. + +**Cybersecurity Innovation Tiger Team (CITT) Leadership** + +Beau Houser (USCB), Sanjay Gupta (SBA), Michael Witt (NASA), James +Saunders (OPM), Han Lin (Sandia), Andrew Havely (DOI). + +**CITT Authors** + +Trafenia Salzman (SBA), Benjamin McChesney (OPM), Robert Collier (USCB), +Matthew Snitchler (Sandia), Darryl Purdy (USCB), Brandon Frankens +(NASA), Brandon Goss (NASA), Nicole Bogeajis (DOI/USGS), Kevin Kelly +(DOI), Adnan Ehsan (CFPB), Michael Griffin (CFPB), Vincent Urias +(Sandia), Angela Calabaza (Sandia). + +**CITT Contributors** + +Dr. Mukesh Rohatgi (MITRE), Lee Szilagyi (MITRE), Nanda Katikaneni +(MITRE), Ted Kolovos (MITRE), Thomas Comeau (MITRE), Karen Caraway +(MITRE), Jackie Whieldon (MITRE), Jeanne Firey (MITRE), Kenneth Myers +(General Services Administration). diff --git a/baselines/teams.md b/baselines/teams.md new file mode 100644 index 0000000000..2931a9e87c --- /dev/null +++ b/baselines/teams.md @@ -0,0 +1,723 @@ +# 1. Introduction + +Microsoft Teams is a text and live chat workspace in Microsoft 365 that +supports video calls, chat messaging, screen-sharing, and file sharing. +It has a permission-based team structure for managing calls and files. +Microsoft teams also enables teams to manage their own user access +rights, security policies, and record video calls. + +Access to Teams can be controlled by the user type. In this baseline, +the types of users are defined as follows (Note: these terms vary in use +across Microsoft documentation): + +1. **Internal users**: members of the agency’s M365 tenant. + +2. **External users**: members of a different M365 tenant. + +3. **Business to Business** (B2B) guest users: external users that are + formally invited to collaborate with the team and added to the + agency’s Azure Active Directory (AAD) as guest users. These users + authenticate with their home organization/tenant and are granted + access to the team by virtue of being listed as guest users on the + tenant’s AAD. + +4. **Unmanaged users**: users who are not members of any M365 tenant or + organization (e.g., personal Microsoft accounts). + +5. **Anonymous users**: Teams users joining calls that are not + authenticated through the agency’s tenant, including unmanaged + users, external users (except for B2B guests), and true anonymous + users, meaning users that are not logged in to any Microsoft or + organization account, such as dial-in users.[^1] + +## 1.1 Assumptions + +The **License Requirements** sections of this document assume the +organization is using an [M365 +E3](https://www.microsoft.com/en-us/microsoft-365/compare-microsoft-365-enterprise-plans) +or [G3](https://www.microsoft.com/en-us/microsoft-365/government) +license level. Therefore, only licenses not included in E3/G3 are +listed. + +## 1.2 Resources + +**License Compliance and Copyright** + +Portions of this document are adapted from documents in Microsoft’s +[Microsoft +365](https://github.com/MicrosoftDocs/microsoft-365-docs/blob/public/LICENSE) +and +[Azure](https://github.com/MicrosoftDocs/azure-docs/blob/main/LICENSE) +GitHub repositories. The respective documents are subject to copyright +and are adapted under the terms of the Creative Commons Attribution 4.0 +International license. Source documents are linked throughout this +document. The United States Government has adapted selections of these +documents to develop innovative and scalable configuration standards to +strengthen the security of widely used cloud-based software services. + +# 2. Baseline + +## 2.1 External Participants SHOULD NOT Be Enabled to Request Control of Shared Desktops or Windows in Meetings + +This setting controls whether external meeting participants can request +control of the shared desktop or window during the meeting. In this +instance, the term “external participants” includes external users, B2B +guest users, unmanaged users and anonymous users. + +While there is some inherent risk in granting an external participant +control of a shared screen, legitimate use cases for this exist. +Furthermore, the risk is minimal as users cannot gain control of another +user’s screen unless the user giving control explicitly accepts a +control request. As such, while enabling external participants to +request control is discouraged, it may be done, depending on agency +need. + +### 2.1.1 Policy + +- 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. + +### 2.1.2 Resources + +- [Configure desktop sharing in Microsoft Teams \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoftteams/configure-desktop-sharing) + +### 2.1.3 License Requirements + +- N/A + +### 2.1.4 Implementation + +To ensure external participants do not have the ability to request +control of the shared desktop or window in the meeting: + +1. Sign in to the [**Microsoft Teams admin + center**](https://admin.teams.microsoft.com). + +2. Select **Meetings** -\> **Meeting policie**s. + +3. Select the **Global (Org-wide default)** policy. + +4. Under the **Content sharing** section, set **Allow an external + participant to give or request control** to **Off**. + +5. If custom policies have been created, repeat these steps for each + policy, selecting the appropriate policy in step 3. + +## 2.2 Anonymous Users SHALL NOT Be Enabled to Start Meetings + +This setting controls which meeting participants can start a meeting. In +this instance, the term “anonymous users” refers to any Teams users +joining calls that are not authenticated through the agency’s tenant. + +### 2.2.1 Policy + +- 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. + +### 2.2.2 Resources + +- [Meeting policy settings - Participants & guests \| Microsoft +Docs](https://docs.microsoft.com/en-us/microsoftteams/meeting-policies-participants-and-guests) + +### 2.2.3 License Requirements + +- N/A + +### 2.2.4 Implementation + +To configure settings for anonymous users: + +1. Sign in to the [**Microsoft Teams admin + center**](https://admin.teams.microsoft.com). + +2. Select **Meetings -\>** **Meeting policies**. + +3. Select the **Global (Org-wide default)** policy. + +4. Under the **Participants & guests** section**,** set **Let anonymous + people start a meeting** to **Off.** + +5. If custom policies have been created, repeat these steps for each + policy, selecting the appropriate policy in step 3. + +## 2.3 Automatic Admittance to Meetings SHOULD Be Restricted + +This setting controls which meeting participants wait in the lobby +before they are admitted to the meeting. + +### 2.3.1 Policy + +- Anonymous users, including dial-in users, SHOULD NOT be admitted + automatically. + +- Internal users SHOULD be admitted automatically. + +- B2B guest users MAY be admitted automatically. + +- The above settings SHOULD be set in the Global (Org-wide default) + meeting policy. + +- Custom meeting policies MAY be created that allow more flexibility for + specific users. + +### 2.3.2 Resources + +- [Meeting policy settings - Participants & guests \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoftteams/meeting-policies-participants-and-guests) + +### 2.3.3 License Requirements + +- N/A + +### 2.3.4 Implementation + +To configure settings for automatic meeting admittance: + +1. Sign in to the [**Microsoft Teams admin + center**](https://admin.teams.microsoft.com). + +2. Select **Meetings -\>** **Meeting** **policies**. + +3. Select the **Global (Org-wide default)** policy. + +4. Under the **Participants & guests** section**,** ensure + **Automatically admit people** is *not* set to **Everyone**. + +5. In the same section, set **Dial-in users can bypass the lobby** to + **Off**. + +6. If custom policies have been created, repeat these steps for each + policy, selecting the appropriate policy in step 3. + +## 2.4 External User Access SHALL Be Restricted + +External access allows external users to look up internal users by their +email address to initiate chats and calls entirely within Teams. +Blocking external access prevents external users from using Teams as an +avenue for reconnaissance or phishing. Even with external access +disabled, external users will still be able to join Teams calls, +assuming anonymous join is enabled. Depending on agency need, if both +external access and anonymous join need to be blocked—neither required +nor recommended by this baseline—external collaborators would only be +able to attend meetings if added as a B2B guest user. + +External access may be granted on a per-domain basis. This may be +desirable in some cases, e.g., for agency-to-agency collaboration (see +the CIO Council's [Interagency Collaboration +Program](https://community.max.gov/display/Egov/Interagency+Collaboration+Program)’s +OMB Max Site for a list of .gov domains for sharing). + +Importantly, this setting only pertains to external users (i.e., members +of a different M365 tenant). Access for unmanaged users is controlled +separately. + +### 2.4.1 Policy + +- External access SHALL only be enabled on a per-domain basis. + +- Anonymous users SHOULD be enabled to join meetings. + +### 2.4.2 Resources + +- [Manage external access in Microsoft Teams \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoftteams/manage-external-access) + +- [Allow anonymous users to join meetings \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoftteams/meeting-settings-in-teams#allow-anonymous-users-to-join-meetings) + +- [Use guest access and external access to collaborate with people + outside your organization \| Microsoft + Docs](https://docs.microsoft.com/en-us/microsoftteams/communicate-with-users-from-other-organizations) + +### 2.4.3 License Requirements + +- N/A + +### 2.4.4 Implementation + +To enable external access for only specific domains: + +1. Sign in to the [**Microsoft Teams admin + center**](https://admin.teams.microsoft.com). + +2. Select **Users** -\> **External access**. + +3. Under **Choose which external domains your users have access to**, + select **Allow only specific external domains**. + +4. Click **Allow domains** to add allowed external domains. All domains + not added in this step will be blocked. + +5. Click **Save.** + +To enable anonymous users to join meetings: + +1. Sign in to the [**Microsoft Teams admin + center**](https://admin.teams.microsoft.com). + +2. Select **Meetings** **-\>** **Meeting settings**. + +3. Under **Participants**, set **Anonymous users can join a meeting** + to **On**. + +4. Click **Save**. + +Anonymous users can also be enabled/blocked on a per-policy basis. + +1. Sign in to the [**Microsoft Teams admin + center**](https://admin.teams.microsoft.com). + +2. Select **Meetings** **-\>** **Meeting policies**. + +3. Select the **Global (Org-wide default)**, or other policy as needed. + +4. Under **Participants & guests**, set **Let anonymous people join a + meeting** to **On**. + +5. Click **Save**. + +## 2.5 Unmanaged User Access SHALL Be Restricted + +Blocking contact with unmanaged Teams users prevents these users from +looking up internal users by their email address and initiating chats +and calls within Teams. These users would still be able to join calls, +assuming anonymous join is enabled. Additionally, unmanaged users may be +added to Teams chats if the internal user initiates the contact. + +### 2.5.1 Policy + +- Unmanaged users SHALL NOT be enabled to initiate contact with internal +users. + + +- Internal users SHOULD NOT be enabled to initiate contact with unmanaged +users. + +### 2.5.2 Resources + +- [Manage contact with external Teams users not managed by an organization +\| Microsoft +Docs](https://docs.microsoft.com/en-us/microsoftteams/manage-external-access#manage-contact-with-external-teams-users-not-managed-by-an-organization) + +### 2.5.3 License Requirements + +- N/A + +### 2.5.4 Implementation + +Steps are outlined in [Manage contact with external Teams users not +managed by an +organization](https://docs.microsoft.com/en-us/microsoftteams/manage-external-access#manage-contact-with-external-teams-users-not-managed-by-an-organization). + +1. Sign in to the **[Microsoft Teams admin + center](https://admin.teams.microsoft.com).** + +2. Select **Users -\> External access.** + +3. To completely block contact with unmanaged users, under **Teams + accounts not managed by an organization**, set **People in my + organization can communicate with Teams users whose accounts aren't + managed by an organization** to **Off**. + +4. To allow contact with unmanaged users only if the internal user + initiates the contact: + + 1. Under **Teams accounts not managed by an organization**, set **People in my organization can communicate with Teams users whose accounts aren't managed by an organization** to **On**. + + 2. Clear the check next to **External users with Teams accounts not managed by an organization can contact users in my organization**. + +## 2.6 Contact with Skype Users SHALL Be Blocked + +Microsoft officially retired Skype for Business Online on July 31, 2021, +and it is no longer supported. + +### 2.6.1 Policy + +- Contact with Skype users SHALL be blocked. + +### 2.6.2 Resources + +- [Communicate with Skype users \| Microsoft +Docs](https://docs.microsoft.com/en-us/microsoftteams/manage-external-access#communicate-with-skype-users) + +- [Skype for Business Online to Be Retired in 2021 \| Microsoft Teams +Blog](https://techcommunity.microsoft.com/t5/microsoft-teams-blog/skype-for-business-online-to-be-retired-in-2021/ba-p/777833) + +### 2.6.3 License Requirements + +- N/A + +### 2.6.4 Implementation + +Instructions for *enabling* communications with Skype users are outlined +in [Communicate with Skype +users](https://docs.microsoft.com/en-us/microsoftteams/manage-external-access#communicate-with-skype-users). + +1. Sign in to the **[Microsoft Teams admin + center](https://admin.teams.microsoft.com).** + +2. Select **Users -\> External access.** + +3. Under **Skype** users, set **Allow users in my organization to + communicate with Skype users** to **Off**. + +4. Click **Save**. + +## 2.7 Teams Email Integration SHALL Be Disabled + +Teams provides an optional feature that allows channels to have an email +address and receive email. These channel email addresses are not under +the tenant’s domain; rather, they are associated with a Microsoft-owned +domain, teams.ms. As such, although some basic checks are performed, +agencies do not have control over the security settings associated with +this email. For this reason, email channel integration should be +disabled. + +### 2.7.1 Policy + +- Teams email integration SHALL be disabled. + +### 2.7.2 Resources + +- [Email Integration \| Microsoft +Docs](https://docs.microsoft.com/en-us/microsoftteams/enable-features-office-365#email-integration) + +### 2.7.3 License Requirements + +- Teams email integration is only available with E3/E5 licenses. It is not +available in GCC or DoD tenants. + +### 2.7.4 Implementation + +To ensure that teams email integration is disabled: + +1. Sign in to the **[Microsoft Teams admin + center](https://admin.teams.microsoft.com).** + +2. Select **Teams** -\> **Teams Settings**. + +3. Under the **Email integration** section, set **Allow users to send + emails to a channel email address** to **Off**. + +## 2.8 Only Approved Apps SHOULD Be Installed + +Teams is capable of integrating with the following classes of apps: + +*Microsoft apps*: apps published by Microsoft. + +*Third-party apps*: apps not authored by Microsoft, published to the +Teams store. + +*Custom apps*: apps not published to the Teams store, such as apps under +development, that users “sideload” into Teams. + +### 2.8.1 Policy + +- Agencies SHOULD allow all apps published by Microsoft, but MAY block +specific Microsoft apps as needed. + +- Agencies SHOULD NOT allow installation of all third-party apps or custom +apps, but MAY allow specific apps as needed. + +- Agencies SHALL establish policy dictating the app review and approval +process to be used by the agency. + +### 2.8.2 Resources + +- [Manage app permission policies in Microsoft Teams \| Microsoft +Docs](https://docs.microsoft.com/en-us/microsoftteams/teams-app-permission-policies) + +- [Upload your app in Microsoft Teams \| Microsoft +Docs](https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/apps-upload) + +### 2.8.3 License Requirements + +- N/A + +### 2.8.4 Implementation + +To restrict which Team apps can be installed: + +1. Sign in to the **[Microsoft Teams admin + center](https://admin.teams.microsoft.com).** + +2. Select **Teams apps -**\> **Permission policies.** + +3. Select **Global (Org-wide default)**. + +4. Under **Microsoft apps**, select **Allow all apps**, unless specific + apps need to be disallowed, in which case select **Block specific + apps and allow all others**. + +5. Set **Third-party apps** to **Block all apps**, unless specific apps + have been approved by the agency, in which case select **Allow + specific apps and block all others**. + +6. Set **Custom apps** to **Block all apps**, unless specific apps have + been approved by the agency, in which case select **Allow specific + apps and block all others**. + +7. Click **Save**. + +8. If custom policies have been created, repeat these steps for each + policy, selecting the appropriate policy in step 3. + +## 2.9 Cloud Recording of Teams Meetings SHOULD Be Disabled for Unapproved Users + +This setting determines whether video can be recorded in meetings hosted +by a user, during one-on-one calls, and on group calls started by a +user. Agencies should comply with any other applicable policies or +legislation in addition to this guidance. + +### 2.9.1 Policy + +- Cloud video recording SHOULD be disabled in the global (org-wide +default) meeting policy. + +- Alternate meeting policies MAY be created that allow agency-approved +users the ability to record. + +- For all meeting polices that allow cloud recording, recordings SHOULD be +stored inside the country of that agency’s tenant. + +### 2.9.2 Resources + +- [Teams cloud meeting recording \| Microsoft +Docs](https://docs.microsoft.com/en-us/microsoftteams/cloud-recording) + +- [Assign policies in Teams – getting started \| Microsoft +Docs](https://docs.microsoft.com/en-us/microsoftteams/policy-assignment-overview) + +### 2.9.3 License Requirements + +- N/A + +### 2.9.4 Implementation + +To configure the Meeting policies for cloud video recording: + +1. Sign in to the **[Microsoft Teams admin + center](https://admin.teams.microsoft.com).** + +2. Select **Meetings** -\> **Meeting policies**. + +3. Select the **Global (Org-wide default)** policy. + +4. Under the **Recording & transcription** section, set **Cloud + recording** to **Off**. + +5. Select **Save**. + +If there is a legitimate business need, *specific* users can be given +permission to record meetings. To allow specific users the ability to +record meetings: + +1. Sign in to the **[Microsoft Teams admin + center](https://admin.teams.microsoft.com).** + +2. Select **Meetings** -\> **Meeting policies**. + +3. Create a new policy by selecting **Add**. Give this new policy a + name and appropriate description. + +4. Under the **Recording & transcription** section, set **Cloud + recording** to **On**. + +5. Under the **Recording & transcription** section, set **Store + recordings outside of your country or region** to **Off**. + +6. Select **Save**. + +7. After selecting **Save**, a table displays the set of policies. + Select the row containing the new policy, then select **Manage + users**. + +8. Assign the users that need the ability to record to this policy. + +9. Select **Apply**. + +## 2.10 Only the Meeting Organizer SHOULD Be Able to Record Live Events + +Live events are recorded by default. Agencies should increase their +privacy by changing the policy so that events are only recorded at the +organizer’s discretion. + +### 2.10.1 Policy + +- Record an event SHOULD be set to Organizer can record. + +### 2.10.2 Resources + +- [Live Event Recording Policies \| Microsoft +Docs](https://docs.microsoft.com/en-us/microsoftteams/teams-live-events/live-events-recording-policies) + +### 2.10.3 License Requirements + +- N/A + +### 2.10.4 Implementation + +1. Sign in to the **[Microsoft Teams admin + center](https://admin.teams.microsoft.com).** + +2. Select **Meetings** -\> **Live events policies**. + +3. Select **Global (Org-wide default)**. + +4. Set **Record an event** to **Organizer can record**. + +5. Click **Save**. + +## 2.11 Data Loss Prevention Solutions SHALL Be Enabled + +Data loss prevention (DLP) helps prevent both accidental leakage of +sensitive information as well as intentional exfiltration of data. DLP +forms an integral part of securing Microsoft Teams. There a several +commercial DLP solutions available that document support for Microsoft +Teams. Agencies may select any service that fits their needs and meets +the requirements outlined in this baseline control. + +Microsoft offers DLP services, controlled within the [Microsoft 365 +compliance](https://compliance.microsoft.com) admin center. Though use +of Microsoft’s DLP solution is not strictly required, guidance for +configuring Microsoft’s DLP solution can be found in the “Data Loss +Prevention SHALL Be Enabled” section of the *Defender for Office 365 +Minimum Viable Secure Configuration Baseline*. The DLP solution selected +by an agency should offer services comparable to those offered by +Microsoft. + +### 2.11.1 Policy + +- A DLP solution SHALL be enabled. + +- Agencies SHOULD use either the native DLP solution offered by Microsoft +or a DLP solution that offers comparable services. + +- 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. + +### 2.11.2 Resources + +- The “Data Loss Prevention SHALL Be Enabled” section of the *Defender for +Office 365 Minimum Viable Secure Configuration Baseline*. + +## 2.12 Attachments SHOULD Be Scanned for Malware + +Though any product that fills the requirements outlined in this baseline +control may be used, for guidance on implementing malware scanning using +Microsoft Defender, see the “Data Loss Prevention SHALL Be Enabled” +section of the *Defender for Office 365 Minimum Viable Secure +Configuration Baseline*. + +### 2.12.1 Policy + +- Attachments included with Teams messages SHOULD be scanned for malware. + +- Users SHOULD be prevented from opening or downloading files detected as +malware. + +### 2.12.2 Resources + +- The “Data Loss Prevention SHALL Be Enabled” section of the *Defender for +Office 365 Minimum Viable Secure Configuration Baseline.* + +## 2.13 Link Protection SHOULD Be Enabled + +Microsoft Defender protects users from malicious links included in Teams +messages by prepending + +`https://\*.safelinks.protection.outlook.com/?url=` + +to URLs included in the messages. By prepending the safe links URL, +Microsoft can proxy the initial URL through their scanning service. +Their proxy performs the following checks: + +- Compares the URL with a block list + +- Compares the URL with a list of know malicious sites + +- If the URL points to a downloadable file, applies real-time file +scanning + +- If all checks pass, the user is redirected to the original URL. + +Though Defender’s use is not strictly required for this purpose, +guidance for enabling link scanning using Microsoft Defender is included +in the “Safe Links Policies SHALL Be Enabled” and “Safe Links in Global +Settings SHALL be Configured” sections of the *Defender for Office 365 +Minimum Viable Secure Configuration Baseline.* + +### 2.13.1 Policy + +- URL comparison with a block-list SHOULD be enabled. + +- Direct download links SHOULD be scanned for malware. + +- User click tracking SHOULD be enabled. + +### 2.13.2 Resources + +- The “Safe Links Policies SHALL Be Enabled” section of the *Defender for +Office 365 Minimum Viable Secure Configuration Baseline.* + + +# Acknowledgements + +In addition to acknowledging the important contributions of a diverse +team of Cybersecurity and Infrastructure Security Agency (CISA) experts, +CISA thanks the following federal agencies and private sector +organizations that provided input during the development of the Secure +Business Cloud Application’s security configuration baselines in +response to Section 3 of [Executive Order (EO) 14028, *Improving the +Nation’s +Cybersecurity*](https://www.federalregister.gov/documents/2021/05/17/2021-10460/improving-the-nations-cybersecurity): + +- Consumer Financial Protection Bureau (CFPB) + +- Department of the Interior (DOI) + +- National Aeronautics and Space Administration (NASA) + +- Sandia National Laboratories (Sandia) + +- U.S. Census Bureau (USCB) + +- U.S. Geological Survey (USGS) + +- U.S. Office of Personnel Management (OPM) + +- U.S. Small Business Administration (SBA) + +The cross-agency collaboration and partnerships developed during this +initiative serve as an example for solving complex problems faced by the +federal government. + +**Cybersecurity Innovation Tiger Team (CITT) Leadership** + +Beau Houser (USCB), Sanjay Gupta (SBA), Michael Witt (NASA), James +Saunders (OPM), Han Lin (Sandia), Andrew Havely (DOI). + +**CITT Authors** + +Trafenia Salzman (SBA), Benjamin McChesney (OPM), Robert Collier (USCB), +Matthew Snitchler (Sandia), Darryl Purdy (USCB), Brandon Frankens +(NASA), Brandon Goss (NASA), Nicole Bogeajis (DOI/USGS), Kevin Kelly +(DOI), Adnan Ehsan (CFPB), Michael Griffin (CFPB), Vincent Urias +(Sandia), Angela Calabaza (Sandia). + +**CITT Contributors** + +Dr. Mukesh Rohatgi (MITRE), Lee Szilagyi (MITRE), Nanda Katikaneni +(MITRE), Ted Kolovos (MITRE), Thomas Comeau (MITRE), Karen Caraway +(MITRE), Jackie Whieldon (MITRE), Jeanne Firey (MITRE), Kenneth Myers +(General Services Administration). + +[^1]: Note that B2B guest users and all anonymous users except for + external users appear in Teams calls as *John Doe (Guest)*. To avoid + any potential confusion this may cause, true guest users are always + referred to as B2B guest users in this document. diff --git a/images/aad-mfa.png b/images/aad-mfa.png new file mode 100644 index 0000000000..3f641b3d7a Binary files /dev/null and b/images/aad-mfa.png differ diff --git a/images/adminconsentapproval1.PNG b/images/adminconsentapproval1.PNG new file mode 100644 index 0000000000..2366d90514 Binary files /dev/null and b/images/adminconsentapproval1.PNG differ diff --git a/images/adminconsentapproval2.PNG b/images/adminconsentapproval2.PNG new file mode 100644 index 0000000000..0c914b1c0f Binary files /dev/null and b/images/adminconsentapproval2.PNG differ diff --git a/images/adminconsentapproval3.PNG b/images/adminconsentapproval3.PNG new file mode 100644 index 0000000000..3bad0685e1 Binary files /dev/null and b/images/adminconsentapproval3.PNG differ diff --git a/images/adminconsentapproval4.PNG b/images/adminconsentapproval4.PNG new file mode 100644 index 0000000000..4ba3c5098a Binary files /dev/null and b/images/adminconsentapproval4.PNG differ diff --git a/images/adminconsentworkflownotconfigured.PNG b/images/adminconsentworkflownotconfigured.PNG new file mode 100644 index 0000000000..7a07ccaf5d Binary files /dev/null and b/images/adminconsentworkflownotconfigured.PNG differ diff --git a/images/cloudadminuserrunningscript.PNG b/images/cloudadminuserrunningscript.PNG new file mode 100644 index 0000000000..1edc648f3f Binary files /dev/null and b/images/cloudadminuserrunningscript.PNG differ diff --git a/images/globaladminuserrunningscript.PNG b/images/globaladminuserrunningscript.PNG new file mode 100644 index 0000000000..1ca68ef3a8 Binary files /dev/null and b/images/globaladminuserrunningscript.PNG differ diff --git a/images/pplatformmissinglicense.PNG b/images/pplatformmissinglicense.PNG new file mode 100644 index 0000000000..f1ddc15300 Binary files /dev/null and b/images/pplatformmissinglicense.PNG differ diff --git a/images/regularuserneedadminapproval.PNG b/images/regularuserneedadminapproval.PNG new file mode 100644 index 0000000000..8f555880b9 Binary files /dev/null and b/images/regularuserneedadminapproval.PNG differ diff --git a/images/regularuserrequestapproval.PNG b/images/regularuserrequestapproval.PNG new file mode 100644 index 0000000000..378d00b46e Binary files /dev/null and b/images/regularuserrequestapproval.PNG differ diff --git a/images/regularuserrequestapproval2.PNG b/images/regularuserrequestapproval2.PNG new file mode 100644 index 0000000000..ff10b4caf4 Binary files /dev/null and b/images/regularuserrequestapproval2.PNG differ diff --git a/opa_windows_amd64.exe b/opa_windows_amd64.exe new file mode 100644 index 0000000000..6fae2292d1 Binary files /dev/null and b/opa_windows_amd64.exe differ