update first - 84
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env pwsh
|
||||
# CASAN H5 Governance Harness - PowerShell port of governance-check.sh
|
||||
# Usage:
|
||||
# governance-check.ps1 <input-file> <output-file> [action-name]
|
||||
#
|
||||
# Non-interactive. High-risk denied unless env:CASAN_APPROVAL_DECISION=approve + env:CASAN_APPROVER set.
|
||||
# Exit codes: 0=approved, 2=denied, 64=usage error
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$true, Position=0)][string]$InputFile,
|
||||
[Parameter(Mandatory=$true, Position=1)][string]$OutputFile,
|
||||
[Parameter(Position=2)][string]$ActionName = "agent_step"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent
|
||||
$projectRoot = (Resolve-Path (Join-Path $scriptDir "../../..")).Path
|
||||
$logDir = Join-Path $projectRoot ".specify/logs"
|
||||
$traceDir = Join-Path $logDir "trace"
|
||||
$auditDir = Join-Path $logDir "audit"
|
||||
$auditLog = Join-Path $auditDir "audit.jsonl"
|
||||
|
||||
foreach ($d in @($traceDir, $auditDir, (Split-Path $OutputFile -Parent))) {
|
||||
if ($d -and !(Test-Path $d)) { New-Item -ItemType Directory -Force -Path $d | Out-Null }
|
||||
}
|
||||
|
||||
if (!(Test-Path $InputFile)) {
|
||||
Write-Error "GOVERNANCE_DENIED: input file not found: $InputFile"
|
||||
exit 2
|
||||
}
|
||||
|
||||
function New-TraceId {
|
||||
try { return [System.Guid]::NewGuid().ToString("D") } catch { return "trace-$(Get-Date -Format 'yyyyMMddHHmmss')-$PID" }
|
||||
}
|
||||
|
||||
function Get-Sha256 ([string]$text) {
|
||||
$bytes = [System.Text.Encoding]::UTF8.GetBytes($text)
|
||||
$hash = [System.Security.Cryptography.SHA256]::Create().ComputeHash($bytes)
|
||||
return ($hash | ForEach-Object { $_.ToString("x2") }) -join ""
|
||||
}
|
||||
|
||||
function ConvertTo-JsonArray ([string[]]$arr) {
|
||||
if (!$arr -or $arr.Count -eq 0) { return "[]" }
|
||||
$escaped = $arr | ForEach-Object { '"' + ($_ -replace '"','\"') + '"' }
|
||||
return "[" + ($escaped -join ",") + "]"
|
||||
}
|
||||
|
||||
$traceId = New-TraceId
|
||||
$timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
|
||||
$input = Get-Content $InputFile -Raw -Encoding UTF8
|
||||
if (!$input) { $input = "" }
|
||||
$lowerInput = $input.ToLower()
|
||||
$actor = if ($env:CASAN_ACTOR) { $env:CASAN_ACTOR } else { "developer" }
|
||||
$approver = if ($env:CASAN_APPROVER) { $env:CASAN_APPROVER } else { "" }
|
||||
$approvalDecision = if ($env:CASAN_APPROVAL_DECISION) { $env:CASAN_APPROVAL_DECISION } else { "auto" }
|
||||
$agentName = if ($env:CASAN_AGENT_NAME) { $env:CASAN_AGENT_NAME } else { "unknown" }
|
||||
|
||||
$riskLevel = "low"
|
||||
$reasons = [System.Collections.Generic.List[string]]::new()
|
||||
|
||||
# ── Risk by action name ────────────────────────────────────────────────────
|
||||
switch -Regex ($ActionName) {
|
||||
"^(write_code|write_file|external_api|tool_call)$" {
|
||||
$riskLevel = "medium"; $reasons.Add("sensitive-action:$ActionName")
|
||||
}
|
||||
"^(deploy|launch|migration|db_write)$" {
|
||||
$riskLevel = "high"; $reasons.Add("high-risk-action:$ActionName")
|
||||
}
|
||||
}
|
||||
|
||||
# ── Tool registry agent whitelist check ───────────────────────────────────
|
||||
if ($agentName -ne "unknown") {
|
||||
if ($agentName -match "okr\.(srs|bd|reviewspec|reviewplan|reviewcode)" -or
|
||||
$agentName -match "speckit\.(specify|clarify|plan|tasks)") {
|
||||
if ($ActionName -match "^(deploy|migration|db_write)$") {
|
||||
$riskLevel = "high"
|
||||
$reasons.Add("unauthorized-action-for-agent:$ActionName")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ── Risk by content keywords ───────────────────────────────────────────────
|
||||
if ($lowerInput -match "(delete|drop table|password|api[_-]?key|secret|token|credential|migration|deploy|external api|shutdown|dump database)") {
|
||||
$riskLevel = "high"
|
||||
if (!$reasons.Contains("high-risk-content")) { $reasons.Add("high-risk-content") }
|
||||
} elseif ($lowerInput -match "(internal|config|system|policy|permission)") {
|
||||
if ($riskLevel -eq "low") { $riskLevel = "medium"; $reasons.Add("medium-risk-content") }
|
||||
}
|
||||
|
||||
# ── Approval decision ──────────────────────────────────────────────────────
|
||||
$approvalStatus = "auto_approved"
|
||||
$decision = "approved"
|
||||
|
||||
if ($riskLevel -eq "medium") { $approvalStatus = "policy_auto_approved_with_audit" }
|
||||
if ($riskLevel -eq "high") {
|
||||
if ($approvalDecision -eq "approve" -and $approver -ne "") {
|
||||
$approvalStatus = "human_approved"; $decision = "approved"
|
||||
} else {
|
||||
$approvalStatus = "approval_required"; $decision = "denied"
|
||||
}
|
||||
}
|
||||
|
||||
# ── Hash + chain ───────────────────────────────────────────────────────────
|
||||
$inputHash = Get-Sha256 $input
|
||||
$prevHash = ""
|
||||
if (Test-Path $auditLog) {
|
||||
$lastLine = Get-Content $auditLog -Tail 1
|
||||
if ($lastLine -match '"record_hash":"([^"]+)"') { $prevHash = $Matches[1] }
|
||||
}
|
||||
|
||||
$recordCore = "$timestamp|$traceId|$ActionName|$actor|$riskLevel|$decision|$approvalStatus|$inputHash|$prevHash"
|
||||
$recordHash = Get-Sha256 $recordCore
|
||||
$reasonsJson = ConvertTo-JsonArray ($reasons.ToArray())
|
||||
|
||||
# ── Trace JSON ─────────────────────────────────────────────────────────────
|
||||
$traceFile = Join-Path $traceDir "governance-$traceId.json"
|
||||
@"
|
||||
{
|
||||
"trace_id": "$traceId",
|
||||
"timestamp": "$timestamp",
|
||||
"harness": "H5-governance",
|
||||
"action": "$ActionName",
|
||||
"actor": "$actor",
|
||||
"risk_level": "$riskLevel",
|
||||
"decision": "$decision",
|
||||
"approval_status": "$approvalStatus",
|
||||
"approver": "$approver",
|
||||
"reasons": $reasonsJson,
|
||||
"input_hash": "$inputHash",
|
||||
"previous_record_hash": "$prevHash",
|
||||
"record_hash": "$recordHash"
|
||||
}
|
||||
"@ | Set-Content -Path $traceFile -Encoding UTF8
|
||||
|
||||
# ── Audit JSONL (append-only) ──────────────────────────────────────────────
|
||||
$auditLine = "{`"timestamp`":`"$timestamp`",`"trace_id`":`"$traceId`",`"harness`":`"H5-governance`",`"action`":`"$ActionName`",`"actor`":`"$actor`",`"risk_level`":`"$riskLevel`",`"decision`":`"$decision`",`"approval_status`":`"$approvalStatus`",`"approver`":`"$approver`",`"input_hash`":`"$inputHash`",`"previous_record_hash`":`"$prevHash`",`"record_hash`":`"$recordHash`"}"
|
||||
Add-Content -Path $auditLog -Value $auditLine -Encoding UTF8
|
||||
|
||||
# ── Result ─────────────────────────────────────────────────────────────────
|
||||
if ($decision -ne "approved") {
|
||||
Set-Content -Path $OutputFile -Value "" -Encoding UTF8
|
||||
Write-Error "GOVERNANCE_DENIED trace_id=$traceId risk=$riskLevel approval_status=$approvalStatus"
|
||||
exit 2
|
||||
}
|
||||
|
||||
$input | Set-Content -Path $OutputFile -Encoding UTF8
|
||||
Write-Output "GOVERNANCE_APPROVED trace_id=$traceId risk=$riskLevel approval_status=$approvalStatus output=$OutputFile"
|
||||
exit 0
|
||||
Reference in New Issue
Block a user