ST3GG/tests/test_mcp_server.py

671 lines
26 KiB
Python

"""
Tests for the ST3GG MCP server.
Covers all 13 tools with positive, negative, and edge-case scenarios
using a real carrier image (basi_team_six.png, 1024x1024 PNG).
"""
import json
import os
import sys
import tempfile
from pathlib import Path
import pytest
# Ensure repo root is on sys.path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from mcp_server import (
stegg_encode,
stegg_decode,
stegg_analyze,
stegg_capacity,
stegg_detect,
stegg_inject_chunk,
stegg_read_chunks,
stegg_inject_exif,
stegg_injection_filename,
stegg_jailbreak_templates,
stegg_analysis_tool,
stegg_list_analysis_tools,
stegg_crypto_status,
)
FIXTURES = Path(__file__).parent / "fixtures"
CARRIER = str(FIXTURES / "basi_team_six.png")
@pytest.fixture
def tmp_dir():
with tempfile.TemporaryDirectory(prefix="stegg_test_") as d:
yield Path(d)
# ---------------------------------------------------------------------------
# stegg_encode
# ---------------------------------------------------------------------------
class TestEncode:
def test_encode_text(self, tmp_dir):
out = str(tmp_dir / "encoded.png")
result = json.loads(stegg_encode(CARRIER, payload_text="hello world", output_path=out))
assert "error" not in result
assert result["payload_bytes"] > 0
assert result["channels"] == "RGB"
assert Path(out).exists()
assert Path(out).stat().st_size > 0
def test_encode_file_payload(self, tmp_dir):
payload_file = tmp_dir / "secret.txt"
payload_file.write_text("file-based secret payload")
out = str(tmp_dir / "encoded.png")
result = json.loads(stegg_encode(CARRIER, payload_file=str(payload_file), output_path=out))
assert "error" not in result
assert result["payload_bytes"] > 0
def test_encode_with_encryption(self, tmp_dir):
out = str(tmp_dir / "encrypted.png")
result = json.loads(stegg_encode(CARRIER, payload_text="encrypted msg", output_path=out, password="s3cret"))
assert "error" not in result
assert result["encrypted"] is True
def test_encode_various_channels(self, tmp_dir):
for ch in ["R", "G", "B", "RG", "RGBA"]:
out = str(tmp_dir / f"encoded_{ch}.png")
result = json.loads(stegg_encode(CARRIER, payload_text="test", output_path=out, channels=ch))
assert "error" not in result, f"Failed for channel {ch}: {result}"
assert result["channels"] == ch
def test_encode_various_strategies(self, tmp_dir):
for strat in ["sequential", "interleaved", "spread", "randomized"]:
out = str(tmp_dir / f"encoded_{strat}.png")
seed = 42 if strat == "randomized" else 0
result = json.loads(stegg_encode(
CARRIER, payload_text="test", output_path=out,
strategy=strat, seed=seed,
))
assert "error" not in result, f"Failed for strategy {strat}: {result}"
def test_encode_high_bit_depth(self, tmp_dir):
out = str(tmp_dir / "encoded_4bit.png")
result = json.loads(stegg_encode(CARRIER, payload_text="deep bits", output_path=out, bits_per_channel=4))
assert "error" not in result
assert result["bits_per_channel"] == 4
def test_encode_no_compression(self, tmp_dir):
out = str(tmp_dir / "uncompressed.png")
result = json.loads(stegg_encode(CARRIER, payload_text="no compress", output_path=out, compress=False))
assert "error" not in result
assert result["compressed"] is False
def test_encode_default_output_path(self, tmp_dir):
# Use a copy in tmp_dir so default output lands there
import shutil
local_carrier = str(tmp_dir / "carrier.png")
shutil.copy(CARRIER, local_carrier)
result = json.loads(stegg_encode(local_carrier, payload_text="default path"))
assert "error" not in result
assert Path(result["output_path"]).exists()
def test_encode_missing_image(self):
result = json.loads(stegg_encode("/nonexistent/image.png", payload_text="fail"))
assert "error" in result
def test_encode_no_payload(self, tmp_dir):
out = str(tmp_dir / "nopayload.png")
result = json.loads(stegg_encode(CARRIER, output_path=out))
assert "error" in result
def test_encode_payload_too_large(self, tmp_dir):
out = str(tmp_dir / "toolarge.png")
# 1024x1024 RGB 1-bit = ~384KB usable; send 1MB
huge = "X" * (1024 * 1024)
result = json.loads(stegg_encode(CARRIER, payload_text=huge, output_path=out))
assert "error" in result
assert "too large" in result["error"].lower() or "Payload" in result["error"]
def test_encode_missing_payload_file(self, tmp_dir):
out = str(tmp_dir / "missing.png")
result = json.loads(stegg_encode(CARRIER, payload_file="/nonexistent/file.bin", output_path=out))
assert "error" in result
# ---------------------------------------------------------------------------
# stegg_decode
# ---------------------------------------------------------------------------
class TestDecode:
def _encode_helper(self, tmp_dir, text="roundtrip test", **kwargs):
out = str(tmp_dir / "encoded.png")
enc = json.loads(stegg_encode(CARRIER, payload_text=text, output_path=out, **kwargs))
assert "error" not in enc
return out
def test_decode_auto_detect(self, tmp_dir):
encoded = self._encode_helper(tmp_dir, text="auto detect me")
result = json.loads(stegg_decode(encoded))
assert result["auto_detected"] is True
assert result["text"] == "auto detect me"
def test_decode_manual_config(self, tmp_dir):
encoded = self._encode_helper(tmp_dir, text="manual config", channels="RG", bits_per_channel=2)
result = json.loads(stegg_decode(
encoded, auto_detect=True,
))
assert result["text"] == "manual config"
def test_decode_with_password(self, tmp_dir):
encoded = self._encode_helper(tmp_dir, text="secret msg", password="p4ss")
result = json.loads(stegg_decode(encoded, password="p4ss"))
assert result["text"] == "secret msg"
def test_decode_wrong_password(self, tmp_dir):
encoded = self._encode_helper(tmp_dir, text="encrypted", password="correct")
# Wrong password should error or return garbled data
result = json.loads(stegg_decode(encoded, password="wrong"))
if "error" in result:
assert "failed" in result["error"].lower() or "invalid" in result["error"].lower()
else:
# If it decoded without error, text should NOT match
assert result.get("text") != "encrypted"
def test_decode_save_to_file(self, tmp_dir):
encoded = self._encode_helper(tmp_dir, text="save to disk")
out_file = str(tmp_dir / "extracted.bin")
result = json.loads(stegg_decode(encoded, output_path=out_file))
assert Path(out_file).exists()
assert Path(out_file).read_text() == "save to disk"
def test_decode_binary_payload(self, tmp_dir):
# Encode a binary file
bin_file = tmp_dir / "binary.bin"
bin_file.write_bytes(bytes(range(256)))
out = str(tmp_dir / "binary_encoded.png")
enc = json.loads(stegg_encode(CARRIER, payload_file=str(bin_file), output_path=out))
assert "error" not in enc
extracted = str(tmp_dir / "extracted.bin")
result = json.loads(stegg_decode(out, output_path=extracted))
assert Path(extracted).read_bytes() == bytes(range(256))
def test_decode_missing_image(self):
result = json.loads(stegg_decode("/nonexistent/image.png"))
assert "error" in result
def test_decode_interleaved_auto_detect(self, tmp_dir):
"""Interleaved strategy supports auto-detection of the STEG header."""
text = "interleaved-auto"
encoded = str(tmp_dir / "enc_interleaved.png")
enc = json.loads(stegg_encode(
CARRIER, payload_text=text, output_path=encoded,
strategy="interleaved",
))
assert "error" not in enc
dec = json.loads(stegg_decode(encoded))
assert dec["text"] == text
def test_decode_sequential_strategy_manual_config(self, tmp_dir):
"""Sequential strategy requires manual config (auto-detect only scans interleaved)."""
text = "strategy-sequential"
encoded = str(tmp_dir / "enc_sequential.png")
enc = json.loads(stegg_encode(
CARRIER, payload_text=text, output_path=encoded,
strategy="sequential",
))
assert "error" not in enc
dec = json.loads(stegg_decode(
encoded, auto_detect=False, strategy="sequential",
))
assert dec["text"] == text
def test_decode_spread_strategy_returns_error(self, tmp_dir):
"""Spread and randomized strategies have known upstream decode bugs (steg_core).
The MCP server should return a JSON error, not crash.
"""
encoded = str(tmp_dir / "enc_spread.png")
enc = json.loads(stegg_encode(
CARRIER, payload_text="spread test", output_path=encoded,
strategy="spread",
))
assert "error" not in enc # Encoding should succeed
dec = json.loads(stegg_decode(encoded, auto_detect=False, strategy="spread"))
# Upstream bug: spread decode fails, but server should return error gracefully
assert "error" in dec
def test_decode_randomized_strategy_returns_error(self, tmp_dir):
"""Randomized strategy has known upstream decode issues.
The MCP server should return a JSON error, not crash.
"""
encoded = str(tmp_dir / "enc_randomized.png")
enc = json.loads(stegg_encode(
CARRIER, payload_text="randomized test", output_path=encoded,
strategy="randomized", seed=42,
))
assert "error" not in enc # Encoding should succeed
dec = json.loads(stegg_decode(
encoded, auto_detect=False, strategy="randomized", seed=42,
))
# Upstream bug: decode fails, but server should return error gracefully
assert "error" in dec
def test_decode_all_channel_presets(self, tmp_dir):
for ch in ["R", "G", "B", "RG", "RGB", "RGBA"]:
text = f"ch-{ch}"
encoded = str(tmp_dir / f"enc_{ch}.png")
enc = json.loads(stegg_encode(
CARRIER, payload_text=text, output_path=encoded, channels=ch,
))
assert "error" not in enc, f"Encode failed for {ch}"
dec = json.loads(stegg_decode(encoded))
assert dec["text"] == text, f"Decode roundtrip failed for {ch}: got {dec.get('text')}"
# ---------------------------------------------------------------------------
# stegg_analyze
# ---------------------------------------------------------------------------
class TestAnalyze:
def test_analyze_clean_image(self):
result = json.loads(stegg_analyze(CARRIER))
assert "verdict" in result
assert result["dimensions"]["width"] == 1024
assert result["dimensions"]["height"] == 1024
assert "channels" in result
assert "capacity" in result
def test_analyze_stegged_image(self, tmp_dir):
out = str(tmp_dir / "stegged.png")
stegg_encode(CARRIER, payload_text="A" * 10000, output_path=out)
result = json.loads(stegg_analyze(out))
# Large payload should trigger anomaly
assert "HIGH" in result["verdict"] or "Possible" in result["verdict"]
def test_analyze_full_mode(self, tmp_dir):
out = str(tmp_dir / "stegged.png")
stegg_encode(CARRIER, payload_text="full analysis test", output_path=out)
result = json.loads(stegg_analyze(out, full=True))
assert "full_analysis" in result or "full_analysis_error" in result
def test_analyze_missing_image(self):
result = json.loads(stegg_analyze("/nonexistent.png"))
assert "error" in result
def test_analyze_channel_fields(self):
result = json.loads(stegg_analyze(CARRIER))
for ch_name in ["R", "G", "B"]:
ch = result["channels"][ch_name]
assert "mean" in ch
assert "std" in ch
assert "lsb_zeros_pct" in ch
assert "lsb_ones_pct" in ch
assert "chi_square_indicator" in ch
assert "anomaly" in ch
assert ch["anomaly"] in ("normal", "slight", "HIGH")
# ---------------------------------------------------------------------------
# stegg_capacity
# ---------------------------------------------------------------------------
class TestCapacity:
def test_capacity_default(self):
result = json.loads(stegg_capacity(CARRIER))
assert result["usable_bytes"] > 0
assert result["total_pixels"] == 1024 * 1024
assert "human" in result
def test_capacity_single_channel(self):
result = json.loads(stegg_capacity(CARRIER, channels="R", bits_per_channel=1))
result_rgb = json.loads(stegg_capacity(CARRIER, channels="RGB", bits_per_channel=1))
# Single channel should be roughly 1/3 of RGB
assert result["usable_bytes"] < result_rgb["usable_bytes"]
def test_capacity_high_bits(self):
result_1 = json.loads(stegg_capacity(CARRIER, bits_per_channel=1))
result_4 = json.loads(stegg_capacity(CARRIER, bits_per_channel=4))
assert result_4["usable_bytes"] > result_1["usable_bytes"]
def test_capacity_missing_image(self):
result = json.loads(stegg_capacity("/nonexistent.png"))
assert "error" in result
# ---------------------------------------------------------------------------
# stegg_detect
# ---------------------------------------------------------------------------
class TestDetect:
def test_detect_clean_image(self):
result = json.loads(stegg_detect(CARRIER))
assert result["detected"] is False
def test_detect_stegged_image(self, tmp_dir):
out = str(tmp_dir / "stegged.png")
stegg_encode(CARRIER, payload_text="detectable", output_path=out)
result = json.loads(stegg_detect(out))
assert result["detected"] is True
assert "config" in result
def test_detect_missing_image(self):
result = json.loads(stegg_detect("/nonexistent.png"))
assert "error" in result
# ---------------------------------------------------------------------------
# stegg_inject_chunk / stegg_read_chunks
# ---------------------------------------------------------------------------
class TestChunks:
def test_inject_and_read_text_chunk(self, tmp_dir):
out = str(tmp_dir / "chunked.png")
inj = json.loads(stegg_inject_chunk(
CARRIER, out, chunk_type="tEXt", keyword="Comment", text="injected comment",
))
assert "error" not in inj
assert Path(out).exists()
read = json.loads(stegg_read_chunks(out))
assert read["text_content"]["Comment"] == "injected comment"
def test_inject_compressed_chunk(self, tmp_dir):
out = str(tmp_dir / "compressed_chunk.png")
inj = json.loads(stegg_inject_chunk(
CARRIER, out, chunk_type="zTXt", keyword="Description",
text="compressed description data", compressed=True,
))
assert "error" not in inj
def test_inject_itxt_chunk(self, tmp_dir):
out = str(tmp_dir / "itxt.png")
inj = json.loads(stegg_inject_chunk(
CARRIER, out, chunk_type="iTXt", keyword="Author", text="ST3GG",
))
assert "error" not in inj
def test_inject_private_chunk(self, tmp_dir):
out = str(tmp_dir / "private.png")
inj = json.loads(stegg_inject_chunk(
CARRIER, out, chunk_type="stEg", keyword="", text="private data",
))
assert "error" not in inj
def test_read_chunks_structure(self):
result = json.loads(stegg_read_chunks(CARRIER))
assert "chunks" in result
assert "total_chunks" in result
assert result["total_chunks"] > 0
# Should have at minimum IHDR, IDAT, IEND
types = [c["type"] for c in result["chunks"]]
assert "IHDR" in types
assert "IEND" in types
def test_read_chunks_missing_image(self):
result = json.loads(stegg_read_chunks("/nonexistent.png"))
assert "error" in result
def test_inject_chunk_missing_image(self, tmp_dir):
out = str(tmp_dir / "fail.png")
result = json.loads(stegg_inject_chunk("/nonexistent.png", out, text="fail"))
assert "error" in result
# ---------------------------------------------------------------------------
# stegg_inject_exif
# ---------------------------------------------------------------------------
class TestInjectExif:
def test_inject_comment_and_author(self, tmp_dir):
out = str(tmp_dir / "exif.png")
result = json.loads(stegg_inject_exif(
CARRIER, out, comment="PoC comment", author="red-team",
))
assert "error" not in result
assert result["field_count"] == 2
assert Path(out).exists()
# Verify by reading chunks
chunks = json.loads(stegg_read_chunks(out))
assert chunks["text_content"].get("Comment") == "PoC comment"
assert chunks["text_content"].get("Author") == "red-team"
def test_inject_custom_fields(self, tmp_dir):
out = str(tmp_dir / "custom_exif.png")
custom = json.dumps({"Software": "ST3GG-MCP", "X-Custom": "payload"})
result = json.loads(stegg_inject_exif(CARRIER, out, custom_fields=custom))
assert "error" not in result
assert result["field_count"] == 2
def test_inject_all_fields(self, tmp_dir):
out = str(tmp_dir / "all_fields.png")
result = json.loads(stegg_inject_exif(
CARRIER, out,
comment="c", author="a", description="d", title="t",
))
assert result["field_count"] == 4
def test_inject_no_fields(self, tmp_dir):
out = str(tmp_dir / "empty.png")
result = json.loads(stegg_inject_exif(CARRIER, out))
assert "error" in result
def test_inject_invalid_custom_json(self, tmp_dir):
out = str(tmp_dir / "bad_json.png")
result = json.loads(stegg_inject_exif(CARRIER, out, custom_fields="not json"))
assert "error" in result
def test_inject_exif_missing_image(self, tmp_dir):
out = str(tmp_dir / "fail.png")
result = json.loads(stegg_inject_exif("/nonexistent.png", out, comment="fail"))
assert "error" in result
# ---------------------------------------------------------------------------
# stegg_injection_filename
# ---------------------------------------------------------------------------
class TestInjectionFilename:
def test_single_filename(self):
result = json.loads(stegg_injection_filename())
assert len(result["filenames"]) == 1
assert result["template"] == "universal_decoder"
def test_multiple_filenames(self):
result = json.loads(stegg_injection_filename(count=5))
assert len(result["filenames"]) == 5
# Should all be unique (randomized)
assert len(set(result["filenames"])) == 5
def test_various_templates(self):
templates = ["chatgpt_decoder", "claude_decoder", "gemini_decoder",
"system_override", "universal_decoder"]
for t in templates:
result = json.loads(stegg_injection_filename(template=t))
assert "error" not in result
assert result["template"] == t
assert len(result["filenames"]) == 1
def test_custom_channels(self):
result = json.loads(stegg_injection_filename(channels="RGBA"))
assert result["channels"] == "RGBA"
assert "RGBA" in result["filenames"][0]
# ---------------------------------------------------------------------------
# stegg_jailbreak_templates
# ---------------------------------------------------------------------------
class TestJailbreakTemplates:
def test_list_templates(self):
result = json.loads(stegg_jailbreak_templates())
assert result["count"] > 0
assert "templates" in result
assert isinstance(result["templates"], dict)
def test_template_previews_truncated(self):
result = json.loads(stegg_jailbreak_templates())
for name, preview in result["templates"].items():
# Previews should be <= 123 chars (120 + "...")
assert len(preview) <= 123, f"Template {name} preview too long: {len(preview)}"
# ---------------------------------------------------------------------------
# stegg_analysis_tool / stegg_list_analysis_tools
# ---------------------------------------------------------------------------
class TestAnalysisTools:
def test_list_tools(self):
result = json.loads(stegg_list_analysis_tools())
assert result["count"] > 0
assert "tools" in result
assert isinstance(result["tools"], list)
def test_rs_analysis(self, tmp_dir):
out = str(tmp_dir / "stegged.png")
stegg_encode(CARRIER, payload_text="A" * 5000, output_path=out)
result = json.loads(stegg_analysis_tool(out, "rs_analysis"))
assert result["success"] is True
def test_png_chi_square_analysis(self):
result = json.loads(stegg_analysis_tool(CARRIER, "png_chi_square_analysis"))
assert result["success"] is True
def test_png_parse_chunks_tool(self):
result = json.loads(stegg_analysis_tool(CARRIER, "png_parse_chunks"))
assert result["success"] is True
def test_detect_unicode_steg(self):
# Create a text file with zero-width chars
import tempfile
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False, mode="w") as f:
f.write("normal text\u200b\u200c\u200d hidden")
f.flush()
result = json.loads(stegg_analysis_tool(f.name, "detect_unicode_steg"))
assert result["success"] is True
os.unlink(f.name)
def test_unknown_action(self):
result = json.loads(stegg_analysis_tool(CARRIER, "nonexistent_tool_xyz"))
assert result["success"] is False
assert "error" in result
def test_analysis_missing_file(self):
result = json.loads(stegg_analysis_tool("/nonexistent.png", "rs_analysis"))
assert "error" in result
# ---------------------------------------------------------------------------
# stegg_crypto_status
# ---------------------------------------------------------------------------
class TestCryptoStatus:
def test_crypto_status(self):
result = json.loads(stegg_crypto_status())
assert "available_methods" in result or "cryptography_available" in result
def test_has_methods(self):
result = json.loads(stegg_crypto_status())
methods = result.get("available_methods", [])
assert len(methods) > 0
assert "xor" in methods # Always available
# ---------------------------------------------------------------------------
# Integration: full pipeline tests
# ---------------------------------------------------------------------------
class TestIntegration:
"""End-to-end pipeline tests combining multiple tools."""
def test_full_pipeline_encode_analyze_detect_decode(self, tmp_dir):
"""Encode -> analyze (should detect anomaly) -> detect (should find header) -> decode."""
msg = "Full pipeline integration test with Basi Team Six!"
out = str(tmp_dir / "pipeline.png")
# Encode
enc = json.loads(stegg_encode(CARRIER, payload_text=msg, output_path=out))
assert "error" not in enc
# Analyze — should show anomaly
ana = json.loads(stegg_analyze(out))
assert "verdict" in ana
# Detect — should find header
det = json.loads(stegg_detect(out))
assert det["detected"] is True
# Decode — should recover exact message
dec = json.loads(stegg_decode(out))
assert dec["text"] == msg
def test_encrypted_pipeline(self, tmp_dir):
"""Encode encrypted -> detect -> decode with correct password."""
msg = "encrypted pipeline test"
pw = "Bas1T3amS1x!"
out = str(tmp_dir / "encrypted_pipeline.png")
enc = json.loads(stegg_encode(CARRIER, payload_text=msg, output_path=out, password=pw))
assert enc["encrypted"] is True
dec = json.loads(stegg_decode(out, password=pw))
assert dec["text"] == msg
def test_chunk_injection_preserves_steg_data(self, tmp_dir):
"""Encode data, then inject chunk — original steg data should survive."""
msg = "steg survives chunk injection"
stegged = str(tmp_dir / "stegged.png")
chunked = str(tmp_dir / "chunked.png")
stegg_encode(CARRIER, payload_text=msg, output_path=stegged)
stegg_inject_chunk(stegged, chunked, keyword="Comment", text="metadata")
# Chunk should be readable
chunks = json.loads(stegg_read_chunks(chunked))
assert chunks["text_content"]["Comment"] == "metadata"
# Steg data should still be decodable
dec = json.loads(stegg_decode(chunked))
assert dec["text"] == msg
def test_large_payload_near_capacity(self, tmp_dir):
"""Encode near-capacity payload and verify roundtrip."""
cap = json.loads(stegg_capacity(CARRIER, channels="RGBA", bits_per_channel=2))
# Use 80% of capacity to stay safe after compression overhead
size = int(cap["usable_bytes"] * 0.6)
payload = "X" * size
out = str(tmp_dir / "large.png")
enc = json.loads(stegg_encode(
CARRIER, payload_text=payload, output_path=out,
channels="RGBA", bits_per_channel=2,
))
assert "error" not in enc
dec = json.loads(stegg_decode(out))
assert dec["text"] == payload
def test_sequential_strategy_roundtrip(self, tmp_dir):
"""Sequential strategy encode/decode roundtrip with manual config."""
msg = "sequential pipeline test"
out = str(tmp_dir / "seq.png")
enc = json.loads(stegg_encode(CARRIER, payload_text=msg, output_path=out, strategy="sequential"))
assert "error" not in enc
dec = json.loads(stegg_decode(out, auto_detect=False, strategy="sequential"))
assert dec["text"] == msg
def test_exif_injection_pipeline(self, tmp_dir):
"""Inject EXIF, then read chunks to verify, then analyze."""
out = str(tmp_dir / "exif_pipeline.png")
stegg_inject_exif(CARRIER, out, comment="pipeline test", author="0xmoose")
chunks = json.loads(stegg_read_chunks(out))
assert chunks["text_content"]["Comment"] == "pipeline test"
assert chunks["text_content"]["Author"] == "0xmoose"
ana = json.loads(stegg_analyze(out))
assert "verdict" in ana