PowerShell operations reporting foundation

Academy-style guide for using the Ops Reporting Foundation helper to turn PowerShell checks into consistent HTML, CSV, JSON, and log artifacts. Start here after creating the helper file, then plug health checks, patch checks, certificate scans, AD hygiene checks, and other collectors into the same reporting pattern.

Good For

  • Learning how the Ops Reporting Foundation is meant to be used
  • Creating a first working HTML/CSV/JSON report from PowerShell
  • Standardizing health, patch, certificate, AD, and infrastructure evidence scripts
  • Building scripts that can stand alone or feed a larger reporting app
  • Preparing future report packs that share one result-row contract

How to Use It

  1. Start with the helper-contract page and build `./templates/reporting/OpsReporting.Foundation.ps1`. This foundation page uses that helper; it does not recreate it.
  2. Create a working folder for your first report test, such as `C:\OpsStackReportingLab`, and run PowerShell from that folder so the relative helper and output paths are easy to follow.
  3. Copy the Command Starter into a file such as `Use-OpsReportingFoundation.FirstRun.ps1` and review the two variables near the top: `$FoundationPath` and `$OutputRoot`.
  4. Run the script once. It performs safe local checks only, converts the findings into normalized report rows, and writes HTML, CSV, JSON, and log artifacts under the output folder.
  5. Review the generated HTML report first, then compare it with the CSV and JSON files. The same result rows should appear in each format.
  6. Study the result-row pattern. One row represents one evaluated check against one target, such as Host plus Disk plus SystemDriveFreePercent.
  7. When adapting another collector, keep its raw evidence output if useful, then add a mapping section that converts important findings into `New-OpsReportResultRow` rows.
  8. Use the same foundation output pattern across health, patch, certificate, AD, Azure Arc, and file-share scripts so they can later be combined into a broader reporting app.

Execution Modes

  • local-rendering
  • imported-results
  • remote-host-list-ready

Inputs and Outputs

Inputs

  • Existing OpsReporting.Foundation.ps1 helper file
  • Local PowerShell session
  • Output folder path
  • Normalized result rows produced by a collector or sample checks
  • Optional raw evidence from other scripts that can be mapped into result rows

Outputs

  • html-report
  • csv
  • json
  • log-file
  • normalized-result-rows
  • operator-notes

Command Starter

Changes system state: review before running

# ---------------------------------------------------------------------
# Use the Ops Reporting Foundation helper
# ---------------------------------------------------------------------
# Purpose:
#   This starter shows how a normal PowerShell collector uses the
#   OpsReporting.Foundation.ps1 helper created by the helper-contract page.
#
# What this script does:
#   1. Verifies the reporting helper exists.
#   2. Loads the helper functions into this PowerShell session.
#   3. Runs a few safe, read-only checks against the local machine.
#   4. Converts each check into a normalized Ops report result row.
#   5. Exports HTML, CSV, JSON, and log artifacts locally.
#
# What this script does NOT do:
#   - It does not change services, reboot systems, patch servers, or modify AD.
#   - It does not send email. Delivery should come after local artifacts exist.
# ---------------------------------------------------------------------

# Stop on terminating errors so setup problems are obvious instead of silent.
$ErrorActionPreference = 'Stop'

# Location of the helper created by the helper-contract page.
# Build this file first: /toolchest/ops-reporting-foundation-helper-contract
$FoundationPath = '.\templates\reporting\OpsReporting.Foundation.ps1'

# Folder where this sample report will write local artifacts.
# The helper creates the folder if it does not already exist.
$OutputRoot = '.\output\ops-report-foundation-first-run'

# The foundation page consumes the helper; it does not create the helper.
# If this check fails, go back and complete the helper-contract page first.
if (-not (Test-Path -Path $FoundationPath)) {
    throw "Missing reporting helper at '$FoundationPath'. Build it first by following /toolchest/ops-reporting-foundation-helper-contract."
}

# Dot-source the helper so its functions are available in this session.
# Dot-sourcing is how this simple starter loads reusable functions from a .ps1 file.
. $FoundationPath

# Confirm the helper exposes the expected public functions before doing any work.
# Failing early here prevents confusing errors later in the script.
$RequiredFunctions = @(
    'New-OpsReportRunContext',
    'Complete-OpsReportRunContext',
    'New-OpsReportResultRow',
    'New-OpsReportSummary',
    'Export-OpsReportArtifacts'
)

foreach ($FunctionName in $RequiredFunctions) {
    Get-Command -Name $FunctionName -ErrorAction Stop | Out-Null
}

# ---------------------------------------------------------------------
# Create the run context
# ---------------------------------------------------------------------
# The run context describes this report execution: what ran, where it ran,
# who ran it, and what input scope was used. Every result row and artifact
# uses the same RunId so the outputs can be tied back to one run.
$Run = New-OpsReportRunContext `
    -CheckName 'FoundationFirstRunLocalHealth' `
    -Environment 'lab' `
    -ExecutionMode 'local-rendering' `
    -InputSource 'local computer read-only sample checks' `
    -RequestedTargetCount 1 `
    -ScriptVersion '0.1.0'

# The foundation model uses one row per evaluated check against one target.
# This array will hold the normalized rows that become the report.
$Results = @()

# Use the local computer as the first target. Later scripts can replace this
# with a server list, AD query, Azure Resource Graph query, or imported CSV.
$TargetName = $env:COMPUTERNAME

try {
    # -----------------------------------------------------------------
    # Check 1: Operating system uptime
    # -----------------------------------------------------------------
    # This is read-only CIM inventory. It does not change the target.
    $Os = Get-CimInstance -ClassName Win32_OperatingSystem
    $UptimeDays = [math]::Round(((Get-Date) - $Os.LastBootUpTime).TotalDays, 1)

    # Decide the status before creating the report row.
    # In a real pack, make thresholds configurable near the top of the script.
    $UptimeStatus = if ($UptimeDays -gt 45) { 'Warning' } else { 'Pass' }

    $Results += New-OpsReportResultRow `
        -RunId $Run.RunId `
        -TargetType 'Host' `
        -TargetName $TargetName `
        -Host $TargetName `
        -CheckGroup 'System' `
        -CheckName 'UptimeDays' `
        -Status $UptimeStatus `
        -Severity $(if ($UptimeStatus -eq 'Warning') { 'Medium' } else { 'Info' }) `
        -ExpectedValue '<= 45 days' `
        -ActualValue "$UptimeDays days" `
        -Finding "Host uptime is $UptimeDays days." `
        -ErrorText ''

    # -----------------------------------------------------------------
    # Check 2: System drive free space
    # -----------------------------------------------------------------
    # This reads local disk capacity and free space. It does not delete files.
    $SystemDrive = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DeviceID='$($env:SystemDrive)'"
    $FreePercent = [math]::Round(($SystemDrive.FreeSpace / $SystemDrive.Size) * 100, 1)

    # Use a simple starter threshold. Adjust this for your environment.
    $DiskStatus = if ($FreePercent -lt 10) { 'Fail' } elseif ($FreePercent -lt 15) { 'Warning' } else { 'Pass' }

    $Results += New-OpsReportResultRow `
        -RunId $Run.RunId `
        -TargetType 'Host' `
        -TargetName $TargetName `
        -Host $TargetName `
        -CheckGroup 'Disk' `
        -CheckName 'SystemDriveFreePercent' `
        -Status $DiskStatus `
        -Severity $(if ($DiskStatus -eq 'Fail') { 'High' } elseif ($DiskStatus -eq 'Warning') { 'Medium' } else { 'Info' }) `
        -ExpectedValue '>= 15 percent free' `
        -ActualValue "$FreePercent percent free" `
        -Finding "$($env:SystemDrive) has $FreePercent percent free." `
        -ErrorText ''

    # -----------------------------------------------------------------
    # Check 3: Common pending reboot indicators
    # -----------------------------------------------------------------
    # These registry checks are read-only. They are common indicators, not a
    # single universal Windows reboot API.
    $RebootIndicators = @(
        'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending',
        'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired'
    )

    $PendingReboot = $false
    foreach ($Path in $RebootIndicators) {
        if (Test-Path -Path $Path) {
            $PendingReboot = $true
        }
    }

    $Results += New-OpsReportResultRow `
        -RunId $Run.RunId `
        -TargetType 'Host' `
        -TargetName $TargetName `
        -Host $TargetName `
        -CheckGroup 'Reboot' `
        -CheckName 'PendingRebootIndicators' `
        -Status $(if ($PendingReboot) { 'Warning' } else { 'Pass' }) `
        -Severity $(if ($PendingReboot) { 'Medium' } else { 'Info' }) `
        -ExpectedValue 'No common pending reboot indicators' `
        -ActualValue $(if ($PendingReboot) { 'Pending reboot indicator found' } else { 'No indicator found' }) `
        -Finding $(if ($PendingReboot) { 'Common pending reboot indicator detected.' } else { 'No common pending reboot indicator detected.' }) `
        -ErrorText ''
}
catch {
    # If collection itself fails, still create a normalized Error row.
    # This keeps the report useful because failures become visible evidence.
    $Results += New-OpsReportResultRow `
        -RunId $Run.RunId `
        -TargetType 'Host' `
        -TargetName $TargetName `
        -Host $TargetName `
        -CheckGroup 'Collection' `
        -CheckName 'LocalSampleCollection' `
        -Status 'Error' `
        -Severity 'High' `
        -Finding 'The local sample collector failed before all checks completed.' `
        -ErrorText $_.Exception.Message `
        -ErrorCategory 'CollectionFailed'
}

# ---------------------------------------------------------------------
# Complete the run, summarize rows, and export artifacts
# ---------------------------------------------------------------------
# Complete-OpsReportRunContext adds completion time and duration.
$CompletedRun = Complete-OpsReportRunContext -RunContext $Run

# New-OpsReportSummary calculates row-level and target-level counts.
$Summary = New-OpsReportSummary -RunContext $CompletedRun -Results $Results

# Export-OpsReportArtifacts writes the local report files.
# Local artifacts should exist before email, Teams, ticket, or webhook delivery.
$Artifacts = Export-OpsReportArtifacts `
    -OutputRoot $OutputRoot `
    -RunContext $CompletedRun `
    -Summary $Summary `
    -Results $Results

# Show the summary and artifact paths so the operator knows what happened.
$Summary | Format-List
$Artifacts | Format-List

# Optional convenience: open the output folder after the first run.
# Comment this out on servers or scheduled tasks where launching Explorer is not desired.
# Invoke-Item $Artifacts.OutputFolder

Validation

  • The helper file exists at `./templates/reporting/OpsReporting.Foundation.ps1` before this page's starter runs.
  • The starter creates HTML, CSV, JSON, and log artifacts under `./output/ops-report-foundation-first-run`.
  • The HTML, CSV, and JSON outputs describe the same normalized result rows and the same run metadata.
  • The summary separates row-level counts from target-level counts, so one target with multiple warnings is not confused with multiple affected targets.
  • A new reader can explain what TargetType, TargetName, CheckGroup, CheckName, Status, Finding, and ErrorText mean after running the sample.

Reporting

  • Use this workflow after the helper file exists and you are ready to turn local PowerShell checks into shared report artifacts.
  • The foundation does not collect every kind of data itself. Collector scripts gather evidence, then map important findings into normalized result rows.
  • The same result-row contract can support Windows health, pending reboot, certificate, AD hygiene, Azure Arc readiness, file-share audit, and other report packs.
  • Each compatible collector should keep useful raw evidence while also emitting foundation-ready rows for consolidated HTML, CSV, JSON, and log artifacts.
  • Local artifacts should be treated as the system of record. Email, webhook, Teams, Graph, or ticket delivery should happen only after files are written.

Safety Notes

  • The starter performs read-only checks against the local computer and writes local report artifacts only.
  • Review `$OutputRoot` before running so test files are written where you expect.
  • Treat the first run as a preview pass. If you do not want to keep the generated files, remove or archive the HTML, CSV, JSON, and log outputs as the explicit undo path.
  • Do not place passwords, tokens, API keys, or secret-bearing command lines in Finding, Evidence, ErrorText, CSV, JSON, HTML, or log output.
  • If the helper file is missing or the expected functions cannot be loaded, stop and complete the helper-contract page first.
  • When adapting this pattern to remote collectors, label remote reads, local file output, and any state-changing actions separately.