Skip to content

Commit 38224a7

Browse files
authored
Merge pull request #2538 from microsoft/May26EOMT
EOMT: Unified Exchange On-premises Mitigation Tool framework
2 parents 85ef993 + d1c0eb7 commit 38224a7

29 files changed

Lines changed: 5734 additions & 139 deletions
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ $BuildVersion = ""
4848
# Force TLS1.2 to make sure we can download from HTTPS
4949
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
5050

51-
. $PSScriptRoot\..\..\Shared\Show-Disclaimer.ps1
51+
. $PSScriptRoot\..\..\..\Shared\Show-Disclaimer.ps1
5252

5353
function Test-ExchangeMitigationRequired {
5454
param()
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
. $PSScriptRoot\..\..\IISManagement\New-IISConfigurationAction.ps1
5+
. $PSScriptRoot\..\..\IISManagement\Invoke-IISConfigurationManagerAction.ps1
6+
7+
<#
8+
.DESCRIPTION
9+
Applies mitigations using the IIS management pipeline. Takes a CVE mitigation definition
10+
object, invokes its GetActions script block to obtain raw actions, wraps each in
11+
New-IISConfigurationAction, then feeds them to Invoke-IISConfigurationManagerAction.
12+
#>
13+
function Invoke-ApplyMitigations {
14+
[CmdletBinding(SupportsShouldProcess)]
15+
param(
16+
[Parameter(Mandatory = $true)]
17+
[PSCustomObject]$MitigationDefinition,
18+
19+
[Parameter()]
20+
[string]$ServerName = $env:COMPUTERNAME
21+
)
22+
begin {
23+
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
24+
}
25+
process {
26+
if ($PSCmdlet.ShouldProcess($ServerName, "Apply $($MitigationDefinition.Id) mitigation to $($MitigationDefinition.SiteName)")) {
27+
Write-Verbose "Invoking GetActions for $($MitigationDefinition.Id)"
28+
$rawActions = & $MitigationDefinition.GetActions
29+
Write-Verbose "GetActions returned $($rawActions.Count) action(s)"
30+
31+
$wrappedActions = @()
32+
33+
foreach ($action in $rawActions) {
34+
Write-Verbose "Wrapping action: $($action.Cmdlet) $(if($action.RuleName) {"RuleName=$($action.RuleName)"})"
35+
$wrappedActions += New-IISConfigurationAction -Action $action
36+
}
37+
38+
Write-Verbose "Wrapped $($wrappedActions.Count) action(s). Sending to IIS configuration manager."
39+
40+
$inputObject = [PSCustomObject]@{
41+
ServerName = $ServerName
42+
Actions = $wrappedActions
43+
BackupFileName = $MitigationDefinition.Id
44+
}
45+
46+
# Capture the manager result to report actual success/failure.
47+
# Invoke-IISConfigurationManagerAction returns {FailedServers, SuccessfulServers, AllSucceeded}
48+
# (see addition in that file). The original shared module does not return a value.
49+
$managerResult = $inputObject | Invoke-IISConfigurationManagerAction -ConfigurationDescription "Apply $($MitigationDefinition.Id) Mitigation"
50+
51+
return [PSCustomObject]@{
52+
CVE = $MitigationDefinition.Id
53+
Success = $managerResult.AllSucceeded
54+
Result = $managerResult
55+
}
56+
}
57+
}
58+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
. $PSScriptRoot\..\..\IISManagement\Invoke-IISConfigurationManagerAction.ps1
5+
6+
<#
7+
.DESCRIPTION
8+
Rolls back mitigations applied by EOMT by using the IIS management restore pipeline.
9+
When mitigations are applied, per-CVE JSON backup files are created at:
10+
%WINDIR%\System32\inetSrv\config\IISManagementRestoreCmdlets-<CVE-ID>.json
11+
12+
The restore mechanism feeds an InputObject with a .Restore property to
13+
Invoke-IISConfigurationManagerAction, which calls Invoke-IISConfigurationRemoteAction.
14+
The remote action reads the JSON, executes each restore cmdlet, and renames the file to .bak.
15+
#>
16+
function Invoke-RollbackMitigations {
17+
[CmdletBinding(SupportsShouldProcess)]
18+
param(
19+
[Parameter(Mandatory = $true)]
20+
[string]$CVE,
21+
22+
[Parameter()]
23+
[string]$ServerName = $env:COMPUTERNAME
24+
)
25+
begin {
26+
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
27+
$rollbackAttempted = $false
28+
$success = $false
29+
}
30+
process {
31+
if ($PSCmdlet.ShouldProcess($ServerName, "Rollback mitigation for $CVE")) {
32+
$rollbackAttempted = $true
33+
34+
$inputObject = [PSCustomObject]@{
35+
ServerName = $ServerName
36+
Restore = @{
37+
FileName = $CVE
38+
PassedWhatIf = $WhatIfPreference
39+
}
40+
}
41+
42+
$managerResult = $inputObject | Invoke-IISConfigurationManagerAction -ConfigurationDescription "Rollback $CVE Mitigation"
43+
if ($managerResult.AllSucceeded) {
44+
$success = $true
45+
}
46+
}
47+
}
48+
end {
49+
return [PSCustomObject]@{
50+
RollbackAttempted = $rollbackAttempted
51+
CVE = $CVE
52+
Success = $success
53+
}
54+
}
55+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
function Get-VulnerabilityStatus {
5+
[CmdletBinding()]
6+
param(
7+
[Parameter(Mandatory = $true)]
8+
[PSCustomObject]$MitigationDefinition
9+
)
10+
11+
Write-Verbose "Calling: $($MyInvocation.MyCommand) for $($MitigationDefinition.Id)"
12+
13+
$mitigationApplied = $false
14+
$codeFixApplied = $false
15+
try {
16+
$testResult = & $MitigationDefinition.TestVulnerable
17+
$mitigationApplied = [bool]$testResult.MitigationApplied
18+
$codeFixApplied = [bool]$testResult.CodeFixApplied
19+
} catch {
20+
Write-Warning "Failed to check vulnerability status for $($MitigationDefinition.Id): $_. Assuming vulnerable."
21+
}
22+
23+
Write-Verbose "$($MitigationDefinition.Id) MitigationApplied: $mitigationApplied CodeFixApplied: $codeFixApplied"
24+
25+
return [PSCustomObject]@{
26+
CVE = $MitigationDefinition.Id
27+
MitigationApplied = $mitigationApplied
28+
CodeFixApplied = $codeFixApplied
29+
Description = $MitigationDefinition.Description
30+
CheckPerformed = [DateTime]::Now
31+
}
32+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingInvokeExpression', '', Justification = 'Pester testing file')]
5+
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidOverwritingBuiltInCmdlets', '', Justification = 'Pester testing file')]
6+
[CmdletBinding()]
7+
param()
8+
9+
BeforeAll {
10+
$Script:parentPath = (Split-Path -Parent $PSScriptRoot)
11+
. $Script:parentPath\Get-VulnerabilityStatus.ps1
12+
}
13+
14+
Describe "Testing Get-VulnerabilityStatus" {
15+
16+
Context "Returns correct status when server is vulnerable and unmitigated" {
17+
18+
It "Should report CodeFixApplied false and MitigationApplied false" {
19+
$definition = [PSCustomObject]@{
20+
Id = "CVE-TEST-001"
21+
Description = "Test vulnerability"
22+
TestVulnerable = { return @{ MitigationApplied = $false; CodeFixApplied = $false } }
23+
}
24+
25+
$result = Get-VulnerabilityStatus -MitigationDefinition $definition
26+
27+
$result.CVE | Should -Be "CVE-TEST-001"
28+
$result.CodeFixApplied | Should -Be $false
29+
$result.MitigationApplied | Should -Be $false
30+
$result.Description | Should -Be "Test vulnerability"
31+
$result.CheckPerformed | Should -Not -BeNullOrEmpty
32+
}
33+
}
34+
35+
Context "Returns correct status when server is patched" {
36+
37+
It "Should report CodeFixApplied true when security fix is installed" {
38+
$definition = [PSCustomObject]@{
39+
Id = "CVE-TEST-002"
40+
Description = "Test patched"
41+
TestVulnerable = { return @{ MitigationApplied = $false; CodeFixApplied = $true } }
42+
}
43+
44+
$result = Get-VulnerabilityStatus -MitigationDefinition $definition
45+
46+
$result.CVE | Should -Be "CVE-TEST-002"
47+
$result.CodeFixApplied | Should -Be $true
48+
$result.MitigationApplied | Should -Be $false
49+
}
50+
}
51+
52+
Context "Returns correct status when mitigation is applied but not patched" {
53+
54+
It "Should report MitigationApplied true and CodeFixApplied false" {
55+
$definition = [PSCustomObject]@{
56+
Id = "CVE-TEST-003"
57+
Description = "Test mitigated"
58+
TestVulnerable = { return @{ MitigationApplied = $true; CodeFixApplied = $false } }
59+
}
60+
61+
$result = Get-VulnerabilityStatus -MitigationDefinition $definition
62+
63+
$result.MitigationApplied | Should -Be $true
64+
$result.CodeFixApplied | Should -Be $false
65+
}
66+
}
67+
68+
Context "Returns correct status when both patched and mitigated" {
69+
70+
It "Should report both CodeFixApplied and MitigationApplied as true" {
71+
$definition = [PSCustomObject]@{
72+
Id = "CVE-TEST-004"
73+
Description = "Test both applied"
74+
TestVulnerable = { return @{ MitigationApplied = $true; CodeFixApplied = $true } }
75+
}
76+
77+
$result = Get-VulnerabilityStatus -MitigationDefinition $definition
78+
79+
$result.MitigationApplied | Should -Be $true
80+
$result.CodeFixApplied | Should -Be $true
81+
}
82+
}
83+
84+
Context "Handles TestVulnerable exceptions gracefully" {
85+
86+
It "Should default to both false when TestVulnerable throws" {
87+
Mock Write-Warning { }
88+
89+
$definition = [PSCustomObject]@{
90+
Id = "CVE-TEST-005"
91+
Description = "Test error"
92+
TestVulnerable = { throw "Cannot determine Exchange version" }
93+
}
94+
95+
$result = Get-VulnerabilityStatus -MitigationDefinition $definition
96+
97+
$result.MitigationApplied | Should -Be $false
98+
$result.CodeFixApplied | Should -Be $false
99+
$result.CVE | Should -Be "CVE-TEST-005"
100+
Should -Invoke Write-Warning -Times 1 -ParameterFilter {
101+
$Message -like "*Cannot determine Exchange version*"
102+
}
103+
}
104+
}
105+
106+
Context "Result object has all expected properties" {
107+
108+
It "Should return CVE, MitigationApplied, CodeFixApplied, Description, and CheckPerformed" {
109+
$definition = [PSCustomObject]@{
110+
Id = "CVE-TEST-006"
111+
Description = "Property check"
112+
TestVulnerable = { return @{ MitigationApplied = $false; CodeFixApplied = $true } }
113+
}
114+
115+
$result = Get-VulnerabilityStatus -MitigationDefinition $definition
116+
117+
$result.PSObject.Properties.Name | Should -Contain "CVE"
118+
$result.PSObject.Properties.Name | Should -Contain "MitigationApplied"
119+
$result.PSObject.Properties.Name | Should -Contain "CodeFixApplied"
120+
$result.PSObject.Properties.Name | Should -Contain "Description"
121+
$result.PSObject.Properties.Name | Should -Contain "CheckPerformed"
122+
}
123+
124+
It "Should have a CheckPerformed timestamp close to now" {
125+
$definition = [PSCustomObject]@{
126+
Id = "CVE-TEST-007"
127+
Description = "Timestamp check"
128+
TestVulnerable = { return @{ MitigationApplied = $false; CodeFixApplied = $false } }
129+
}
130+
131+
$before = [DateTime]::Now
132+
$result = Get-VulnerabilityStatus -MitigationDefinition $definition
133+
134+
$result.CheckPerformed | Should -BeGreaterOrEqual $before
135+
}
136+
}
137+
}

0 commit comments

Comments
 (0)