Files
CASAN/AINative_OKR_CASAN5/packages/casan-harness/scripts/powershell/security-check.ps1
T
thanhnvandClaude Opus 4.8 664bd1f00c feat(plan-01): Phase 1 — relocate harness code to packages/casan-harness (symlink facade)
Physically move the pure-code subtrees out of .specify into the package, leaving
compat symlinks at the old .specify/<dir> paths so every existing reference (internal
CASAN_HARNESS_ROOT + external CI/docker/mjs) keeps resolving. Runtime state stays put.

Moved (git mv): scripts/ tests/ security/ templates/ config/ governance/ memory/
  .specify/<dir>  ->  packages/casan-harness/<dir>   (+ .specify/<dir> symlink)
Stays in .specify (state/governance/domain, handled later): logs/ agentops/ level5/
  init-options.json traceability-map.json

Python `.resolve()` self-location followed the compat symlink into packages and lost
the app root; generate-casan-demo-context.py, generate-agentops-dashboard.py and
dashboard-server.py now walk UP for the `.specify` state marker instead of a fixed
parent depth (fixes "missing trace files" in run-casan4).

Full gate: PASS=64 FAIL=0 SKIP=3 (CASAN_CI_STEP_TIMEOUT_SEC=1200 — track-a ~450s runs
close to the 600s default and can tip over under load; this is timing variance, not a
regression — it passed cleanly with headroom). Runtime log/audit artifacts kept unstaged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 00:06:00 +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