Files
CASAN/AINative_OKR_CASAN5/.specify/scripts/powershell/security-check.ps1
T
2026-06-30 02:21:39 +09:00

219 lines
9.2 KiB
PowerShell

#!/usr/bin/env pwsh
# CASAN H4 Security Harness - PowerShell port of security-check.sh
# Usage:
# security-check.ps1 <input-file> <output-file> [input|output]
#
# input mode: blocks prompt injection / jailbreak / secrets, masks PII.
# output mode: redacts PII/secrets from generated output, flags risky language.
# Exit codes: 0=pass, 2=blocked, 64=usage error
param(
[Parameter(Mandatory=$true, Position=0)][string]$InputFile,
[Parameter(Mandatory=$true, Position=1)][string]$OutputFile,
[Parameter(Position=2)][ValidateSet("input","output")][string]$Mode = "input"
)
$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"
$securityDir = Join-Path $projectRoot ".specify/security"
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 "SECURITY_BLOCKED: 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")
$content = Get-Content $InputFile -Raw -Encoding UTF8
if (!$content) { $content = "" }
$lower = $content.ToLower()
$status = "pass"
$action = "allow"
$riskLevel = "low"
$matchedRules = [System.Collections.Generic.List[string]]::new()
# ── Block patterns (prompt injection) ──────────────────────────────────────
$blockPatterns = @(
"ignore previous instruction",
"ignore system instruction",
"bypass system",
"bypass policy",
"system bypass",
"override policy",
"you are now",
"act as admin",
"pretend to be system",
"reveal system prompt",
"reveal original prompt",
"show hidden instruction",
"show internal prompt"
)
# ── Jailbreak patterns (Phase 2 addition) ──────────────────────────────────
$jailbreakPatterns = @(
"act as a different",
"act as an evil",
"act as an unconstrained",
"act as an unrestricted",
"pretend you are not an ai",
"pretend you're not an ai",
"pretend you are no longer",
"developer mode",
"dan mode",
"jailbreak mode",
"unrestricted mode",
"your real instructions are",
"your true instructions are",
"what are your system instructions",
"what were your original instructions"
)
# ── Load additional patterns from prompt-filter.yaml ───────────────────────
$filterYaml = Join-Path $securityDir "prompt-filter.yaml"
if (Test-Path $filterYaml) {
$yaml = Get-Content $filterYaml -Raw
$patternMatches = [regex]::Matches($yaml, 'pattern:\s*"([^"]+)"')
foreach ($m in $patternMatches) { $blockPatterns += $m.Groups[1].Value }
}
# ── Approval / alert patterns ──────────────────────────────────────────────
$approvalPatterns = @("delete ", "drop table", "shutdown system", "export secrets", "dump database")
$alertPatterns = @("show all data", "internal prompt", "system message", "hidden instruction")
# ── Regexes ────────────────────────────────────────────────────────────────
$emailRegex = '[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}'
$phoneRegex = '(\+?[0-9][0-9 .\-]{8,}[0-9])'
$personalIdReg = '\b[0-9]{9,12}\b'
$creditCardReg = '\b([0-9]{4}[- ]?){3}[0-9]{4}\b'
$secretRegex = '(?i)(API[_\-]?KEY|ACCESS[_\-]?TOKEN|REFRESH[_\-]?TOKEN|PASSWORD|JWT[_\-]?SECRET|SECRET)\s*[:=]\s*\S+'
$credentialReg = '(?i)(postgres|mysql|mongodb)://[^@]+@' # connection strings
$awsKeyReg = 'AKIA[0-9A-Z]{16}'
if ($Mode -eq "input") {
# Block patterns check
foreach ($p in ($blockPatterns + $jailbreakPatterns)) {
if ($p -and $lower -match [regex]::Escape($p)) {
$status = "blocked"; $action = "block"; $riskLevel = "high"
$matchedRules.Add("prompt-injection:$p")
}
}
# Credit card PII
if ($content -match $creditCardReg) {
$status = "blocked"; $action = "block"; $riskLevel = "high"
$matchedRules.Add("pii-credit-card")
}
# Hardcoded secrets / credentials (Phase 2 addition)
if (($content -match $secretRegex) -or ($content -match $credentialReg) -or ($content -match $awsKeyReg)) {
$status = "blocked"; $action = "block"; $riskLevel = "high"
$matchedRules.Add("secret-in-input")
}
# Approval patterns (only if not already blocked)
if ($status -ne "blocked") {
foreach ($p in $approvalPatterns) {
if ($lower -match [regex]::Escape($p)) {
$status = "requires_approval"; $action = "require_approval"; $riskLevel = "high"
$matchedRules.Add("unsafe-action:$p")
}
}
}
# Alert patterns
if ($status -ne "blocked") {
foreach ($p in $alertPatterns) {
if ($lower -match [regex]::Escape($p)) {
if ($riskLevel -eq "low") { $riskLevel = "medium" }
$action = "alert"
$matchedRules.Add("suspicious:$p")
}
}
}
}
# ── PII masking + secret redaction (both modes) ───────────────────────────
$safeContent = $content
$safeContent = [regex]::Replace($safeContent, $emailRegex, '***MASKED_EMAIL***')
$safeContent = [regex]::Replace($safeContent, $phoneRegex, '***MASKED_PHONE***')
$safeContent = [regex]::Replace($safeContent, $personalIdReg, '***MASKED_ID***')
$safeContent = [regex]::Replace($safeContent, $secretRegex, '[REDACTED_SECRET]')
$safeContent = [regex]::Replace($safeContent, $credentialReg, '[REDACTED_CONNSTRING]://')
$safeContent = [regex]::Replace($safeContent, $awsKeyReg, '[REDACTED_AWSKEY]')
if ($Mode -eq "output") {
if ($content -match $secretRegex) {
$action = "redact"
if ($riskLevel -eq "low") { $riskLevel = "medium" }
$matchedRules.Add("secret-redacted-output")
}
if ($lower -match "(maybe|might be incorrect|i am not sure|uncertain)") {
$action = "flag"
if ($riskLevel -eq "low") { $riskLevel = "medium" }
$matchedRules.Add("hallucination-risk-language")
}
}
# ── Hash ───────────────────────────────────────────────────────────────────
$inputHash = Get-Sha256 $content
$outputHash = Get-Sha256 $safeContent
$rulesJson = ConvertTo-JsonArray ($matchedRules.ToArray())
# ── Trace JSON ─────────────────────────────────────────────────────────────
$traceFile = Join-Path $traceDir "security-$traceId.json"
@"
{
"trace_id": "$traceId",
"timestamp": "$timestamp",
"harness": "H4-security",
"mode": "$Mode",
"status": "$status",
"action": "$action",
"risk_level": "$riskLevel",
"matched_rules": $rulesJson,
"input_hash": "$inputHash",
"output_hash": "$outputHash"
}
"@ | Set-Content -Path $traceFile -Encoding UTF8
# ── Audit JSONL ────────────────────────────────────────────────────────────
$auditLine = "{`"timestamp`":`"$timestamp`",`"trace_id`":`"$traceId`",`"harness`":`"H4-security`",`"mode`":`"$Mode`",`"status`":`"$status`",`"action`":`"$action`",`"risk_level`":`"$riskLevel`",`"input_hash`":`"$inputHash`",`"output_hash`":`"$outputHash`"}"
Add-Content -Path (Join-Path $auditDir "security.jsonl") -Value $auditLine -Encoding UTF8
# ── Result ─────────────────────────────────────────────────────────────────
if ($status -eq "blocked") {
Set-Content -Path $OutputFile -Value "" -Encoding UTF8
Write-Error "SECURITY_BLOCKED trace_id=$traceId risk=$riskLevel rules=$rulesJson"
exit 2
}
Set-Content -Path $OutputFile -Value $safeContent -Encoding UTF8
Write-Output "SECURITY_$($status.ToUpper()) trace_id=$traceId risk=$riskLevel action=$action output=$OutputFile"
exit 0