LibreDevOpsHelpers.Conftest/LibreDevOpsHelpers.Conftest.psm1
|
Set-StrictMode -Version Latest function Install-LdoConftest { <# .SYNOPSIS Installs the Conftest CLI. .DESCRIPTION Downloads the official Conftest release binary from GitHub. The version defaults to 'latest' and is resolved at runtime (no hard-pinned version to maintain); a specific version can be requested. On Linux and macOS the binary is installed to /usr/local/bin; on Windows it is extracted to a per-user directory which is added to the current session's PATH. .PARAMETER Version Conftest version to install: 'latest' (default) or a specific tag like '0.62.0' / 'v0.62.0'. .EXAMPLE Install-LdoConftest .EXAMPLE Install-LdoConftest -Version 0.62.0 .OUTPUTS None #> [CmdletBinding()] [OutputType([void])] param( [string]$Version = 'latest' ) $os = (Get-LdoOperatingSystem).ToLower() # Resolve 'latest' to a concrete tag by following the releases/latest redirect (no API token, # no rate-limit concern). curl is run through bash because in PowerShell "curl" is an alias. if ($Version -eq 'latest') { $effectiveUrl = (bash -c "curl -fsSL -o /dev/null -w '%{url_effective}' https://github.com/open-policy-agent/conftest/releases/latest").Trim() Assert-LdoLastExitCode -Operation 'resolve latest conftest release' $tag = ($effectiveUrl.TrimEnd('/') -split '/')[-1] } else { $tag = if ($Version.StartsWith('v')) { $Version } else { "v$Version" } } # Conftest release asset names use the version without the leading 'v'. $verNum = $tag.TrimStart('v') # Conftest asset naming: OS is Linux/Darwin/Windows, arch is x86_64/arm64. $arch = if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { 'arm64' } else { 'x86_64' } $work = Join-Path ([System.IO.Path]::GetTempPath()) ("ldo-conftest-" + [guid]::NewGuid()) New-Item -ItemType Directory -Path $work | Out-Null try { if ($os -eq 'windows') { $url = "https://github.com/open-policy-agent/conftest/releases/download/$tag/conftest_${verNum}_Windows_${arch}.zip" Write-LdoLog -Level INFO -Message "Downloading Conftest $tag from $url" $archive = Join-Path $work 'conftest.zip' Invoke-WebRequest -Uri $url -OutFile $archive Expand-Archive -Path $archive -DestinationPath $work -Force $dir = Join-Path $env:LOCALAPPDATA 'Programs\conftest' New-Item -ItemType Directory -Path $dir -Force | Out-Null Move-Item -Path (Join-Path $work 'conftest.exe') -Destination (Join-Path $dir 'conftest.exe') -Force if (($env:PATH -split ';') -notcontains $dir) { $env:PATH = "$dir;$env:PATH" } Write-LdoLog -Level INFO -Message "Conftest installed to $dir (added to PATH for this session; add it permanently to use it in new shells)." } else { $platform = if ($os -eq 'macos') { 'Darwin' } else { 'Linux' } $url = "https://github.com/open-policy-agent/conftest/releases/download/$tag/conftest_${verNum}_${platform}_${arch}.tar.gz" Write-LdoLog -Level INFO -Message "Downloading Conftest $tag from $url" $archive = Join-Path $work 'conftest.tar.gz' Invoke-WebRequest -Uri $url -OutFile $archive & tar -xzf $archive -C $work conftest Assert-LdoLastExitCode -Operation 'extract conftest' $binary = Join-Path $work 'conftest' & chmod '+x' $binary $dest = '/usr/local/bin/conftest' try { Move-Item -Path $binary -Destination $dest -Force -ErrorAction Stop } catch { bash -c "sudo mv '$binary' '$dest'" Assert-LdoLastExitCode -Operation 'install conftest to /usr/local/bin' } } } finally { Remove-Item $work -Recurse -Force -ErrorAction SilentlyContinue } Assert-LdoCommand -Name @('conftest') Write-LdoLog -Level SUCCESS -Message "Conftest $tag installed." } function Assert-LdoConftest { <# .SYNOPSIS Asserts that the Conftest CLI is available on PATH. .DESCRIPTION Throws a clear error when conftest is not installed, pointing at Install-LdoConftest. .EXAMPLE Assert-LdoConftest .OUTPUTS None #> [CmdletBinding()] [OutputType([void])] param() if (-not (Get-Command conftest -ErrorAction SilentlyContinue)) { throw 'Conftest is not installed or not on PATH. Run Install-LdoConftest first.' } } function Invoke-LdoConftest { <# .SYNOPSIS Runs Conftest policies against a Terraform plan rendered to JSON. .DESCRIPTION Runs 'conftest test' over a Terraform plan JSON (produced by 'terraform show -json plan.bin') using the Rego policies under -PolicyPath. Conftest 'deny' rules fail the run; 'warn' rules are informational and do not fail unless -FailOnWarn is set (the Libre DevOps naming checks are warn rules). Throws on a failing run unless -SoftFail is set. .PARAMETER PlanJsonPath Path to the Terraform plan rendered to JSON. .PARAMETER PolicyPath Path to the directory of Rego policies. .PARAMETER AllNamespaces Evaluate every policy namespace. Defaults to true. Ignored when -Namespace is supplied. .PARAMETER Namespace One or more specific policy namespaces to evaluate (instead of all). .PARAMETER FailOnWarn When set, 'warn' findings also fail the run. Off by default so naming checks stay informational. .PARAMETER SoftFail When set, a failing run is logged as a warning instead of throwing. .PARAMETER ExtraArgs Additional arguments passed through to conftest. .EXAMPLE Invoke-LdoConftest -PlanJsonPath ./plan.json -PolicyPath ./policies .EXAMPLE Invoke-LdoConftest -PlanJsonPath ./plan.json -PolicyPath ./policies -FailOnWarn .OUTPUTS None #> [CmdletBinding()] [OutputType([void])] param( [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$PlanJsonPath, [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$PolicyPath, [bool]$AllNamespaces = $true, [string[]]$Namespace = @(), [switch]$FailOnWarn, [switch]$SoftFail, [string[]]$ExtraArgs = @() ) # Validate the provided paths first (deterministic, no external dependency), then assert the # CLI is present, so argument errors are reported clearly even when conftest is not installed. if (-not (Test-Path $PlanJsonPath)) { throw "Plan JSON not found: $PlanJsonPath" } if (-not (Test-Path $PolicyPath)) { throw "Policy path not found: $PolicyPath" } Assert-LdoConftest $conftestArgs = @('test', $PlanJsonPath, '--policy', $PolicyPath) # Wrap in @() so a $null (which a splatted empty array binds to) is treated as empty rather # than tripping .Count under Set-StrictMode. if (@($Namespace).Count -gt 0) { foreach ($ns in $Namespace) { $conftestArgs += @('--namespace', $ns) } } elseif ($AllNamespaces) { $conftestArgs += '--all-namespaces' } if ($FailOnWarn) { $conftestArgs += '--fail-on-warn' } $conftestArgs += $ExtraArgs Write-LdoLog -Level INFO -Message "Executing Conftest: conftest $($conftestArgs -join ' ')" # Capture the output so it can be re-shown in the end-of-run findings summary, and print it. # Strip ANSI colour codes so the stored text is clean and matches reliably. $report = & conftest @conftestArgs 2>&1 $code = $LASTEXITCODE $reportText = (($report | Out-String) -replace '\x1b\[[0-9;]*m', '').TrimEnd() Write-Host $reportText if ($code -eq 0) { # A clean exit can still carry informational warnings (warn rules do not fail by default). $hasWarn = $reportText -match '(?m)^WARN ' Write-LdoLog -Level SUCCESS -Message 'Conftest completed with no failures (warnings, if any, are listed above).' if ($hasWarn) { Add-LdoFinding -Tool 'conftest' -Target $PlanJsonPath -Status 'WARN' -Summary 'policy warnings (informational)' -Detail $reportText } else { Add-LdoFinding -Tool 'conftest' -Target $PlanJsonPath -Status 'PASS' -Summary 'no policy findings' -Detail $reportText } } elseif ($SoftFail) { Write-LdoLog -Level WARN -Message "Conftest reported failures (exit $code); continuing because -SoftFail." Add-LdoFinding -Tool 'conftest' -Target $PlanJsonPath -Status 'WARN' -Summary "failures (soft-fail, exit $code)" -Detail $reportText } else { Add-LdoFinding -Tool 'conftest' -Target $PlanJsonPath -Status 'FAIL' -Summary "failures (exit $code)" -Detail $reportText throw "Conftest failed (exit $code)." } } Export-ModuleMember -Function ` Install-LdoConftest, ` Assert-LdoConftest, ` Invoke-LdoConftest |