Files
CASAN/packages/casan-harness/scripts/powershell/security-check.ps1
T
thanhnvandClaude Opus 4.8 36a4812ef3 refactor(structure): promote app to repo root + remove redundant workspace cruft
Standard production layout: the OKR app (was nested under AINative_OKR_CASAN5/) is now
the repository root. No more wrapper directory.

- Promote AINative_OKR_CASAN5/* -> repo root (backend/ frontend/ packages/ apps/
  .specify/ docs/ infra/ nginx/ scripts/ + configs). Merge tool dirs: .gitea (kept the
  active deploy ci.yml, added harness-ci.yml + runbooks), .claude (agents/commands +
  launch.json), .github moved up.
- Remove redundant: 00_SUBMISSION_PACKAGE, scattered root notes (FPT_CASAN_Full.md,
  tu-tuong-casan.md, casan-tu-sinh..., casan_harness_assessment.md, source-review...,
  README_CASAN5_REFINED.md), casan-next-plans/ and optimize-docs/ (competition/planning
  artifacts — roadmap + design history preserved in git log / commit messages).
- Update all references to the old layout:
  - .gitea/workflows/{ci,harness-ci}.yml, .github/workflows/{ci,deploy}.yml:
    working-directory .; drop AINative_OKR_CASAN5/ prefix; .specify/{tests,scripts}
    -> packages/casan-harness/... (.specify/logs state kept)
  - .claude/launch.json, .gitea/*-runbook.md: path prefixes
  - CLAUDE.md, README.md: docs/input -> apps/okr/domain/input
  - policy-bundle.yaml: 8 policy paths -> packages/casan-harness/...; manifest re-signed
- secrets-scan.sh: fixture excludes -> new package/domain paths.

Full gate from the new root: PASS=64 FAIL=0 SKIP=3.

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