Files
CASAN/AINative_OKR_CASAN5/.specify/scripts/bash/decode-suspicious.py
T
thanhnvandClaude Opus 4.8 ac40c0b281 feat(h4-hardening): Plan-07 Track A — A1 strict semantic, A2 unicode/encoding, A3 tool-output scan
A1 (V1): CASAN_SECURITY_STRICT=1 makes semantic classification REQUIRED and
  fail-closed — model unavailable/no-verdict → BLOCK, never a silent SKIP.
  Non-strict CASAN_SEMANTIC_CLASSIFY=1 keeps regex verdict but logs
  SEMANTIC_SKIPPED loudly (sourced casan-log.sh). Default (no flags) unchanged.
A2 (V3/V4): unicode-normalize.py (NFKC + zero-width strip + Cyrillic/Greek
  homoglyph fold) and decode-suspicious.py (base64/hex decode + rescan, printable
  filter to avoid false positives) feed new match_either/secret_match haystacks.
  Blocks homoglyph, zero-width, fullwidth, base64/hex-smuggled injection & secrets.
A3 (V7): tool-output-scan.sh scans tool output for injection/secret before it
  re-enters model context; wrapper runs it after H6-exec (mode off|warn|block,
  strict→block). warn is default to preserve benign-draft behaviour.

Baseline preserved: run-casan4 35/35, adversarial 44/44.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 22:20:50 +09:00

65 lines
2.0 KiB
Python
Executable File

#!/usr/bin/env python3
"""CASAN H4 — Suspicious base64/hex decoder (Track A, V4 encoding smuggling).
Reads text on stdin. Finds embedded base64 and hex blobs, decodes them, and
prints the decoded plaintext (one blob per line) on stdout so the caller can
re-run its injection/secret pattern scan on the *decoded* content.
Safety / no-false-positive design:
* Only blobs >= MIN_LEN characters are considered (short words are ignored).
* A decoded blob is emitted ONLY if it is mostly printable text. Random
base64-looking words (e.g. "objectives", DER key bytes) decode to
non-printable garbage and are dropped, so they can never trigger a match.
* Output is advisory: the caller decides a decoded blob is malicious only if
the decoded text itself matches a block/secret pattern.
Deterministic: same input always yields the same output.
"""
import base64
import binascii
import re
import sys
MIN_LEN = 16
PRINTABLE_RATIO = 0.8
B64_RE = re.compile(r"[A-Za-z0-9+/]{%d,}={0,2}" % MIN_LEN)
HEX_RE = re.compile(r"\b[0-9a-fA-F]{%d,}\b" % MIN_LEN)
def _mostly_printable(text: str) -> bool:
if not text:
return False
ok = sum(1 for c in text if c.isprintable() or c.isspace())
return ok >= PRINTABLE_RATIO * len(text)
def decode_blobs(data: str):
out = []
for m in B64_RE.findall(data):
pad = m + "=" * ((4 - len(m) % 4) % 4)
try:
dec = base64.b64decode(pad, validate=True)
except (binascii.Error, ValueError):
continue
txt = dec.decode("utf-8", "ignore")
if _mostly_printable(txt):
out.append(txt)
for m in HEX_RE.findall(data):
if len(m) % 2 != 0:
continue
try:
dec = bytes.fromhex(m)
except ValueError:
continue
txt = dec.decode("utf-8", "ignore")
if _mostly_printable(txt):
out.append(txt)
return out
if __name__ == "__main__":
blobs = decode_blobs(sys.stdin.read())
if blobs:
sys.stdout.write("\n".join(blobs))