Skip to content

Commit 4bba1b3

Browse files
Test Metadata Update: Stage 1 (#456)
* Solving test findings, general code cleanup * Undo breaking the path system * Fixing encoding issue * Fixed check in environment without MacOS policies * Added Test Metadata attribute * Updating Test Scaffolding * reading the test metadata * Metadata tooling wrap up * Updated Assessments to use new attribute * Bugfix command rename missing Renamed the command when moving it to the module internals, failed to update its name in the "Set"-Command
1 parent f16bbae commit 4bba1b3

169 files changed

Lines changed: 2513 additions & 167 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
#requires -Modules Refactor
2+
function Set-TestMetadata {
3+
<#
4+
.SYNOPSIS
5+
Update the test metadata included in the command attributes.
6+
7+
.DESCRIPTION
8+
Update the test metadata included in the command attributes.
9+
Uses AST-Parsing to correctly insert & update the attribute used to maintain configuration data in our tests.
10+
11+
.PARAMETER Test
12+
The Test ID to update. Used to select the command to modify.
13+
14+
.PARAMETER TestId
15+
The actual ID to insert into the attribute.
16+
17+
.PARAMETER Category
18+
What category the test belongs to.
19+
20+
.PARAMETER ImplementationCost
21+
How high is the cost to implement the finding this test reports?
22+
23+
.PARAMETER Pillar
24+
What pillar does the test belong to?
25+
26+
.PARAMETER RiskLevel
27+
How high is the risk of not mitigating this?
28+
29+
.PARAMETER SfiPillar
30+
What SFI pillar does the test map to?
31+
32+
.PARAMETER TenantType
33+
What kind of tenant can apply this test?
34+
35+
.PARAMETER Title
36+
The title the test uses in the test summary table.
37+
38+
.PARAMETER UserImpact
39+
How high is the impact to users, when implementing this?
40+
41+
.EXAMPLE
42+
PS C:\> Set-TestMetadata -Test 21771 -RiskLevel High
43+
44+
Updates the Risk Level of test 21771 to high.
45+
#>
46+
[CmdletBinding()]
47+
param (
48+
[Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
49+
[string[]]
50+
$Test,
51+
52+
[string]
53+
$TestId,
54+
55+
[string]
56+
$Category,
57+
58+
[ValidateSet('Low', 'Medium', 'High')]
59+
[string]
60+
$ImplementationCost,
61+
62+
[string]
63+
$Pillar,
64+
65+
[ValidateSet('Low', 'Medium', 'High')]
66+
[string]
67+
$RiskLevel,
68+
69+
[string]
70+
$SfiPillar,
71+
72+
[ValidateSet('Workforce', 'External')]
73+
[string[]]
74+
$TenantType,
75+
76+
[string]
77+
$Title,
78+
79+
[ValidateSet('Low', 'Medium', 'High')]
80+
[string]
81+
$UserImpact
82+
)
83+
begin {
84+
. "$PSScriptRoot\..\..\src\powershell\private\tests-metadata\Get-ZtTestMetadata.ps1"
85+
86+
#region Utility Functions
87+
function New-ZtiAttribute {
88+
[CmdletBinding()]
89+
param (
90+
[Parameter(Mandatory = $true)]
91+
[hashtable]
92+
$Update,
93+
94+
[Parameter(Mandatory = $true)]
95+
$Current,
96+
97+
[Parameter(Mandatory = $true)]
98+
$Definition
99+
)
100+
101+
$data = @{}
102+
foreach ($label in $Definition.Keys) {
103+
$data[$label] = $Current.$label
104+
}
105+
foreach ($pair in $Update.GetEnumerator()) {
106+
$data[$pair.Key] = $pair.Value
107+
}
108+
109+
$text = [System.Text.StringBuilder]::new()
110+
$null = $text.AppendLine('[ZtTest(')
111+
foreach ($pair in $Definition.GetEnumerator()) {
112+
switch ($pair.Value) {
113+
'string[]' {
114+
$entries = foreach ($item in $data[$pair.Key]) {
115+
"'$([System.Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent($item))'"
116+
}
117+
$valueText = "($($entries -join ','))"
118+
if (-not $entries) {
119+
$valueText = '$null'
120+
}
121+
}
122+
'int' {
123+
$valueText = '{0}' -f ($data[$pair.Key] -as [int])
124+
}
125+
# Default to string
126+
default {
127+
$valueText = "'$([System.Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent($data[$pair.Key]))'"
128+
}
129+
}
130+
$line = "`t$($pair.Key) = $($valueText),"
131+
if ($pair.Key -eq $($Definition.Keys)[-1]) {
132+
$line = $line.TrimEnd(',')
133+
}
134+
$null = $text.AppendLine($line)
135+
}
136+
$null = $text.Append(')]')
137+
138+
$data.Text = $text.ToString()
139+
[PSCustomObject]$data
140+
}
141+
function Update-ZtiAttribute {
142+
[CmdletBinding()]
143+
param (
144+
[Parameter(Mandatory = $true)]
145+
[Refactor.Component.AstResult]
146+
$Command,
147+
148+
[Parameter(Mandatory = $true)]
149+
$Attribute
150+
)
151+
152+
# 1) Grab Existing Attribute (if exists)
153+
$testAttribute = @($Command.Ast.Body.ParamBlock.Attributes).Where{ $_.TypeName.FullName -eq 'ZtTest' }[0]
154+
$bindingAttribute = @($Command.Ast.Body.ParamBlock.Attributes).Where{ $_.TypeName.FullName -eq 'CmdletBinding' }[0]
155+
$baseOffset = $Command.Ast.Extent.StartOffset
156+
$addBinding = $null -eq $bindingAttribute
157+
158+
# 2A) If so, figure out offsets to insert into and indentation to use.
159+
if ($testAttribute) {
160+
$startAt = $testAttribute.Extent.StartOffset - $baseOffset
161+
$resumeAt = $testAttribute.Extent.EndOffset - $baseOffset
162+
$baseIndentation = $Command.Text.SubString($startAt - $testAttribute.Extent.StartColumnNumber + 1, $testAttribute.Extent.StartColumnNumber - 1)
163+
}
164+
165+
# 2B) If not, figure out offsets & indentation above CmdletBinding
166+
elseif ($bindingAttribute) {
167+
$startAt = $bindingAttribute.Extent.StartOffset - $baseOffset
168+
$resumeAt = $startAt
169+
$baseIndentation = $Command.Text.SubString($startAt - $bindingAttribute.Extent.StartColumnNumber + 1, $bindingAttribute.Extent.StartColumnNumber - 1)
170+
}
171+
# 2C) ... or just above the param
172+
else {
173+
$paramBlock = $Command.Ast.Body.ParamBlock
174+
$startAt = $paramBlock.Extent.StartOffset - $baseOffset
175+
$resumeAt = $startAt
176+
$baseIndentation = $Command.Text.SubString($startAt - $paramBlock.Extent.StartColumnNumber + 1, $paramBlock.Extent.StartColumnNumber - 1)
177+
}
178+
179+
# 3) Update Text with indentation
180+
$firstLine = $true
181+
$lines = foreach ($line in $Attribute.Text -split "`n") {
182+
if ($firstLine) {
183+
$firstLine = $false
184+
$line
185+
continue
186+
}
187+
188+
"$($baseIndentation)$($line)"
189+
}
190+
if ($addBinding) {
191+
$lines += "$($baseIndentation)[CmdletBinding()]"
192+
}
193+
$newText = $lines -join "`n"
194+
if (-not $testAttribute) {
195+
$newText += "`n$($baseIndentation)"
196+
}
197+
198+
# 4) Update Command Object
199+
$Command.NewText = $Command.Text.SubString(0, $startAt) + $newText + $Command.Text.SubString($resumeAt)
200+
$Command
201+
}
202+
#endregion Utility Functions
203+
204+
$properties = [ordered]@{
205+
Category = 'string'
206+
ImplementationCost = 'string'
207+
Pillar = 'string'
208+
RiskLevel = 'string'
209+
SfiPillar = 'string'
210+
TenantType = 'string[]'
211+
TestId = 'int'
212+
Title = 'string'
213+
UserImpact = 'string'
214+
}
215+
}
216+
process {
217+
foreach ($testID in $Test) {
218+
#region Preparation
219+
try {
220+
$metaData = Get-ZtTestMetadata -Test $testID -ErrorAction Stop
221+
}
222+
catch {
223+
$PSCmdlet.WriteError($_)
224+
continue
225+
}
226+
227+
$toUpdate = $PSBoundParameters | ConvertTo-PSFHashtable -Include $properties.Keys
228+
foreach ($key in $($toUpdate.Keys)) {
229+
if ($metaData.$key -eq $toUpdate[$key]) {
230+
$toUpdate.Remove($key)
231+
}
232+
}
233+
234+
if ($toUpdate.Count -lt 1) {
235+
Write-PSFMessage -Level Verbose -Message 'Skipping {0}: Nothing to change' -StringValues $testID -Target $testID
236+
continue
237+
}
238+
#endregion Preparation
239+
240+
$intendedAttribute = New-ZtiAttribute -Update $toUpdate -Current $metaData -Definition $properties
241+
$command = Read-ReAstComponent -LiteralPath $metaData.Path -Select FunctionDefinitionAst | Where-Object {
242+
$_.Ast.Name -eq "Test-Assessment-$testID"
243+
}
244+
$updCommand = Update-ZtiAttribute -Command $command -Attribute $intendedAttribute
245+
$updCommand | Write-ReAstComponent
246+
}
247+
}
248+
}

build/powershell/Install-Prerequisites.ps1

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ param (
66

77
Invoke-WebRequest 'https://raw.githubusercontent.com/PowershellFrameworkCollective/PSFramework.NuGet/refs/heads/master/bootstrap.ps1' | Invoke-Expression
88

9-
$modules = @("Pester", "PSScriptAnalyzer")
9+
$modules = @(
10+
"Pester" # Test Framework, runs the tests
11+
"PSScriptAnalyzer" # PowerShell Best Practices analyzer, will be used in tests
12+
'Refactor' # Used to update the metadata for individual test commands
13+
)
1014

1115
# Automatically add missing dependencies
1216
$data = Import-PowerShellDataFile -Path "$PSScriptRoot\..\..\src\powershell\ZeroTrustAssessmentV2.psd1"

build/scaffold/Create-TestScaffold.ps1

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,11 @@ function Get-DefaultLevel
1515
}
1616

1717
# Import the CSV file
18-
$csv = Import-Csv -Path .\ado-tests.csv
18+
$csv = Import-Csv -Path "$PSScriptRoot\ado-tests.csv"
1919

2020
# go through each row in the CSV file, check if the test exists in the ./src/private/tests directory ending with the same .id.ps1 format
2121
# if it does not exist, create the file with the test scaffold
22-
$createdTests = @()
23-
24-
foreach ($row in $csv) {
22+
$createdTests = foreach ($row in $csv) {
2523
$testId = $row.ID
2624
$testTitle = $row.Title
2725
Write-Host "Processing test $testId - $testTitle"
@@ -33,25 +31,28 @@ foreach ($row in $csv) {
3331

3432
# use wildcard for the Test-name prefix
3533
# Check if file name ends with .$testId.ps1 and if it does not exist, create the file
36-
$testFile = "../../src/powershell/private/tests/*.$testId.ps1"
34+
$testFile = "../../src/powershell/tests/*.$testId.ps1"
3735

3836
if (-not (Test-Path $testFile)) {
3937
Write-Host "Creating test file $testFile"
40-
$createdTests += "Test-Assessment-$testId"
41-
$testContent = Get-Content -Path .\Test-Template.ps1
38+
39+
$testContent = Get-Content -Path "$PSScriptRoot\Test-Template.ps1"
4240
$testContent = $testContent -replace "%testid%", $testId
4341
$testContent = $testContent -replace "%testTitle%", $testTitle
4442
$testContent = $testContent -replace "%risk%", $risk
4543
$testContent = $testContent -replace "%userImpact%", $userImpact
4644
$testContent = $testContent -replace "%implementationCost%", $implementationCost
45+
$testContent = $testContent -replace "%category%", $row.Categories
4746

4847
$fileName = "Test-Assessment.$testId"
49-
$testPsFile = "../../src/powershell/private/tests/$fileName.ps1"
50-
$testContent | Out-File -FilePath $testPsFile
48+
$testPsFile = "../../src/powershell/tests/$fileName.ps1"
49+
$testContent | Set-Content -Path $testPsFile
50+
51+
$markdownFile = "../../src/powershell/tests/$fileName.md"
52+
$markdownContent = Get-Content -Path "$PSScriptRoot\Test-Template.md"
53+
$markdownContent | Set-Content -Path $markdownFile
5154

52-
$markdownFile = "../../src/powershell/private/tests/$fileName.md"
53-
$markdownContent = Get-Content -Path .\Test-Template.md
54-
$markdownContent | Out-File -FilePath $markdownFile
55+
"Test-Assessment-$testId"
5556
}
5657
}
5758

build/scaffold/Test-Template.ps1

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@
44
#>
55

66
function Test-Assessment-%testId%{
7+
[ZtTest(
8+
Category = '%category%',
9+
ImplementationCost = '%implementationCost%',
10+
Pillar = 'Identity',
11+
RiskLevel = '%risk%',
12+
SfiPillar = "Protect identities and secrets",
13+
TenantType = ('Workforce', 'External'),
14+
TestId = %testid%,
15+
Title = "%testTitle%",
16+
UserImpact = '%userImpact%'
17+
)]
718
[CmdletBinding()]
819
param()
920

0 commit comments

Comments
 (0)