Files
STEGOSAURUS-WRECKS/stegg_cli.py
T
GangGreenTemperTatum 6c1e972454 Add subprocess CLI wrapper and skill for agent integration
stegg_cli.py — thin argparse CLI outputting JSON, designed for agent
subprocess consumption (zero context cost). 13 commands matching the
MCP server 1:1: encode, decode, analyze, detect, capacity,
inject-chunk, read-chunks, inject-exif, inject-name, templates,
analysis-tool, list-tools, crypto-status.

Skills:
- skills/stegg-cli/SKILL.md — 97% tessl review score
  (description 100%, content 92%)

Entry point: stegg-cli (pip install stegg)
2026-06-15 09:05:52 -04:00

436 lines
14 KiB
Python

#!/usr/bin/env python3
"""
stegg-cli: Thin CLI wrapper around the ST3GG steganography engine.
Designed for agent consumption — compact JSON output, subprocess-friendly,
zero context cost. Runs as a subprocess so output stays out of LLM context.
Subcommands:
encode -i IMAGE [-t TEXT|-f FILE] [-o OUT] [--channels C] [--bits N] [--password P]
decode -i IMAGE [-o OUT] [--no-auto] [--channels C] [--bits N] [--password P]
analyze IMAGE [--full]
detect IMAGE
capacity IMAGE [--channels C] [--bits N]
inject-chunk -i IMAGE -o OUT [--type TYPE] [--keyword K] --text TEXT
read-chunks IMAGE
inject-exif -i IMAGE -o OUT [--comment C] [--author A] [--custom-fields JSON]
inject-name [--template T] [--channels C] [--count N]
templates
analysis-tool IMAGE ACTION
list-tools
crypto-status
"""
import argparse
import io
import json
import sys
from pathlib import Path
# Add repo root to path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from PIL import Image
from steg_core import (
encode, decode, create_config, calculate_capacity,
analyze_image, detect_encoding,
)
from analysis_tools import execute_action, list_available_tools
from injector import (
generate_injection_filename, get_jailbreak_template,
get_jailbreak_names, inject_text_chunk, inject_itxt_chunk,
inject_private_chunk, read_png_chunks, extract_text_chunks,
inject_metadata_pil,
)
try:
from crypto import encrypt, decrypt, get_available_methods, crypto_status
except Exception:
encrypt = decrypt = None
def get_available_methods(): return ["xor"]
def crypto_status(): return {"cryptography_available": False, "available_methods": ["xor"]}
import numpy as np
class _Encoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, (np.bool_,)): return bool(obj)
if isinstance(obj, (np.integer,)): return int(obj)
if isinstance(obj, (np.floating,)): return float(obj)
if isinstance(obj, np.ndarray): return obj.tolist()
return super().default(obj)
def _out(obj):
print(json.dumps(obj, cls=_Encoder))
def _err(msg):
_out({"error": msg})
sys.exit(1)
def _load_image(path):
p = Path(path).expanduser().resolve()
if not p.exists():
_err(f"Image not found: {p}")
return Image.open(p), p
# ---------------------------------------------------------------------------
# Subcommands
# ---------------------------------------------------------------------------
def cmd_encode(args):
image, _ = _load_image(args.input)
if args.file:
fp = Path(args.file).expanduser().resolve()
if not fp.exists():
_err(f"Payload file not found: {fp}")
payload = fp.read_bytes()
elif args.text:
payload = args.text.encode("utf-8")
else:
_err("Provide --text or --file")
out = Path(args.output).expanduser().resolve() if args.output else Path(args.input).with_name(
Path(args.input).stem + "_steg.png"
)
config = create_config(
channels=args.channels, bits=args.bits,
compress=not args.no_compress,
strategy=args.strategy, seed=args.seed or None,
)
cap = calculate_capacity(image, config)
if len(payload) > cap["usable_bytes"]:
_err(f"Payload too large: {len(payload)} > {cap['usable_bytes']} bytes")
encrypted = False
if args.password and encrypt:
payload = encrypt(payload, args.password)
encrypted = True
encode(image, payload, config, str(out))
_out({
"output": str(out), "payload_bytes": len(payload),
"capacity": cap["human"], "encrypted": encrypted,
})
def cmd_decode(args):
image, _ = _load_image(args.input)
config = None
detected = False
if not args.no_auto:
det = detect_encoding(image)
if det:
detected = True
config = None
else:
config = create_config(
channels=args.channels, bits=args.bits,
strategy=args.strategy, seed=args.seed or None,
)
else:
config = create_config(
channels=args.channels, bits=args.bits,
strategy=args.strategy, seed=args.seed or None,
)
try:
data = decode(image, config)
except Exception as e:
_err(f"Decode failed: {e}")
if args.password and decrypt:
try:
data = decrypt(data, args.password)
except Exception as e:
_err(f"Decryption failed: {e}")
result = {"bytes": len(data), "auto_detected": detected}
if args.output:
out = Path(args.output).expanduser().resolve()
out.write_bytes(data)
result["output"] = str(out)
try:
result["text"] = data.decode("utf-8")
except UnicodeDecodeError:
result["hex_preview"] = data[:512].hex()
_out(result)
def cmd_analyze(args):
image, p = _load_image(args.image)
analysis = analyze_image(image)
channels = {}
max_ind = 0.0
for ch, d in analysis["channels"].items():
lsb = d["lsb_ratio"]
ind = d.get("chi_square_indicator", 0.0)
max_ind = max(max_ind, ind)
channels[ch] = {
"mean": round(d["mean"], 2), "std": round(d["std"], 2),
"lsb_zeros_pct": round(lsb["zeros"] * 100, 1),
"chi_sq_indicator": round(ind, 4),
"anomaly": "HIGH" if ind > 0.3 else ("slight" if ind > 0.1 else "normal"),
}
verdict = ("HIGH PROBABILITY" if max_ind > 0.3
else "Possible" if max_ind > 0.1
else "No indicators")
result = {
"dimensions": analysis["dimensions"], "mode": analysis["mode"],
"pixels": analysis["total_pixels"], "channels": channels,
"capacity": analysis["capacity_by_config"], "verdict": verdict,
}
if args.full:
from analysis_tools import png_full_analysis
try:
result["full_analysis"] = png_full_analysis(p.read_bytes())
except Exception as e:
result["full_analysis_error"] = str(e)
_out(result)
def cmd_detect(args):
image, _ = _load_image(args.image)
det = detect_encoding(image)
_out({"detected": bool(det), "config": det} if det else {"detected": False})
def cmd_capacity(args):
image, _ = _load_image(args.image)
config = create_config(channels=args.channels, bits=args.bits)
cap = calculate_capacity(image, config)
_out({
"usable_bytes": cap["usable_bytes"], "human": cap["human"],
"pixels": image.width * image.height,
})
def cmd_inject_chunk(args):
p = Path(args.input).expanduser().resolve()
if not p.exists():
_err(f"Image not found: {p}")
raw = p.read_bytes()
ct = args.type
if ct == "iTXt":
modified = inject_itxt_chunk(raw, args.keyword, args.text)
elif len(ct) == 4 and ct not in ("tEXt", "zTXt", "iTXt"):
modified = inject_private_chunk(raw, ct, args.text.encode("utf-8"))
else:
modified = inject_text_chunk(raw, args.keyword, args.text, compressed=args.compressed)
out = Path(args.output).expanduser().resolve()
out.write_bytes(modified)
_out({"output": str(out), "chunk_type": ct, "keyword": args.keyword, "bytes": len(args.text)})
def cmd_read_chunks(args):
p = Path(args.image).expanduser().resolve()
if not p.exists():
_err(f"Image not found: {p}")
raw = p.read_bytes()
chunks = read_png_chunks(raw)
text = extract_text_chunks(raw)
summary = [{"type": c.get("type", "?"), "size": c.get("length", 0)} for c in chunks]
_out({"chunks": summary, "text": text, "total": len(chunks)})
def cmd_inject_exif(args):
p = Path(args.input).expanduser().resolve()
if not p.exists():
_err(f"Image not found: {p}")
meta = {}
if args.comment: meta["Comment"] = args.comment
if args.author: meta["Author"] = args.author
if args.description: meta["Description"] = args.description
if args.title: meta["Title"] = args.title
if args.custom_fields:
try:
meta.update(json.loads(args.custom_fields))
except json.JSONDecodeError:
_err("--custom-fields must be valid JSON")
if not meta:
_err("Provide at least one metadata field")
image = Image.open(p)
_, png_bytes = inject_metadata_pil(image, meta)
out = Path(args.output).expanduser().resolve()
out.write_bytes(png_bytes)
_out({"output": str(out), "fields": list(meta.keys())})
def cmd_inject_name(args):
names = [generate_injection_filename(args.template, args.channels) for _ in range(args.count)]
_out({"filenames": names})
def cmd_templates(_args):
templates = {}
for name in get_jailbreak_names():
content = get_jailbreak_template(name)
templates[name] = content[:120] + ("..." if len(content) > 120 else "")
_out({"templates": templates, "count": len(templates)})
def cmd_analysis_tool(args):
p = Path(args.image).expanduser().resolve()
if not p.exists():
_err(f"File not found: {p}")
result = execute_action(args.action, p.read_bytes())
if hasattr(result, "to_dict"):
_out(result.to_dict())
else:
_out({"result": str(result)})
def cmd_list_tools(_args):
tools = list_available_tools()
_out({"tools": tools, "count": len(tools)})
def cmd_crypto_status(_args):
s = crypto_status()
_out(s if isinstance(s, dict) else {"status": str(s)})
# ---------------------------------------------------------------------------
# Parser
# ---------------------------------------------------------------------------
def build_parser():
p = argparse.ArgumentParser(prog="stegg-cli", description="ST3GG steganography CLI for agents")
sub = p.add_subparsers(dest="command", required=True)
# encode
enc = sub.add_parser("encode", help="Encode data into an image")
enc.add_argument("-i", "--input", required=True, help="Carrier image")
enc.add_argument("-t", "--text", help="Text payload")
enc.add_argument("-f", "--file", help="File payload")
enc.add_argument("-o", "--output", help="Output path")
enc.add_argument("--channels", default="RGB")
enc.add_argument("--bits", type=int, default=1)
enc.add_argument("--strategy", default="interleaved")
enc.add_argument("--seed", type=int, default=0)
enc.add_argument("--password", default="")
enc.add_argument("--no-compress", action="store_true")
enc.set_defaults(func=cmd_encode)
# decode
dec = sub.add_parser("decode", help="Decode data from an image")
dec.add_argument("-i", "--input", required=True, help="Encoded image")
dec.add_argument("-o", "--output", help="Save extracted data to file")
dec.add_argument("--no-auto", action="store_true", help="Disable auto-detect")
dec.add_argument("--channels", default="RGB")
dec.add_argument("--bits", type=int, default=1)
dec.add_argument("--strategy", default="interleaved")
dec.add_argument("--seed", type=int, default=0)
dec.add_argument("--password", default="")
dec.set_defaults(func=cmd_decode)
# analyze
ana = sub.add_parser("analyze", help="Analyze image for steg indicators")
ana.add_argument("image", help="Image to analyze")
ana.add_argument("--full", action="store_true", help="Full 264-function analysis")
ana.set_defaults(func=cmd_analyze)
# detect
det = sub.add_parser("detect", help="Quick STEG v3 header check")
det.add_argument("image")
det.set_defaults(func=cmd_detect)
# capacity
cap = sub.add_parser("capacity", help="Calculate carrier capacity")
cap.add_argument("image")
cap.add_argument("--channels", default="RGB")
cap.add_argument("--bits", type=int, default=1)
cap.set_defaults(func=cmd_capacity)
# inject-chunk
ic = sub.add_parser("inject-chunk", help="Inject PNG text chunk")
ic.add_argument("-i", "--input", required=True)
ic.add_argument("-o", "--output", required=True)
ic.add_argument("--type", default="tEXt")
ic.add_argument("--keyword", default="Comment")
ic.add_argument("--text", required=True)
ic.add_argument("--compressed", action="store_true")
ic.set_defaults(func=cmd_inject_chunk)
# read-chunks
rc = sub.add_parser("read-chunks", help="Read PNG chunks")
rc.add_argument("image")
rc.set_defaults(func=cmd_read_chunks)
# inject-exif
ie = sub.add_parser("inject-exif", help="Inject EXIF metadata")
ie.add_argument("-i", "--input", required=True)
ie.add_argument("-o", "--output", required=True)
ie.add_argument("--comment", default="")
ie.add_argument("--author", default="")
ie.add_argument("--description", default="")
ie.add_argument("--title", default="")
ie.add_argument("--custom-fields", default="")
ie.set_defaults(func=cmd_inject_exif)
# inject-name
inj = sub.add_parser("inject-name", help="Generate injection filenames")
inj.add_argument("--template", default="universal_decoder")
inj.add_argument("--channels", default="RGB")
inj.add_argument("--count", type=int, default=1)
inj.set_defaults(func=cmd_inject_name)
# templates
tmpl = sub.add_parser("templates", help="List jailbreak templates")
tmpl.set_defaults(func=cmd_templates)
# analysis-tool
at = sub.add_parser("analysis-tool", help="Run a specific analysis function")
at.add_argument("image")
at.add_argument("action")
at.set_defaults(func=cmd_analysis_tool)
# list-tools
lt = sub.add_parser("list-tools", help="List all analysis actions")
lt.set_defaults(func=cmd_list_tools)
# crypto-status
cs = sub.add_parser("crypto-status", help="Check encryption availability")
cs.set_defaults(func=cmd_crypto_status)
return p
def main():
parser = build_parser()
args = parser.parse_args()
try:
args.func(args)
except SystemExit:
raise
except Exception as e:
_err(str(e))
if __name__ == "__main__":
main()