# BSD 3-Clause License Copyright (c) 2023, FB Pro GmbH All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #> $webIcon = @" "@ $mailIcon = @" "@ $phoneIcon = @" "@ <# icons from https://www.svgrepo.com/ #> enum AuditInfoStatus { True False Warning None Error } $ScriptRoot = Split-Path -Parent $PSCommandPath $Settings = Import-PowerShellDataFile -Path "$ScriptRoot\Settings.psd1" $ModuleVersion = (Import-PowerShellDataFile -Path "$ScriptRoot\ATAPHtmlReport.psd1").ModuleVersion $StatusValues = 'True', 'False', 'Warning', 'None', 'Error' $AuditProperties = @{ Name = 'Id' }, @{ Name = 'Task' }, @{ Name = 'Message' }, @{ Name = 'Status' } #read in all information needed for Mitre Attack Mapping from json file $global:CISToAttackMappingData = Get-Content -Raw "$PSScriptRoot\resources\CISToAttackMappingData.json" | ConvertFrom-Json function Get-MitreMappingMetaData { <# .SYNOPSIS Returns the specified metadata to the mapping data .EXAMPLE Get-MitreMappingMetaData -Get BasedOn Get-MitreMappingMetaData BasedOn #> param( [Parameter(Mandatory)][ValidateSet('Version', 'BasedOn', 'Compatible')] [string]$Get ) return $CISToAttackMappingData.'MappingMetaData'.$Get } function Get-MitreTacticName { <# .SYNOPSIS Returns the corresponding name for a given Mitre Tactic Id .EXAMPLE Get-MitreTacticName TacticId 'TA0043' #> param( [Parameter(Mandatory = $true)] [string] $TacticId ) # $CISToAttackMappingData[AttackTactics][$tacticId] cannot be used because CISToAttackMappingData is a customObject and not a map return $CISToAttackMappingData.'AttackTactics'.$tacticId } function Get-MitreTactics { <# .SYNOPSIS Returns a List of Mitre Tactic IDs for a given Mitre Technique Id .EXAMPLE Get-MitreTactics -TechniqueID 'T1133' #> param( [Parameter(Mandatory = $true)] $TechniqueID ) return $CISToAttackMappingData.'TechniquesToTactis'.$TechniqueID } function Get-MitreTechniqueName { <# .SYNOPSIS Returns the name of a Mitre technique for a given Mitre Technique Id .EXAMPLE Get-MitreTechniqueName -TechniqueID 'T1133' #> param( [Parameter(Mandatory = $true)] $TechniqueID ) return $CISToAttackMappingData.'AttackTechniques'.$TechniqueID.'name' } function Test-CompatibleMitreReport { <# .SYNOPSIS Returns if the report is compatible with the current mitre heatmap .EXAMPLE Test-CompatibleMitreReport -Title "Windows 10 Report" -os "Win32NT" #> param( [Parameter(Mandatory = $true)] $Title, [Parameter(Mandatory = $true)] $os ) if (($Title -eq "Windows 10 Report" -or $Title -eq "Windows 11 Report" -or $Title -eq "Windows Server 2019 Audit Report" -or $Title -eq "Windows Server 2022 Audit Report") -and $os -match "Win32NT") { return $true } else { return $false } } function Get-MitreTechniqueCategories { <# .SYNOPSIS Returns the categories of a Mitre technique in order to apply filters to the report. Will return a string that provides all categories stored in the JSON file. .EXAMPLE Get-MitreTechniqueCategories -TechniqueID 'T1133' #> param( [Parameter(Mandatory = $true)] $TechniqueID ) return $CISToAttackMappingData.'AttackTechniques'.$TechniqueID.'categories' } class MitreMap { [System.Collections.Generic.Dictionary[string, [System.Collections.Generic.Dictionary[string, [System.Collections.Generic.Dictionary[string, AuditInfoStatus]]]]]] $Map MitreMap() { $this.Map = @{} #read in techniques from json-file $techniques = $global:CISToAttackMappingData.'AttackTechniques' $tactics = $global:CISToAttackMappingData.'AttackTactics' foreach ($tacitc in $tactics.psobject.properties.name) { $this.Map[$tacitc] = @{} } #add all techniques and tactics to map foreach ($technique in $techniques.psobject.properties.name) { $tactics = Get-MitreTactics -TechniqueID $techniques.$technique.'ID' foreach ($tactic in $tactics) { if ($null -eq $this.Map[$tactic][$techniques.$technique.'ID']) { $this.Map[$tactic][$techniques.$technique.'ID'] = @{} } } } } [void] Add($tactic, $technique, $id, $value) { if ($tactic -and $technique -and $id -and $null -ne $value -and $tactic.GetType().Name -eq 'String' -and $technique.GetType().Name -eq 'String' -and $id.GetType().Name -eq 'String' -and $value.GetType().Name -eq 'AuditInfoStatus') { if ($null -eq $this.Map[$tactic]) { $this.Map[$tactic] = @{} } if ($null -eq $this.Map[$tactic][$technique]) { $this.Map[$tactic][$technique] = @{} } $this.Map[$tactic][$technique][$id] = $value } else { if (!$tactic) { Write-Error -Message 'Could not add value to Map. $tactic is $null or empty' -Category InvalidType } elseif (!$technique) { Write-Error -Message 'Could not add value to Map. $technique is $null or empty' -Category InvalidType } elseif (!$id) { Write-Error -Message 'Could not add value to Map. $id is $null or empty' -Category InvalidType } elseif ($null -eq $value) { Write-Error -Message 'Could not add value to Map. $value is $null' -Category InvalidType } else { Write-Error -Message 'Could not add value to Map' -Category InvalidType } } } [void] Print() { foreach ($tactic in $this.Map.Keys) { Write-Host "$tactic = " foreach ($technique in $this.Map[$tactic].Keys) { Write-Host " $technique = " foreach ($id in $this.Map[$tactic][$technique].Keys) { Write-Host " $id = $($this.Map[$tactic][$technique][$id])" } } } } } function get-MitreLink { <# .SYNOPSIS Creates a url which points to the documentation of mitre for a given tactic or technique .PARAMETER id id of the tactic or technique .PARAMETER type one of 'tactic', 'technique' or 'mitigations' .EXAMPLE get-MitreLink -type technique -id 'T1548' | Should -Be 'https://attack.mitre.org/techniques/T1548/' #> param( [string] $id, [Parameter(Mandatory)][ValidateSet('tactics', 'techniques', 'mitigations')] [string]$type ) $url = 'https://attack.mitre.org/' $url += "$type/$id/" return $url } function Join-ATAPReportStatus { [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory = $true)] [string[]] $Statuses ) if ($Statuses -contains 'False') { return 'False' } elseif ($Statuses -contains 'Error') { return 'Warning' } elseif ($Statuses -contains 'Warning') { return 'Warning' } elseif ($Statuses -contains 'True') { return 'True' } else { return 'None' } } function htmlElement { param( [Parameter(Mandatory = $true, Position = 0)] [string] $ElementName, [Parameter(Mandatory = $true, Position = 1)] [hashtable] $Attributes, [Parameter(Mandatory = $true, Position = 2)] [scriptblock] $Children ) $htmlAttributes = @() foreach ($attribute in $Attributes.GetEnumerator()) { $htmlAttributes += '{0}="{1}"' -f $attribute.Name, $attribute.Value } [string[]]$htmlChildren = & $Children return '<{0} {1}>{2}{0}>' -f $ElementName, ($htmlAttributes -join ' '), ($htmlChildren -join '') } function Get-SectionStatus { param( [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [Alias('AuditInfos')] [array] $ConfigAudits, [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [array] $Subsections ) process { $allStatuses = @() if ($null -ne $ConfigAudits) { $allStatuses += $ConfigAudits.Status } if ($null -ne $Subsections) { foreach ($subsection in $Subsections) { $allStatuses += $subsection | Get-SectionStatus } } return Join-ATAPReportStatus $allStatuses } } function Get-HtmlClassFromStatus { param( [Parameter(Mandatory = $true)] [string] $Status ) process { switch ($Status) { 'True' { 'passed' } 'False' { 'failed' } 'Warning' { 'warning' } 'None' { 'none' } 'Error' { 'error' } Default { "" } } } } function Convert-SectionTitleToHtmlId { param( [Parameter(Mandatory = $true)] [string] $Title ) $charMap = { switch ($_) { ' ' { "-" } '-' { "--" } Default { $_ } } } return ([char[]]$Title | ForEach-Object $charMap) -join '' } function CreateToc { param( [Parameter(Mandatory = $true)] $title ) htmlElement 'li' @{} { htmlElement 'a' @{ href = "#$($title)" } { "$($title)" } } } function CreateHashTable { htmlElement 'div'@{id = "hashTableDiv" } { htmlElement 'h2' @{style = "margin-top: 0;" } { "Overall integrity" } htmlElement 'p' @{} { "This table outlines integrity checksums for each hardening recommendation. This allows for a quick comparison between reports by simply comparing provided hash values." } htmlElement 'table'@{ id = "hashTable" } { htmlElement 'thead' @{} { htmlElement 'tr' @{} { htmlElement 'th' @{style = "border: 1px solid var(--color-dark-gray); border-collapse: collapse; background-color: var(--color-dark-gray);" } { "Integrity Check for following scopes" } htmlElement 'th' @{style = "border: 1px solid var(--color-dark-gray); border-collapse: collapse; background-color: var(--color-dark-gray);" } { "Checksum (SHA-256)" } } } htmlElement 'tbody' @{id = "hashTableBody" } { htmlElement 'tr' @{} { #Scope htmlElement 'td' @{style = "border: 1px solid var(--color-dark-gray); border-collapse: collapse;vertical-align: middle; " } { "Overall integrity check" } #Checksum htmlElement 'td' @{style = "border: 1px solid var(--color-dark-gray); border-collapse: collapse; " } { htmlElement 'p' @{style = "padding-right: 20px;" } { "$($hashtable_sha256.Get_Item($Title))" } } } # $index = 0 # $trColorSwitch = 0 # foreach ($section in $Sections) { # if ($trColorSwitch -eq 0) { # htmlElement 'tr' @{style = "border: 1px solid #d2d2d2; border-collapse: collapse; background-color: #efefef;" } { # #Scope # htmlElement 'td' @{style = "border: 1px solid #d2d2d2; border-collapse:; vertical-align: middle; " } { "$($section.Title)" } # #Checksum # htmlElement 'td' @{style = "border: 1px solid #d2d2d2; border-collapse: collapse; " } { # htmlElement 'p' @{style = "padding-right: 20px;" } { "$($hashtable_sha256.Get_Item($section.Title))" } # } # } # $trColorSwitch = 1 # } # else { # htmlElement 'tr' @{style = "border: 1px solid #d2d2d2; border-collapse: collapse;" } { # #Scope # htmlElement 'td' @{style = "border: 1px solid #d2d2d2; border-collapse:; vertical-align: middle; " } { "$($section.Title)" } # #Checksum # htmlElement 'td' @{style = "border: 1px solid #d2d2d2; border-collapse: collapse; " } { # htmlElement 'p' @{style = "padding-right: 20px;" } { "$($hashtable_sha256.Get_Item($section.Title))" } # } # } # $trColorSwitch = 0 # } # $index += 1 # } $index = 0 foreach ($section in $Sections) { $trColorSwitch += 1 $background = "" if ($index%2 -eq 0) { $background = "background-color: var(--color-light-gray);" }else{ $background = "" } htmlElement 'tr' @{style = "border: 1px solid var(--color-dark-gray); border-collapse: collapse;$($background)" } { #Scope htmlElement 'td' @{style = "border: 1px solid var(--color-dark-gray); border-collapse:; vertical-align: middle; " } { "$($section.Title)" } #Checksum htmlElement 'td' @{style = "border: 1px solid var(--color-dark-gray); border-collapse: collapse; " } { htmlElement 'p' @{style = "padding-right: 20px;" } { "$($hashtable_sha256.Get_Item($section.Title))" } } } $index += 1 } } } } } function CreateReportContent { param( [Parameter(Mandatory = $true)] $tests, [Parameter(Mandatory = $true)] $title ) $amountOfFailedTests = 0 foreach ($test in $tests) { if ($test.Status -eq 'False') { $amountOfFailedTests ++ } } #if at least one test is failed if ($amountOfFailedTests -gt 0) { htmlElement 'h2' @{ id = "$($title)"; class = "severityResultFalse" } { "$($title)" } } else { htmlElement 'h2' @{ id = "$($title)"; class = "severityResultTrue" } { "$($title)" } } htmlElement 'table' @{class = 'audit-info'; style = 'margin-bottom: 50px; margin-top: 20px;' } { htmlElement 'tbody' @{} { htmlElement 'tr' @{} { htmlElement 'th' @{} { "Id" } htmlElement 'th' @{} { "Task" } htmlElement 'th' @{} { "Message" } htmlElement 'th' @{} { "Status" } } foreach ($test in $tests) { htmlElement 'tr' @{} { htmlElement 'td' @{} { "$($test.Id)" } htmlElement 'td' @{} { "$($test.Task)" } htmlElement 'td' @{} { "$($test.Message)" } htmlElement 'td' @{} { if ($test.Status -eq 'False') { htmlElement 'span' @{class = "severityResultFalse" } { "$($test.Status)" } } elseif ($test.Status -eq 'True') { htmlElement 'span' @{class = "severityResultTrue" } { "$($test.Status)" } } elseif ($test.Status -eq 'None') { htmlElement 'span' @{class = "severityResultNone" } { "$($test.Status)" } } elseif ($test.Status -eq 'Warning') { htmlElement 'span' @{class = "severityResultWarning" } { "$($test.Status)" } } elseif ($test.Status -eq 'Error') { htmlElement 'span' @{class = "severityResultError" } { "$($test.Status)" } } } } } } } } function Get-HtmlTableRow { param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] $Audit ) process { # $properties = $Audit | Get-Member -MemberType Property htmlElement 'tr' @{} { foreach ($property in $AuditProperties) { $value = $Audit | Select-Object -ExpandProperty $property.Name if ($Property.Name -eq 'Status') { $class = Get-HtmlClassFromStatus $Audit.Status $value = htmlElement 'span' @{ class = "auditstatus $class" } { $value } } htmlElement 'td' @{} { $value } } } } } function Get-HtmlToc { param( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $Title, [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [array] $Subsections, [string] $Prefix = '' ) process { $id = Convert-SectionTitleToHtmlId -Title ($Prefix + $Title) htmlElement 'li' @{} { htmlElement 'a' @{ href = "#$id" } { $Title } if ($null -ne $Subsections) { htmlElement 'ul' @{} { foreach ($subsection in $Subsections) { $subsection | Get-HtmlToc -Prefix ($Prefix + $Title) } } } } } } function Merge-CisAuditsToMitreMap { <# .Synopsis Merges the stati of multiple AuditInfos into a 2 dimensional map which can be indexd by the corresponding Mitre tactics an techniques. This allows to simply find out how many Audits where succesfull for a given Mitre technique. The result is a MitreMap Object. .PARAMETER Audit An AuditTest Object containing the Audit results. Multiple can be passed from a pipeline .EXAMPLE $mitreMap = $Sections | Where-Object { $_.Title -eq "CIS Benchmarks" } | ForEach-Object { return $_.SubSections } | ForEach-Object { return $_.AuditInfos } | Merge-CisAuditsToMitreMap $mitreMap.Print() #> param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] $Audit ) Begin { $json = $global:CISToAttackMappingData.'CISAttackMapping' $mitreMap = [MitreMap]::new() } Process { $id = $Audit.Id $technique1 = $json.$id.'Technique1' $technique2 = $json.$id.'Technique2' if ($technique1) { foreach ($tactic in Get-MitreTactics -TechniqueID $technique1) { if ($tactic) { $mitreMap.Add($tactic, $technique1, $id, $Audit.Status) } } } if ($technique2) { foreach ($tactic in Get-MitreTactics -TechniqueID $technique2) { if ($tactic) { $mitreMap.Add($tactic, $technique2, $id, $Audit.Status) } } } } End { return [MitreMap] $mitreMap } } function Get-MitigationsFromFailedTests { <# .Synopsis Returns a map with a array with all Techniques which had a failed test and the Mitigation. .PARAMETER Mappings Is a mitre Mapping from Get-MitigationsFromFailedTests .EXAMPLE $CISAMitigations = $Mappings.Map | Get-MitigationsFromFailedTests #> param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] $Mappings ) Begin { $json = $global:CISToAttackMappingData.'CISAttackMapping' #mapping with Mitigation IDs as keys #array with all techniques where the mititgation is in the cisa paper and a tests failed #mitigation from the cisa paper $CISAMitigationsFromPaper = [ordered]@{ 'M1017' = @{ 'MitreTechniqueIDs' = @() 'Mitigation' = 'Train users to be aware of access or manipulation attempts by an adversary to reduce the risk of successful spear-phishing and social engineering.' } 'M1018' = @{ 'MitreTechniqueIDs' = @() 'Mitigation' = 'Manage the creation, modification, use, and permissions associated to user accounts.' } 'M1021' = @{ 'MitreTechniqueIDs' = @() 'Mitigation' = 'Restrict or block certain websites.' } 'M1027' = @{ 'MitreTechniqueIDs' = @() 'Mitigation' = 'Set and enforce secure password policies for accounts.' } 'M1028' = @{ 'MitreTechniqueIDs' = @() 'Mitigation' = 'Make configuration changes related to the operating system or a common feature of the operating system that result in system hardening against techniques.' } 'M1030' = @{ 'MitreTechniqueIDs' = @() 'Mitigation' = 'Architect sections of the network to isolate critical systems, functions, or resources. Use physical and logical segmentation to prevent access to sensitive systems and information.' } 'M1031' = @{ 'MitreTechniqueIDs' = @() 'Mitigation' = 'Configure Network Intrusion Prevention systems to block malicious file signatures and file types at the network boundary.' } 'M1038' = @{ 'MitreTechniqueIDs' = @() 'Mitigation' = 'Block execution of code on a system.' } 'M1041' = @{ 'MitreTechniqueIDs' = @() 'Mitigation' = 'Use strong encryption mechanisms to protect sensitive data.' } 'M1042' = @{ 'MitreTechniqueIDs' = @() 'Mitigation' = 'Remove or deny access to unnecessary and potentially vulnerable software to prevent abuse by adversaries.' } 'M1057' = @{ 'MitreTechniqueIDs' = @() 'Mitigation' = 'Use a data loss prevention (DLP) strategy to categorize sensitive data, identify data formats indicative of personally identifiable information (PII), and restrict exfiltration of sensitive data.' } } $CISAMitigations = @() $KeysToRemove = @() } Process { foreach ($tactic in $Mappings.Keys) { foreach ($technique in $Mappings[$tactic].Keys) { $Mappings[$tactic][$technique].Keys | #checks for each technique if there is a failed test Where-Object { $Mappings[$tactic][$technique][$_] -eq [AuditInfoStatus]::False } | ForEach-Object { #if the mitigation from the failed test is in ihe mitigation from the cisa paper if ($null -ne $json.$_.'Mitigation1' -and $CISAMitigationsFromPaper.Keys -contains $json.$_.'Mitigation1') { #put the technique in the mapping (no doubles) if ($CISAMitigationsFromPaper[$json.$_.'Mitigation1']['MitreTechniqueIDs'] -notcontains $technique) { $CISAMitigationsFromPaper[$json.$_.'Mitigation1']['MitreTechniqueIDs'] += $technique } #put the mitigation in a separate array (no doubles) if ($CISAMitigations -notcontains $json.$_.'Mitigation1') { $CISAMitigations += $json.$_.'Mitigation1' } } #if the mitigation from the failed test is in ihe mitigation from the cisa paper if ($null -ne $json.$_.'Mitigation2' -and $CISAMitigationsFromPaper.Keys -contains $json.$_.'Mitigation2') { #put the technique in the mapping (no doubles) if ($CISAMitigationsFromPaper[$json.$_.'Mitigation2']['MitreTechniqueIDs'] -notcontains $technique) { $CISAMitigationsFromPaper[$json.$_.'Mitigation2']['MitreTechniqueIDs'] += $technique } #put the mitigation in a separate array (no doubles) if ($CISAMitigations -notcontains $json.$_.'Mitigation2') { $CISAMitigations += $json.$_.'Mitigation2' } } } } } #write keys which where not in the sperat mitigation array in $KeysToRemove beacause you can't delete in a foreach over the object you want to delete from $CISAMitigationsFromPaper.Keys | Where-Object { $CISAMitigations -notcontains $_ } | ForEach-Object { $KeysToRemove += $_ } #delete the keys from $CISAMitigation from paper which were not in the sperate mitigation array $KeysToRemove | ForEach-Object { $CISAMitigationsFromPaper.Remove($_) } } End { return $CISAMitigationsFromPaper } } function ConvertTo-HtmlTable { <# .Synopsis Generates a html table using the mapping keys of the tactics and techniques It also adds the links to the table using the function "get-MitreLink" and colours the cells .Example ConvertTo-HtmlTable $Mappings.map #> param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] $Mappings ) htmlElement 'table' @{id = 'MITRETable' } { htmlElement 'thead' @{id = 'MITREthead' } { htmlElement 'tr' @{} { foreach ($tactic in $Mappings.Keys) { $url = get-MitreLink -type tactics -id $tactic $TacticCount = Get-TacticCounter $tactic $Mappings htmlElement 'td' @{} { $tacticName = Get-MitreTacticName -TacticId $tactic $link = htmlElement 'a' @{href = $url; target = "blank" } { "$tacticName" } htmlElement 'p' @{} { $link + "`n" + "$TacticCount/" + $Mappings[$tactic].Count } } } } } htmlElement 'tbody' @{id = 'MITREtbody' } { htmlElement 'tr' @{} { foreach ($tactic in $Mappings.Keys) { htmlElement 'td' @{} { foreach ($technique in $Mappings[$tactic].Keys) { $successCounter = 0 foreach ($id in $Mappings[$tactic][$technique].Keys) { if ($Mappings[$tactic][$technique][$id] -eq [AuditInfoStatus]::True) { $successCounter++ } } $url = get-MitreLink -type techniques -id $technique $color = Get-ColorValue $successCounter $Mappings[$tactic][$technique].Count $categories = Get-MitreTechniqueCategories -TechniqueID $technique htmlElement 'div' @{class = "MITRETechnique $categories"; style = "background-color: $color; background-clip: border-box" } { htmlElement 'a' @{href = $url; target = "_blank"; class = "tooltip" } { "$technique" htmlElement 'span' @{class = "tooltiptext" } { Get-MitreTechniqueName -TechniqueID $technique } } htmlElement 'span' @{} { ": $successCounter/" + $Mappings[$tactic][$technique].Count } } } } } } } } } function ConvertTo-HtmlCISA { <# .Synopsis Generates a html table using the CISA Mitigation, Mitre Mitigation id and failed techniques .Example ConvertTo-HtmlCISA $CISAMitigations #> param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] $CISAMitigations ) #create CISA table htmlElement 'table' @{id = 'CISATable' } { #create table head with the column CISA Mitigation, MITRE Mitigation ID, MITRE Technique IDs htmlElement 'thead' @{id = 'CISAthead' } { htmlElement 'tr' @{} { htmlElement 'th' @{class = 'CISAMitigationIDs' } { 'ID' } htmlElement 'th' @{class = 'CISAMitigations' } { 'Mitigation Description' } htmlElement 'th' @{class = 'CISAMitreTechniqueIDs' } { 'caused Audit failures' } } } #fill the columns with the information from the $CISAMitigation map htmlElement 'tbody' @{id = 'CISAtbody' } { $KeyOrder = $CISAMitigations.GetEnumerator() | Sort-Object { $_.Value.MitreTechniqueIDs.Count } -Descending $KeyOrder | ForEach-Object { htmlElement 'tr' @{} { htmlElement 'td' @{class = 'CISAMitigationIDs' } { htmlElement 'a' @{href = $(get-MitreLink -type mitigations -id $_.Key); target = "_blank" } { $_.Key } } htmlElement 'td' @{class = 'CISAMitigations' } { htmlElement 'a' @{} { $CISAMitigations[$_.Key]['Mitigation'] } } htmlElement 'td' @{class = 'CISAMitreTechniqueIDs' } { $mitigationsList = $CISAMitigations[$_.Key]['MitreTechniqueIDs'] for ($i = 0; $i -lt $mitigationsList.Length; $i++) { htmlElement 'a' @{href = $(get-MitreLink -type techniques -id $mitigationsList[$i]); target = "_blank" } { $mitigationsList[$i] } } } } } } } } function Get-ColorValue { <# .Synopsis Compares two Integer variables returns true if equal, false if not .Example $colorValue = Get-ColorValue $successCounter $Mappings[$tactic][$technique].Count #> param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [int]$FirstValue, [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [int]$SecondValue ) if ($SecondValue -eq 0) { $result = '#a7a7a7' } else { $successPercentage = ($FirstValue / $SecondValue) switch ($successPercentage) { 1 { $result = '#33cca6' } { $_ -le 0.99 } { $result = '#52CC8F' } { $_ -le 0.89 } { $result = '#70CC78' } { $_ -le 0.79 } { $result = '#8FCC61' } { $_ -le 0.69 } { $result = '#ADCC4A' } { $_ -le 0.59 } { $result = '#CCCC33' } { $_ -le 0.49 } { $result = '#CCA329' } { $_ -le 0.39 } { $result = '#CC7A1F' } { $_ -le 0.29 } { $result = '#CC5214' } { $_ -le 0.19 } { $result = '#CC290A' } { $_ -le 0.09 } { $result = '#cc0000' } } } return $result } function Get-TacticCounter { <# .Synopsis Counts the amount of successful techniques per tactic .Example $TacticCounter = Get-TacticCounter $tactic $Mappings #> param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object]$tactic, [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object]$Mappings ) $TacticCount = 0 foreach ($technique in $Mappings[$tactic].Keys) { $successCounter = 0 foreach ($id in $Mappings[$tactic][$technique].Keys) { if ($Mappings[$tactic][$technique][$id] -eq [AuditInfoStatus]::True) { $successCounter++ } if ($successCounter -eq $Mappings[$tactic][$technique].Count -And $successCounter -gt 0) { $TacticCount++ } } } return $TacticCount } #in the current state the function checks the cis version used for the mapping and used in the Save-ATAPHtmlReport #but the versions don't match so the function prints the status in the HTML but doesn't block Merge-CisAuditsToMitreMap function Compare-EqualCISVersions { <# .Synopsis Returns a boolean, if the $ReportBasedOn and $MitreMappingCompatible Versions can be used together or not. .Parameter $Title The Title of the Report .Parameter $ReportBasedOn The BasedOn information from the report .Parameter $MitreMappingCompatible The Compatible CIS versions of the mitre mapping .Example Compare-EqualCISVersions -Title:$Title -ReportBasedOn:$ReportBasedOn -MitreMappingCompatible:$MitreMappingCompatible #> param( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $Title, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string[]] $ReportBasedOn, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string[]] $MitreMappingCompatible ) $os = [System.Environment]::OSVersion.Platform if (Test-CompatibleMitreReport -Title $Title -os $os) { $ReportBasedOn = $ReportBasedOn | Where-Object { $_ -match 'CIS' } return $($null -ne $ReportBasedOn -and $null -ne $MitreMappingCompatible -and $($ReportBasedOn -in $MitreMappingCompatible)) } return $false } function Get-HtmlReportSection { param( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $Title, [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [string] $Description, [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [alias('AuditInfos')] [array] $ConfigAudits, [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [alias('Sections')] [array] $Subsections, [Parameter(Mandatory = $false)] [string] $Prefix ) process { $id = Convert-SectionTitleToHtmlId -Title ($Prefix + $Title) $sectionStatus = Get-SectionStatus -ConfigAudits $ConfigAudits -Subsections $Subsections $class = Get-HtmlClassFromStatus $sectionStatus htmlElement 'section' @{} { htmlElement 'h1' @{ id = $id } { htmlElement 'span' @{ class = $class } { $Title } htmlElement 'span' @{ class = 'sectionAction collapseButton' } { '-' } htmlElement 'a' @{ href = '#toc'; class = 'sectionAction' } { htmlElement 'span' @{ style = "font-size: 75%;" } { '↑' } } } if ($null -ne $Description) { htmlElement 'p' @{} { $Description } } if ($null -ne $ConfigAudits) { htmlElement 'table' @{ class = 'audit-info' } { htmlElement 'tbody' @{} { htmlElement 'tr' @{} { foreach ($columnName in $AuditProperties.Name) { htmlElement 'th' @{} { $columnName } } } foreach ($configAudit in $ConfigAudits) { $configAudit | Get-HtmlTableRow } } } } if ($null -ne $Subsections) { foreach ($subsection in $Subsections) { $subsection | Get-HtmlReportSection -Prefix ($Prefix + $Title) } } } } } function Get-ATAPHostInformation { $unixOS = [System.Environment]::OSVersion.Platform -eq 'Unix' # returns 'Unix' on Linux and MacOS and 'Win32NT' on Windows, PS v6+ has builtin environment variable for this if ($unixOS) { return @{ "Hostname" = hostname "Operating System" = (Get-Content /etc/os-release | Select-String -Pattern '^PRETTY_NAME=\"(.*)\"$').Matches.Groups[1].Value "Installation Language" = (($(locale) | Where-Object { $_ -match "LANG=" }) -split '=')[1] "Kernel Version" = uname -r "Free physical memory" = "{0:N1} GB" -f (( -split (Get-Content /proc/meminfo | Where-Object { $_ -match 'MemFree:' }))[1] / 1MB) "Free disk space" = "{0:N1} GB" -f ((Get-PSDrive | Where-Object { $_.Name -eq '/' }).Free / 1GB) "System Uptime" = Get-Uptime -p "OS Architecture" = lscpu | awk '/Architecture/ {print $2}' "System Manufacturer" = (dmidecode -t system)[6] | cut -d ':' -f 2 | xargs "System SKU" = (dmidecode -t system)[12] | cut -d ':' -f 2 | xargs "System Serialnumber" = (dmidecode -t system)[9] | cut -d ':' -f 2 | xargs "BIOS Version" = dmidecode -s bios-version } } } function Get-CompletionStatus { param( [string[]] $Statuses, [array]$Sections ) $totalCount = $Statuses.Count $status = @{ TotalCount = $totalCount } #Total completion status foreach ($value in $StatusValues) { $count = ($Statuses | Where-Object { $_ -eq $value }).Count $status[$value] = @{ Count = $count Percent = (100 * ($count / $totalCount)).ToString("0.00", [cultureinfo]::InvariantCulture) } } #Section Total Count $sectionTotalCountHash = @{} foreach ($section in $Sections) { $sectionResult = $section | Select-ConfigAudit | Select-Object -ExpandProperty 'Status' $totalSectionCount = 0 foreach ($value in $StatusValues) { $count = ($sectionResult | Where-Object { $_ -eq $value }).Count $totalSectionCount += $count } $sectionTotalCountHash.Add($section.Title, $totalSectionCount) } #Counts the completion status for each section and each value. Also calculates the percentage. $sectionCountHash = @{} foreach ($section in $Sections) { $sectionResult = $section | Select-ConfigAudit | Select-Object -ExpandProperty 'Status' foreach ($value in $StatusValues) { $count = ($sectionResult | Where-Object { $_ -eq $value }).Count $sectionCountHash.Add($section.Title + $value + "Count", $count) $percent = (100 * ($count / $sectionTotalCountHash[$section.Title])).ToString("0.00", [cultureinfo]::InvariantCulture) $sectionCountHash.Add($section.Title + $value + "Percent", $percent) } } return $status, $sectionTotalCountHash, $sectionCountHash } function Get-OverallComplianceCSS { [CmdletBinding()] [OutputType([string])] param( $completionStatus ) $css = "" $percent = $completionStatus['True'].Percent / 1 if ($percent -gt 50) { $degree = 180 + ((($percent - 50) / 1) * 3.6) $css += ".donut-chart.chart .slice.one {clip: rect(0 200px 100px 0); -webkit-transform: rotate(90deg); transform: rotate(90deg);}" $css += ".donut-chart.chart .slice.two {clip: rect(0 100px 200px 0); -webkit-transform: rotate($($degree)deg); transform: rotate($($degree)deg);}" } else { $degree = 90 + ($percent * 3.6) $css += ".donut-chart.chart .slice.one {clip: rect(0 200px 100px 0); -webkit-transform: rotate($($degree)deg); transform: rotate($($degree)deg);}" $css += ".donut-chart.chart .slice.two {clip: rect(0 100px 200px 0); -webkit-transform: rotate(0deg); transform: rotate(0deg);}" } $css += ".donut-chart.chart .chart-center span:after {content: `"$percent %`";}" return $css } function Select-ConfigAudit { param( [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [Alias('AuditInfos')] [array] $ConfigAudits, [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [array] $Subsections ) process { $results = @() if ($null -ne $ConfigAudits) { $results += $ConfigAudits } if ($null -ne $Subsections) { foreach ($subsection in $Subsections) { $results += $subsection | Select-ConfigAudit } } return $results } } function Get-ATAPHtmlReport { <# .Synopsis Generates an audit report in an html file. .Description The `Get-ATAPHtmlReport` cmdlet collects data from the current machine to generate an audit report. .Parameter Path Specifies the relative path to the file in which the report will be stored. .Example C:\PS> Get-ATAPHtmlReport -Path "MyReport.html" #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory = $true)] [string] $Path, [Parameter(Mandatory = $false)] [hashtable] $HostInformation = (Get-ATAPHostInformation), [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $Title, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $ModuleName, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $AuditorVersion, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string[]] $BasedOn, [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [array] $Sections, [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [string] $LicenseStatus, [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [RSFullReport[]] $RSReport, [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [FoundationReport] $FoundationReport, [Parameter(Mandatory = $false)] [switch] $RiskScore, [Parameter(Mandatory = $false)] [switch] $MITRE, [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [hashtable] $hashtable_sha256, [switch] $ComplianceStatus, [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [SystemInformation] $SystemInformation ) process { Write-Progress -Activity "Creating HTML report head" -Status "Progress:" -PercentComplete 0 $allConfigResults = foreach ($section in $Sections) { $section | Select-ConfigAudit | Select-Object -ExpandProperty 'Status' } $completionStatus, $sectionTotalCountHash, $sectionCountHash = Get-CompletionStatus -Statuses $allConfigResults -sections $Sections # HTML
markup $head = htmlElement 'head' @{} { htmlElement 'meta' @{ charset = 'UTF-8' } { } htmlElement 'meta' @{ name = 'viewport'; content = 'width=device-width, initial-scale=1.0' } { } htmlElement 'meta' @{ 'http-equiv' = 'X-UA-Compatible'; content = 'ie=edge' } { } htmlElement 'title' @{} { "$Title [$(Get-Date)]" } htmlElement 'style' @{} { $cssPath = $ScriptRoot | Join-path -ChildPath "/report.css" Get-Content $cssPath Get-OverallComplianceCSS $completionStatus } htmlElement 'script' @{} { $jsPath = $ScriptRoot | Join-path -ChildPath "/report.js" Get-Content $jsPath } } #Handles Release Date from Releases; Compares Release with this ATAP Version Write-Progress -Activity "Creating HTML report body" -Status "Progress:" -PercentComplete 13 $body = htmlElement 'body' @{onload = "startConditions()" } { # Header htmlElement 'div' @{ class = 'header content' } { htmlElement 'div' @{ id = "logo" } { htmlElement 'a' @{id = "companyLink"; href = "https://www.fb-pro.com/"; target = "_blank" } { htmlElement 'h1' @{id = "companyName" } { "FB PRO GMBH" } htmlElement 'p' @{id = "companySlogan" } { "System Hardening & Secure Configuration" } } } htmlElement 'div' @{ id = "reportInformation" } { htmlElement 'h1' @{} { $Title } $datum = "{0:d}. {1} {2} {3:D2}:{4:D2}" -f (Get-Date).Day, (Get-Date).ToString("MMMM"), (Get-Date).Year, (Get-Date).Hour, (Get-Date).Minute htmlElement 'div' @{} { "Generated on $($datum)" } } } # Main section htmlElement 'div' @{ class = 'main content' } { htmlElement 'div' @{ class = 'host-information' } { # Show compliance status if ($ComplianceStatus) { $sliceColorClass = Get-HtmlClassFromStatus 'True' htmlElement 'div' @{ class = 'card' } { htmlElement 'h2' @{} { 'Compliance status' } htmlElement 'div' @{ class = 'donut-chart chart' } { htmlElement 'div' @{ class = "slice one $sliceColorClass" } { } htmlElement 'div' @{ class = "slice two $sliceColorClass" } { } htmlElement 'div' @{ class = 'chart-center' } { htmlElement 'span' @{} { } } } } } $os = [System.Environment]::OSVersion.Platform ### Risk Checks ### if ($RiskScore) { # Quantity $TotalAmountOfRules = $completionStatus.TotalCount; $AmountOfCompliantRules = 0; $AmountOfNonCompliantRules = 0; $None_Rules = 0; foreach ($value in $StatusValues) { if ($value -eq 'True') { $AmountOfCompliantRules = $completionStatus[$value].Count } #exclude Rules, which are set to None, to make an independent calculation between Compliant and non Compliant if ($value -eq 'None') { $None_Rules = $completionStatus[$value].Count } if ($value -eq 'False') { $AmountOfNonCompliantRules = $completionStatus[$value].Count } } $TotalAmountOfRules = $TotalAmountOfRules - $None_Rules if ($os -match "Win32NT" -and $Title -match "Win") { # percentage of compliance quantity $QuantityCompliance = [math]::round(($AmountOfCompliantRules / $TotalAmountOfRules) * 100, 2); # Variables, which will be evaluated in report.js htmlElement 'div' @{id = "AmountOfNonCompliantRules"; hidden="hidden"} { "$($AmountOfNonCompliantRules)" } htmlElement 'div' @{id = "AmountOfCompliantRules"; hidden="hidden"} { "$($AmountOfCompliantRules)" } htmlElement 'div' @{id = "TotalAmountOfRules"; hidden="hidden"} { "$($TotalAmountOfRules)" } htmlElement 'div' @{id = "QuantityCompliance"; hidden="hidden"} { "$($QuantityCompliance)" } # Severity htmlElement 'div' @{id = "TotalAmountOfSeverityRules"; hidden="hidden"} { "$($RSReport.RSSeverityReport.AuditInfos.Length)" } $AmountOfFailedSeverityRules = 0; foreach ($rule in $RSReport.RSSeverityReport.AuditInfos) { if ($rule.Status -eq "False") { $AmountOfFailedSeverityRules ++; } } htmlElement 'div' @{id = "AmountOfFailedSeverityRules"; hidden="hidden"} { "$($AmountOfFailedSeverityRules)" } } } htmlElement 'div' @{id = 'navigationButtons' } { htmlElement 'button' @{type = 'button'; class = 'navButton selectedNavButton'; id = 'summaryBtn'; onclick = "clickButton('1')" } { "Benchmark Compliance" } htmlElement 'button' @{type = 'button'; class = 'navButton'; id = 'foundationDataBtn'; onclick = "clickButton('5')" } { "Security Base Data" } if ($RiskScore -and ($os -match "Win32NT" -and $Title -match "Win")) { htmlElement 'button' @{type = 'button'; class = 'navButton'; id = 'riskScoreBtn'; onclick = "clickButton('2')" } { "Risk Score" } } if ($MITRE) { if (Test-CompatibleMitreReport -Title $Title -os $os) { htmlElement 'button' @{type = 'button'; class = 'navButton'; id = 'MITREBtn'; onclick = "clickButton('6')" } { "MITRE ATT&CK" } htmlElement 'button' @{type = 'button'; class = 'navButton'; id = 'CISABtn'; onclick = "clickButton('7')" } { "CISA Recommendations" } } } htmlElement 'button' @{type = 'button'; class = 'navButton'; id = 'settingsOverviewBtn'; onclick = "clickButton('4')" } { "Hardening Settings" } htmlElement 'button' @{type = 'button'; class = 'navButton'; id = 'referenceBtn'; onclick = "clickButton('3')" } { "About Us" } } Write-Progress -Activity "Creating settings overview page" -Status "Progress:" -PercentComplete 25 htmlElement 'div' @{class = 'tabContent'; id = 'settingsOverview'; style = 'display:none' } { # Table of Contents htmlElement 'h1' @{ id = 'toc' } { 'Hardening Settings' } CreateHashTable htmlElement 'h2' @{} { "Table Of Contents" } htmlElement 'p' @{} { 'Click the link(s) below for quick access to a report section.' } htmlElement 'ul' @{} { foreach ($section in $Sections) { $section | Get-HtmlToc } } htmlElement 'h2' @{} { "Benchmark Details" } # Report Sections for hardening settings foreach ($section in $Sections) { $section | Get-HtmlReportSection } } Write-Progress -Activity "Creating summary page" -Status "Progress:" -PercentComplete 38 #This div hides/reveals the whole summary section htmlElement 'div' @{class = 'tabContent'; id = 'summary' } { # Host information htmlElement 'h1' @{} { 'Benchmark Compliance' } htmlElement 'div' @{style = "float: left;" } { htmlElement 'p' @{} { "Modules:" htmlElement 'ul' @{} { htmlElement 'div' @{} { "ATAPAuditor version $AuditorVersion" } htmlElement 'div' @{} { "ATAPHtmlReport version $ModuleVersion" } } } htmlElement 'p' @{} { "Test baseline:" htmlElement 'ul' @{} { foreach ($item in $BasedOn) { htmlElement 'li' @{} { $item } } } htmlElement 'div' @{} { "Does your system show low benchmark compliance? Check out our hardening solutions." } } } htmlElement 'div' @{id = 'riskMatrixSummaryArea' } { if ($RiskScore -and ($os -match "Win32NT" -and $Title -match "Win")) { htmlElement 'h2' @{id = 'CurrentRiskScore' } { "Current Risk Score of tested System: " } htmlElement 'h3' @{} { 'For further information, please head to the tab "Risk Score".' } htmlElement 'div' @{id = 'riskMatrixSummary' } { htmlElement 'div' @{id = 'dotSummaryTab'; style = 'display:none'} {} htmlElement 'div' @{id = 'severity' } { htmlElement 'p' @{id = 'severityArea' } { 'Severity' } } htmlElement 'div' @{id = 'quantity' } { htmlElement 'p' @{id = 'quantityArea' } { 'Quantity' } } htmlElement 'div' @{id = 'severityCritical' } { "Critical" } htmlElement 'div' @{id = 'severityHigh' } { "High" } htmlElement 'div' @{id = 'severityMedium' } { "Medium" } htmlElement 'div' @{id = 'severityLow' } { "Low" } htmlElement 'div' @{id = 'quantityCritical' } { "Critical" } htmlElement 'div' @{id = 'quantityHigh' } { "High" } htmlElement 'div' @{id = 'quantityMedium' } { "Medium" } htmlElement 'div' @{id = 'quantityLow' } { "Low" } #colored areas htmlElement 'div' @{id = 'critical_low' } {} htmlElement 'div' @{id = 'high_low' } {} htmlElement 'div' @{id = 'medium_low' } {} htmlElement 'div' @{id = 'low_low' } {} htmlElement 'div' @{id = 'critical_medium' } {} htmlElement 'div' @{id = 'high_medium' } {} htmlElement 'div' @{id = 'medium_medium' } {} htmlElement 'div' @{id = 'low_medium' } {} htmlElement 'div' @{id = 'critical_high' } {} htmlElement 'div' @{id = 'high_high' } {} htmlElement 'div' @{id = 'medium_high' } {} htmlElement 'div' @{id = 'low_high' } {} htmlElement 'div' @{id = 'critical_critical' } {} htmlElement 'div' @{id = 'high_critical' } {} htmlElement 'div' @{id = 'medium_critical' } {} htmlElement 'div' @{id = 'low_critical' } {} } } else { if ($RiskScore) { htmlElement 'h2' @{id = 'CurrentRiskScore' } { "Current Risk Score of tested System:" } htmlElement 'h2' @{id = 'invalidOS' } { "N/A" } htmlElement 'h3' @{} { 'Risk Score calculation implemented for Microsoft Windows OS for now.' } htmlElement 'div' @{id = 'riskMatrixSummary' } { htmlElement 'div' @{id = 'severity' } { htmlElement 'p' @{id = 'severityArea' } { 'Severity' } } htmlElement 'div' @{id = 'quantity' } { htmlElement 'p' @{id = 'quantityArea' } { 'Quantity' } } htmlElement 'div' @{id = 'severityCritical' } { "Critical" } htmlElement 'div' @{id = 'severityHigh' } { "High" } htmlElement 'div' @{id = 'severityMedium' } { "Medium" } htmlElement 'div' @{id = 'severityLow' } { "Low" } htmlElement 'div' @{id = 'quantityCritical' } { "Critical" } htmlElement 'div' @{id = 'quantityHigh' } { "High" } htmlElement 'div' @{id = 'quantityMedium' } { "Medium" } htmlElement 'div' @{id = 'quantityLow' } { "Low" } #colored areas htmlElement 'div' @{id = 'critical_low' } {} htmlElement 'div' @{id = 'high_low' } {} htmlElement 'div' @{id = 'medium_low' } {} htmlElement 'div' @{id = 'low_low' } {} htmlElement 'div' @{id = 'critical_medium' } {} htmlElement 'div' @{id = 'high_medium' } {} htmlElement 'div' @{id = 'medium_medium' } {} htmlElement 'div' @{id = 'low_medium' } {} htmlElement 'div' @{id = 'critical_high' } {} htmlElement 'div' @{id = 'high_high' } {} htmlElement 'div' @{id = 'medium_high' } {} htmlElement 'div' @{id = 'low_high' } {} htmlElement 'div' @{id = 'critical_critical' } {} htmlElement 'div' @{id = 'high_critical' } {} htmlElement 'div' @{id = 'medium_critical' } {} htmlElement 'div' @{id = 'low_critical' } {} } } } } # Benchmark compliance htmlElement 'h1' @{ style = 'clear:both;' } {} htmlElement 'p' @{} { 'A total of {0} tests have been executed.' -f @( $completionStatus.TotalCount ) } # Status percentage gauge htmlElement 'div' @{ class = 'gauge' } { foreach ($value in $StatusValues) { $count = $completionStatus[$value].Count if($count -gt 0){ $htmlClass = Get-HtmlClassFromStatus $value $percent = $completionStatus[$value].Percent htmlElement 'div' @{ class = "gauge-meter $htmlClass" style = "--weight: $count;" #fills the gauge bar to some percent title = "$value $count test(s), $($percent)%" } { } } } } htmlElement 'ol' @{ class = 'gauge-info' } { foreach ($value in $StatusValues) { $count = $completionStatus[$value].Count $htmlClass = Get-HtmlClassFromStatus $value $percent = $completionStatus[$value].Percent htmlElement 'li' @{ class = 'gauge-info-item' } { htmlElement 'span' @{ class = "auditstatus $htmlClass" } { "$($percent)% $value" } "