Skip to content

Commit

Permalink
Create Json Output (#970)
Browse files Browse the repository at this point in the history
* Tweak comments

* Create Merge-JsonOutput function

* Undo pds1 file modification

* Correct config bug for OutJsonFileName

* Mock Merge-JsonOutput function in the RunCached test

* Modify pester tests

* Remove trailing whitespace

* Tweak comments

* Create Merge-JsonOutput function

* Undo pds1 file modification

* Correct config bug for OutJsonFileName

* Mock Merge-JsonOutput function in the RunCached test

* Modify pester tests

* Update PowerShell/ScubaGear/Testing/Unit/PowerShell/Orchestrator/Invoke-RunCached.Tests.ps1

Co-authored-by: Richard Crutchfield <crutchfield@users.noreply.github.com>

* Add switch for MergeJson

* Update README.md with MergeJson

* Change keys to "Tool" and "ToolVersion"

Co-authored-by: David Bui <105074908+buidav@users.noreply.github.com>

---------

Co-authored-by: Richard Crutchfield <crutchfield@users.noreply.github.com>
Co-authored-by: David Bui <105074908+buidav@users.noreply.github.com>
  • Loading branch information
3 people authored Mar 12, 2024
1 parent 91e7937 commit 5258fa7
Show file tree
Hide file tree
Showing 8 changed files with 275 additions and 4 deletions.
24 changes: 22 additions & 2 deletions PowerShell/ScubaGear/Modules/CreateReport/CreateReport.psm1
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
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.
This function creates the individual HTML/json reports using the TestResults.json.
Output will be stored as HTML/json files in the InvidualReports folder in the OutPath Folder.
The report Home page and link tree will be named BaselineReports.html
.Functionality
Internal
Expand Down Expand Up @@ -75,6 +75,12 @@ function New-Report {
"Module Version" = $SettingsExport.module_version
}

# Json version of the product-specific report
$ReportJson = @{
"MetaData" = $MetaData
"Results" = @()
};

$MetaDataTable = $MetaData | ConvertTo-HTML -Fragment
$MetaDataTable = $MetaDataTable -replace '^(.*?)<table>','<table id="tenant-data" style = "text-align:center;">'
$Fragments += $MetaDataTable
Expand Down Expand Up @@ -163,8 +169,22 @@ function New-Report {
$GroupAnchor = New-MarkdownAnchor -GroupNumber $BaselineGroup.GroupNumber -GroupName $BaselineGroup.GroupName
$MarkdownLink = "<a class='control_group' href=`"$($ScubaGitHubUrl)/blob/v$($SettingsExport.module_version)/PowerShell/ScubaGear/baselines/$($BaselineName.ToLower()).md$GroupAnchor`" target=`"_blank`">$Name</a>"
$Fragments += $Fragment | ConvertTo-Html -PreContent "<h2>$Number $MarkdownLink</h2>" -Fragment
$ReportJson.Results += $Fragment
}

# Craft the json report
$ReportJson.ReportSummary = $ReportSummary
$JsonFileName = Join-Path -Path $IndividualReportPath -ChildPath "$($BaselineName)Report.json"
$ReportJson = ConvertTo-Json @($ReportJson) -Depth 3

# ConvertTo-Json for some reason converts the <, >, and ' characters into unicode escape sequences.
# Convert those back to ASCII.
$ReportJson = $ReportJson.replace("\u003c", "<")
$ReportJson = $ReportJson.replace("\u003e", ">")
$ReportJson = $ReportJson.replace("\u0027", "'")
$ReportJson | Out-File $JsonFileName

# Finish building the html report
$Title = "$($FullName) Baseline Report"
$AADWarning = "<p> Note: Conditional Access (CA) Policy exclusions and additional policy conditions
may limit a policy's scope more narrowly than desired. Recommend reviewing matching policies
Expand Down
180 changes: 179 additions & 1 deletion PowerShell/ScubaGear/Modules/Orchestrator.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ function Invoke-SCuBA {
.Parameter OutReportName
The name of the main html file page created in the folder created in OutPath.
Defaults to "BaselineReports".
.Parameter MergeJson
Set switch to merge all json output into a single file and delete the individual files
after merging.
.Parameter OutJsonFileName
If MergeJson is set, the name of the consolidated json created in the folder
created in OutPath. Defaults to "ScubaResults".
.Parameter DisconnectOnExit
Set switch to disconnect all active connections on exit from ScubaGear (default: $false)
.Parameter ConfigFilePath
Expand Down Expand Up @@ -195,6 +201,18 @@ function Invoke-SCuBA {
[string]
$OutReportName = [ScubaConfig]::ScubaDefault('DefaultOutReportName'),

[Parameter(Mandatory = $false, ParameterSetName = 'Configuration')]
[Parameter(Mandatory = $false, ParameterSetName = 'Report')]
[ValidateNotNullOrEmpty()]
[switch]
$MergeJson,

[Parameter(Mandatory = $false, ParameterSetName = 'Configuration')]
[Parameter(Mandatory = $false, ParameterSetName = 'Report')]
[ValidateNotNullOrEmpty()]
[string]
$OutJsonFileName = [ScubaConfig]::ScubaDefault('DefaultOutJsonFileName'),

[Parameter(Mandatory = $true, ParameterSetName = 'Configuration')]
[ValidateNotNullOrEmpty()]
[ValidateScript({
Expand Down Expand Up @@ -250,6 +268,8 @@ function Invoke-SCuBA {
'OutProviderFileName' = $OutProviderFileName
'OutRegoFileName' = $OutRegoFileName
'OutReportName' = $OutReportName
'MergeJson' = $MergeJson
'OutJsonFileName' = $OutJsonFileName
}

$ScubaConfig = New-Object -Type PSObject -Property $ProvidedParameters
Expand Down Expand Up @@ -377,6 +397,19 @@ function Invoke-SCuBA {
'Quiet' = $Quiet
}
Invoke-ReportCreation @ReportParams

if ($MergeJson) {
# Craft the complete json version of the output
$JsonParams = @{
'ProductNames' = $ScubaConfig.ProductNames;
'OutFolderPath' = $OutFolderPath;
'OutProviderFileName' = $ScubaConfig.OutProviderFileName;
'TenantDetails' = $TenantDetails;
'ModuleVersion' = $ModuleVersion;
'OutJsonFileName' = $ScubaConfig.OutJsonFileName;
}
Merge-JsonOutput @JsonParams
}
}
finally {
if ($ScubaConfig.DisconnectOnExit) {
Expand Down Expand Up @@ -410,6 +443,8 @@ $ProdToFullName = @{
SharePoint = "SharePoint Online";
}

$IndividualReportFolderName = "IndividualReports"

function Get-FileEncoding{
<#
.Description
Expand Down Expand Up @@ -735,6 +770,121 @@ function Pluralize {
}
}

function Merge-JsonOutput {
<#
.Description
This function packages all the json output created into a single json file.
.Functionality
Internal
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", '*', IgnoreCase = $false)]
[string[]]
$ProductNames,

[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$OutFolderPath,

[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$OutProviderFileName,

[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[object]
$TenantDetails,

[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$ModuleVersion,

[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$OutJsonFileName
)
process {
try {
# Extract the metadata
$Results = [pscustomobject]@{}
$Summary = [pscustomobject]@{}
$MetaData = [pscustomobject]@{
"TenantId" = $TenantDetails.TenantId;
"DisplayName" = $TenantDetails.DisplayName;
"DomainName" = $TenantDetails.DomainName;
"Product" = "M365";
"Tool" = "ScubaGear";
"ToolVersion" = $ModuleVersion;
}

# Files to delete at the end if no errors are encountered
$DeletionList = @()

# Aggregate the report results and summaries
$IndividualReportPath = Join-Path -Path $OutFolderPath $IndividualReportFolderName -ErrorAction 'Stop'
foreach ($Product in $ProductNames) {
$BaselineName = $ArgToProd[$Product]
$FileName = Join-Path $IndividualReportPath "$($BaselineName)Report.json"
$DeletionList += $FileName
$IndividualResults = Get-Content $FileName | ConvertFrom-Json
$Results | Add-Member -NotePropertyName $BaselineName `
-NotePropertyValue $IndividualResults.Results

# The date is listed under the metadata, no need to include it in the summary as well
$IndividualResults.ReportSummary.PSObject.Properties.Remove('Date')

$Summary | Add-Member -NotePropertyName $BaselineName `
-NotePropertyValue $IndividualResults.ReportSummary
}

# Load the raw provider output
$SettingsFileName = Join-Path -Path $OutFolderPath -ChildPath "$($OutProviderFileName).json"
$DeletionList += $SettingsFileName
$SettingsExport = Get-Content $SettingsFileName -Raw

# Convert the output a json string
$MetaData = ConvertTo-Json $MetaData
$Results = ConvertTo-Json $Results -Depth 3
$Summary = ConvertTo-Json $Summary -Depth 3
$ReportJson = @"
{
"MetaData": $MetaData,
"Summary": $Summary,
"Results": $Results,
"Raw": $SettingsExport
}
"@

# ConvertTo-Json for some reason converts the <, >, and ' characters into unicode escape sequences.
# Convert those back to ASCII.
$ReportJson = $ReportJson.replace("\u003c", "<")
$ReportJson = $ReportJson.replace("\u003e", ">")
$ReportJson = $ReportJson.replace("\u0027", "'")

# Save the file
$JsonFileName = Join-Path -Path $OutFolderPath "$($OutJsonFileName).json" -ErrorAction 'Stop'
$ReportJson | Out-File $JsonFileName

# Delete the now redundant files
foreach ($File in $DeletionList) {
Remove-Item $File
}
}
catch {
$MergeJsonErrorMessage = "Fatal Error involving the Json reports aggregation. `
Ending ScubaGear execution. See the exception message for more details: $($_)"
throw $MergeJsonErrorMessage
}
}
}

function Invoke-ReportCreation {
<#
.Description
Expand Down Expand Up @@ -798,7 +948,6 @@ function Invoke-ReportCreation {
$N = 0
$Len = $ProductNames.Length
$Fragment = @()
$IndividualReportFolderName = "IndividualReports"
$IndividualReportPath = Join-Path -Path $OutFolderPath -ChildPath $IndividualReportFolderName
New-Item -Path $IndividualReportPath -ItemType "Directory" -ErrorAction "SilentlyContinue" | Out-Null

Expand Down Expand Up @@ -1229,6 +1378,12 @@ function Invoke-RunCached {
.Parameter OutReportName
The name of the main html file page created in the folder created in OutPath.
Defaults to "BaselineReports".
.Parameter MergeJson
Set switch to merge all json output into a single file and delete the individual files
after merging.
.Parameter OutJsonFileName
If MergeJson is set, the name of the consolidated json created in the folder
created in OutPath. Defaults to "ScubaResults".
.Parameter DarkMode
Set switch to enable report dark mode by default.
.Example
Expand Down Expand Up @@ -1324,6 +1479,16 @@ function Invoke-RunCached {
[string]
$OutReportName = [ScubaConfig]::ScubaDefault('DefaultOutReportName'),

[Parameter(Mandatory = $false, ParameterSetName = 'Report')]
[ValidateNotNullOrEmpty()]
[switch]
$MergeJson,

[Parameter(Mandatory = $false, ParameterSetName = 'Report')]
[ValidateNotNullOrEmpty()]
[string]
$OutJsonFileName = [ScubaConfig]::ScubaDefault('DefaultOutJsonFileName'),

[Parameter(Mandatory = $false, ParameterSetName = 'Report')]
[ValidateNotNullOrEmpty()]
[ValidateSet($true, $false)]
Expand Down Expand Up @@ -1417,6 +1582,19 @@ function Invoke-RunCached {
}
Invoke-RunRego @RegoParams
Invoke-ReportCreation @ReportParams

if ($MergeJson) {
# Craft the complete json version of the output
$JsonParams = @{
'ProductNames' = $ProductNames;
'OutFolderPath' = $OutFolderPath;
'OutProviderFileName' = $OutProviderFileName;
'TenantDetails' = $TenantDetails;
'ModuleVersion' = $ModuleVersion;
'OutJsonFileName' = $OutJsonFileName;
}
Merge-JsonOutput @JsonParams
}
}
}

Expand Down
5 changes: 5 additions & 0 deletions PowerShell/ScubaGear/Modules/ScubaConfig/ScubaConfig.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class ScubaConfig {
DefaultOutProviderFileName = "ProviderSettingsExport"
DefaultOutRegoFileName = "TestResults"
DefaultOutReportName = "BaselineReports"
DefaultOutJsonFileName = "ScubaResults"
DefaultPrivilegedRoles = @(
"Global Administrator",
"Privileged Role Administrator",
Expand Down Expand Up @@ -117,6 +118,10 @@ class ScubaConfig {
$this.Configuration.OutReportName = [ScubaConfig]::ScubaDefault('DefaultOutReportName')
}

if (-Not $this.Configuration.OutJsonFileName){
$this.Configuration.OutJsonFileName = [ScubaConfig]::ScubaDefault('DefaultOutJsonFileName')
}

return
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ InModuleScope Orchestrator {
Mock -ModuleName Orchestrator Invoke-RunRego {}
function Invoke-ReportCreation {}
Mock -ModuleName Orchestrator Invoke-ReportCreation {}
function Merge-JsonOutput {throw 'this will be mocked'}
Mock -ModuleName Orchestrator Merge-JsonOutput {}
function Disconnect-SCuBATenant {}
Mock -ModuleName Orchestrator Disconnect-SCuBATenant

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ InModuleScope Orchestrator {
Mock -ModuleName Orchestrator Invoke-RunRego {}

Mock -ModuleName Orchestrator Invoke-ReportCreation {}
Mock -ModuleName Orchestrator Merge-JsonOutput {}
function Disconnect-SCuBATenant {throw 'this will be mocked'}
Mock -ModuleName Orchestrator Disconnect-SCuBATenant {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ InModuleScope Orchestrator {
$script:TestSplat.Add('OutFolderName', $ScubaConfig.OutFolderName)
$script:TestSplat.Add('OutReportName', $ScubaConfig.OutReportName)
}
Mock -ModuleName Orchestrator Merge-JsonOutput {
$script:TestSplat.Add('OutJsonFileName', $ScubaConfig.OutJsonFileName)
}
function Disconnect-SCuBATenant {
$script:TestSplat.Add('DisconnectOnExit', $DisconnectOnExit)
}
Expand Down Expand Up @@ -62,7 +65,8 @@ InModuleScope Orchestrator {
}
}
[ScubaConfig]::ResetInstance()
Invoke-SCuBA -ConfigFilePath (Join-Path -Path $PSScriptRoot -ChildPath "orchestrator_config_test.yaml")
Invoke-SCuBA -ConfigFilePath (Join-Path -Path $PSScriptRoot -ChildPath "orchestrator_config_test.yaml")`
-MergeJson
}

It "Verify parameter ""<parameter>"" with value ""<value>""" -ForEach @(
Expand All @@ -75,6 +79,7 @@ InModuleScope Orchestrator {
@{ Parameter = "OutProviderFileName"; Value = "TenantSettingsExport" },
@{ Parameter = "OutRegoFileName"; Value = "ScubaTestResults" },
@{ Parameter = "OutReportName"; Value = "ScubaReports" },
@{ Parameter = "OutJsonFileName"; Value = "ScubaResults" },
@{ Parameter = "Organization"; Value = "sub.domain.com" },
@{ Parameter = "AppID"; Value = "7892dfe467aef9023be" },
@{ Parameter = "CertificateThumbprint"; Value = "8A673F1087453ABC894" }
Expand All @@ -95,7 +100,9 @@ InModuleScope Orchestrator {
-OutFolderName "MyReports" `
-OutProviderFileName "MySettingsExport" `
-OutRegoFileName "RegoResults" `
-MergeJson:$true `
-OutReportName "MyReport" `
-OutJsonFileName "JsonResults" `
-Organization "good.four.us" `
-AppID "1212121212121212121" `
-CertificateThumbprint "AB123456789ABCDEF01" `
Expand All @@ -112,6 +119,7 @@ InModuleScope Orchestrator {
@{ Parameter = "OutProviderFileName"; Value = "MySettingsExport" },
@{ Parameter = "OutRegoFileName"; Value = "RegoResults" },
@{ Parameter = "OutReportName"; Value = "MyReport" },
@{ Parameter = "OutJsonFileName"; Value = "JsonResults" },
@{ Parameter = "Organization"; Value = "good.four.us" },
@{ Parameter = "AppID"; Value = "1212121212121212121" },
@{ Parameter = "CertificateThumbprint"; Value = "AB123456789ABCDEF01" }
Expand Down
Loading

0 comments on commit 5258fa7

Please sign in to comment.