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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
7101af9fd4
commit
36a4812ef3
@@ -0,0 +1,218 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user