# Specification

  • Overview
  • Configuration Management and Script Generation
  • Session and Progress Persistence
  • Robustness and Error Handling
  • Build Certificate
  • Scirpt progress, Documentation and Compliance
  • Requirements
  • Success Criteria
  • Key Outcomes
  • Menu Presentation
  • Logging
  • Error/Warning Handling
  • Code examples
    • Build certificate generate in the post-deployment script
    • Post-deployment script code to date, assuming everything is checked for

# Overview

This project provides a modular PowerShell 7 toolkit to automate and validate Windows device builds, including:

  • Device and configuration validation (naming checks - as naming needs to be done when logged in as an administrator on the device, Secure Boot, BitLocker, PCR7, etc.)
  • Company Portal, OneDrive, Office 365, Teams, Edge, and required app checks
  • Intune and AD join validation
  • Build certificate generation and export
  • Robust logging, error handling, and accessibility

The project will acheive and provide these functions this by providing a text-based CLI for the user.

# Configuration Management and Script Generation

  • Handled in a CLI, preferably and most likely text-based from the outset of the project
    • Subject to difficulty, may also be a Windows Forms UI that is automatically maintained as features in the project are added, removed, updated, or adjusted.
  • Store and version current and past configurations, with user-defined names and last update dates for reference.
  • Enable user-driven configuration and export of OOBE and post-deployment scripts.
  • Allow customisation of post-deployment steps and script content.
    • Customisation is via a text-based CLI method, where options can be switched on or off easily on a single screen
  • Provide backup/version control for configuration scripts.
  • Ensure all configuration changes are logged and auditable.

# Session and Progress Persistence

  • Post-deployment scripts must detect incomplete build sessions and offer the option to continue without repeating already completed steps.
  • Persist session and progress state to support device reboots and interrupted builds.

# Robustness and Error Handling

  • Provide robust fallback for Autopilot enrolment, supporting both PowerShell 5 and 7.
  • Implement comprehensive validation and error handling for all steps.
  • Include detailed logging of the CLI, pre and post deployment scripts, with a --Logverbose option for in-depth diagnostics.
  • Ensure all scripts are idempotent and safe to re-run.

# Build Certificate

  • Generate a text-based build certificate at a user-defined location.
  • Copy the build certificate to the clipboard for easy transmission.
  • Include all relevant build, validation, and configuration details in the certificate.

# Scirpt progress, Documentation and Compliance

  • All script outputs, messages, information, and details passed to the user interface - regardless of the CLI or script being ran - must be clear, concise, specific, detailled, include errors from the system, and enable appropriate next best actions to overcome or address the issue.
  • Provide documentation for quality control, compliance, and integration with ITSM/ticketing systems.
  • Documentation and code must include references to official documentation for all commands run and modules used.
  • Ensure all scripts and documentation are accessible, well-structured, and use EN-AU spelling.
  • Maintain a Retype static documentation site, with regular updates and version control.

# Requirements

  • PowerShell 7 compatibility
  • User interface for script generation and management, ideally from a command-line based CLI in the first instance
  • Modular scripts and functions
  • User prompts and menu-driven flows
  • Exportable scripts for OOBE and post-deployment
  • Retype static documentation site
  • CI/CD and linting integration
  • Configuration management and versioning
  • Session and progress persistence
  • Robust error handling and logging
  • Accessibility and compliance with EN-AU standards

# Success Criteria

  • All build steps are validated and logged
  • Build certificate is generated, exportable, and includes all required details
  • Session and progress are reliably persisted and recoverable
  • Documentation and code meet accessibility and EN-AU standards
  • All requirements above are met without regression or loss of existing functionality

# Key Outcomes

  • Reliable, repeatable device builds
  • Reduced staff time and effort for device configuration and delivery
  • Compliance with organisational and ITSM requirements
  • Accessible, well-documented scripts and processes

# Menu Presentation

  • All menu titles must use blue (cyan) for visibility.
  • Documentation/resources in help menus must use purple (magenta).
  • All other output must be accessible and clear.

# Logging

  • Use two log files: transcript (session) and custom (actions/errors).
  • All menu actions, errors, and warnings must be logged in the custom log file.
  • No direct writes to the transcript file except via Start-Transcript.

# Error/Warning Handling

  • Errors and warnings are shown in colour in the terminal.
  • All details are logged in the custom log file.

# Code examples

# Build certificate generate in the post-deployment script

```powershell
#region Final Certificate Generation

$status.Complete = $true
$status.EndTime = (Get-Date).ToString('yyyy-MM-dd HH:mm')
$filename = "build_certificate_$($status.UserName -replace '\s','_')_$(Get-Date -Format 'yyyyMMdd_HHmm').txt"
$certPath = Join-Path $historyFolder $filename
$status.CertificatePath = $certPath

    $finalOutput = @(
    "BUILD CERTIFICATE",
    "User: $($status.UserName)",
    "Computer: $env:COMPUTERNAME",
    "Started: $($status.StartTime)",
    "Completed: $($status.EndTime)",
    "",
    ($status.GetEnumerator() | Where-Object { $_.Key -like 'Step*' } | Sort-Object Name | ForEach-Object { "$($_.Key): $($_.Value)" })
)

$finalOutput | Set-Content -Path $certPath
$status | ConvertTo-Json | Set-Content $logPath

Write-Host "\nBuild complete. Certificate written to: $certPath"

#endregion
```

# Post-deployment script code to date, assuming everything is checked for

```powershell
#region Initialization and Pre-Checks

# Ensure BuildStatus folder exists
$buildFolder = "C:\BuildStatus"
if (-not (Test-Path $buildFolder)) {
    New-Item -Path $buildFolder -ItemType Directory -Force
}

# Check if running in PowerShell 7
if ($PSVersionTable.PSEdition -ne "Core") {
    Write-Host "This script requires PowerShell 7+."
    $pwshPath = Get-Command pwsh.exe -ErrorAction SilentlyContinue
    if (-not $pwshPath) {
        Write-Host "PowerShell 7 not found. Installing from Microsoft Store..."
        Start-Process "ms-windows-store://pdp/?productid=9MZ1SNWT0N5D"
        Read-Host "Install PowerShell 7 and press Enter to continue..."
    }
    Write-Host "Launching script in PowerShell 7..."
    Start-Process pwsh -ArgumentList "-NoExit", "-File `"$PSCommandPath`""
    exit
}

#endregion

#region Load or initialise Build Status

$logPath = Join-Path $buildFolder 'status.json'
$historyFolder = Join-Path $buildFolder 'Certificates'
if (-not (Test-Path $historyFolder)) {
    New-Item -Path $historyFolder -ItemType Directory -Force
}

$status = @{}
$existingBuild = $false
$buildStartTime = Get-Date

if (Test-Path $logPath) {
    $status = Get-Content $logPath | ConvertFrom-Json
    if ($status.Complete -eq $true) {
        $choice = Read-Host "Build already completed. (V)iew certificate, (R)e-run, or (E)xit?"
        switch ($choice.ToUpper()) {
            'V' {
                Get-Content -Path $status.CertificatePath
                exit
            }
            'R' {
                Remove-Item $logPath -Force
                $status = @{}
            }
            default {
                exit
            }
        }
    } else {
        $existingBuild = $true
        $continue = Read-Host "Incomplete build found. Continue? (Y/N)"
        if ($continue -ne 'Y') { exit }
    }
}

#endregion

#region Capture User Name and initialise Timestamps

$userName = if ($existingBuild -and $status.UserName) {
    Read-Host "Confirm or update your name (current: $($status.UserName))"
} else {
    Read-Host "Enter your full name to record with this build"
}

$status.UserName = $userName
$status.StartTime = $status.StartTime ?? $buildStartTime.ToString('yyyy-MM-dd HH:mm')

#endregion

#region Helper Functions

function Log-Step {
    param($step, $success, $message)
    if (-not $success) {
        Write-Warning "$step failed: $message"
        $retry = Read-Host "Would you like to retry this step? (Y/N)"
        if ($retry -eq 'Y') {
            return 'RETRY'
        } else {
            $status[$step] = $false
            $status | ConvertTo-Json | Set-Content $logPath
            exit
        }
    } else {
        $status[$step] = $true
        $status | ConvertTo-Json | Set-Content $logPath
    }
}

#endregion

#region Step Functions

function Check-DeviceName {
    $hostname = $env:COMPUTERNAME
    if ($hostname -match "^UCW(LAP|WKS)\d{3}$") {
        Log-Step -step 'Step1_DeviceName' -success $true -message ""
    } else {
        Log-Step -step 'Step1_DeviceName' -success $false -message "Computer name must follow schema UCWLAP### or UCWWKS###."
    }
}

function Check-SecureBoot {
    try {
        $sbState = Confirm-SecureBootUEFI
        Log-Step 'Step2_SecureBoot' ($sbState -eq $true) "Secure Boot is not enabled."
    } catch {
        Log-Step 'Step2_SecureBoot' $false "Secure Boot not supported or inaccessible."
    }
}

function Check-BitLocker {
    $bitlocker = Get-BitLockerVolume | Where-Object { $_.ProtectionStatus -eq 'On' }
    Log-Step 'Step3_BitLocker' ($bitlocker -ne $null) "BitLocker is not enabled on any drive."
}

function Check-PCR7 {
    $pcr7 = (Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\PCR7\Configuration" -ErrorAction SilentlyContinue).PCR7BindingState
    Log-Step 'Step4_PCR7' ($pcr7 -eq "Bound") "PCR7 is not bound."
}

function Check-CompanyPortal {
    $cpInstalled = Get-AppxPackage -Name "Microsoft.CompanyPortal"
    if ($cpInstalled) {
        Log-Step 'Step5_CompanyPortal' $true ""
    } else {
        Write-Host "Installing Company Portal..."
        Start-Process "ms-windows-store://pdp/?productid=9wzdncrfj3pz"
        Read-Host "Install and sign into Company Portal, then press Enter"
        $cpCheck = Get-AppxPackage -Name "Microsoft.CompanyPortal"
        Log-Step 'Step5_CompanyPortal' ($cpCheck -ne $null) "Company Portal is still not installed."
    }
}

function Check-OneDrive {
    $oneDrivePath = "$env:LOCALAPPDATA\Microsoft\OneDrive\OneDrive.exe"
    $userProfile = "$env:USERPROFILE\OneDrive - *"
    if ((Test-Path $oneDrivePath) -and (Test-Path $userProfile)) {
        Log-Step 'Step6_OneDrive' $true ""
    } else {
        Start-Process $oneDrivePath
        Read-Host "Sign into OneDrive then press Enter"
        if (Test-Path "$env:USERPROFILE\OneDrive - *") {
            Log-Step 'Step6_OneDrive' $true ""
        } else {
            Log-Step 'Step6_OneDrive' $false "OneDrive is not set up for current user."
        }
    }
}

function Check-WindowsSecurity {
    $secHealth = Get-MpComputerStatus
    $allGreen = $secHealth | Get-Member -MemberType NoteProperty | ForEach-Object { $secHealth.$($_.Name) } | Where-Object { $_ -eq $false } | Measure-Object
    Log-Step 'Step7_Security' ($allGreen.Count -eq 0) "Windows Security has one or more failing components."
}

function Run-WindowsUpdate {
    $targetBuild = "24H2"
    $updateSuccess = $false
    try {
        Install-Module PSWindowsUpdate -Force -Scope CurrentUser -ErrorAction Stop
        Import-Module PSWindowsUpdate -ErrorAction Stop

        do {
            $pending = Get-WindowsUpdate -AcceptAll -Install -AutoReboot -ErrorAction Stop
            Start-Sleep -Seconds 15
            $remaining = Get-WindowsUpdate -IsPending -ErrorAction SilentlyContinue
        } while ($remaining.Count -gt 0)

        $build = (Get-ComputerInfo).WindowsVersion
        if ($build -notmatch $targetBuild) {
            Write-Warning "Target build $targetBuild not detected. Manual intervention may be needed."
        }

        $updateSuccess = $true
    } catch {
        Write-Warning "Windows Update via PSWindowsUpdate failed. Attempting fallback to Windows Settings..."
        try {
            Start-Process "ms-settings:windowsupdate"
            Read-Host "Check for updates manually, ensure they are all installed, then press Enter to continue"
            $updateSuccess = $true
        } catch {
            Log-Step 'Step8_WindowsUpdate' $false "Windows Update fallback failed. $_"
        }
    }

    if ($updateSuccess) {
        $restartRequired = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" -ErrorAction SilentlyContinue)
        $pending = Get-WindowsUpdate -IsPending -ErrorAction SilentlyContinue
        if ($restartRequired -or $pending.Count -gt 0) {
            Log-Step 'Step8_WindowsUpdate' $false "Updates still pending or restart required."
        } else {
            Log-Step 'Step8_WindowsUpdate' $true ""
        }
    }
}


function Check-OfficeApps {
    $apps = "Word", "Excel", "Outlook", "Teams", "PowerPoint"
    $missing = $apps | Where-Object { -not (Get-Command $_ -ErrorAction SilentlyContinue) }
    Log-Step 'Step9_OfficeApps' ($missing.Count -eq 0) "Missing Office apps: $($missing -join ", ")."
}

function Check-Outlook {
    $outlookProfile = Test-Path "$env:APPDATA\Microsoft\Outlook\*.xml"
    Log-Step 'Step10_Outlook' $outlookProfile "Outlook is not configured."
}

function Check-Edge {
    $profilePath = "$env:LOCALAPPDATA\Microsoft\Edge\User Data\Default"
    $preferences = Join-Path $profilePath "Preferences"
    $signedIn = $false
    if (Test-Path $preferences) {
        $prefsJson = Get-Content $preferences -Raw | ConvertFrom-Json
        $signedIn = $prefsJson.account_info -ne $null
    }
    Log-Step 'Step11_Edge' $signedIn "User not signed into Edge profile."
}

function Check-RequiredApps {
    $requiredApps = @("Netskope", "Datto RMM")
    $running = Get-Process | Select-Object -ExpandProperty Name
    $found = $requiredApps | Where-Object { $_ -in $running }
    Log-Step 'Step12_RequiredApps' ($found.Count -eq $requiredApps.Count) "One or more required apps are not running."
}

function Check-Teams {
    $teamsRunning = Get-Process -Name Teams -ErrorAction SilentlyContinue
    if (-not $teamsRunning) {
        Start-Process "Teams"
        Read-Host "Log into Teams, then press Enter"
    }
    $teamsDir = "$env:APPDATA\Microsoft\Teams"
    $signedIn = Test-Path "$teamsDir\storage.json"
    Log-Step 'Step13_Teams' $signedIn "Microsoft Teams is not signed in."
}

#endregion

#region Run Checks

Check-DeviceName
Check-SecureBoot
Check-BitLocker
Check-PCR7
Check-CompanyPortal
Check-OneDrive
Check-WindowsSecurity
Run-WindowsUpdate
Check-OfficeApps
Check-Outlook
Check-Edge
Check-RequiredApps
Check-Teams

#endregion

#region Final Certificate Generation

$status.Complete = $true
$status.EndTime = (Get-Date).ToString('yyyy-MM-dd HH:mm')
$filename = "build_certificate_$($status.UserName -replace '\s','_')_$(Get-Date -Format 'yyyyMMdd_HHmm').txt"
$certPath = Join-Path $historyFolder $filename
$status.CertificatePath = $certPath

$finalOutput = @(
    "BUILD CERTIFICATE",
    "User: $($status.UserName)",
    "Computer: $env:COMPUTERNAME",
    "Started: $($status.StartTime)",
    "Completed: $($status.EndTime)",
    "",
    ($status.GetEnumerator() | Where-Object { $_.Key -like 'Step*' } | Sort-Object Name | ForEach-Object { "$($_.Key): $($_.Value)" })
)

$finalOutput | Set-Content -Path $certPath
$status | ConvertTo-Json | Set-Content $logPath

Write-Host "\nBuild complete. Certificate written to: $certPath"

#endregion
```

# CLI Module Menu Flow Standard

For all CLI modules and menu-driven scripts:

  • CLI modules must never redirect to menus themselves.
    All CLI modules and functions should return control to their caller after completing their task, including after errors or user exits.

  • Menu navigation and flow must be managed by the calling (wrapper) function or script.
    This ensures consistent, predictable menu behaviour and prevents accidental returns to the wrong menu or main menu.

  • CLI modules must not call other menus, main menus, or perform navigation logic.
    They should only perform their core function and return.

  • Document this pattern in all new CLI modules and review existing modules for compliance.

This standard ensures robust, maintainable, and accessible menu navigation throughout the project.