reverse-skills-max: initial public release (restored, redacted, hardened)

This commit is contained in:
baradok609hecl1pq6qi
2026-10-20 02:17:15 +03:00
commit da24c2bd20
2227 changed files with 443931 additions and 0 deletions

View File

@@ -0,0 +1,186 @@
"""Unit tests for skills/apk-reverse/scripts/blob_decode.py.
The encoded-blob search is the part worth pinning: the whole method exists
because a single un-rotated inflate attempt fails and the failure looks like
"this is encrypted". The tests build blobs with known parameters and require the
search to recover them.
"""
from __future__ import annotations
import gzip
import json
import zlib
import pytest
from conftest import load_script, run_script
blob_decode = load_script("blob_decode.py")
pytestmark = pytest.mark.unit
PAYLOAD = json.dumps({
"flags": {"newCheckout": True, "minVersion": 4210},
"endpoints": ["https://api.example.invalid/v3/config"],
"notes": "synthetic payload for the rotation search regression" * 3,
}).encode("utf-8")
# --------------------------------------------------------------------------- #
# outer encodings
# --------------------------------------------------------------------------- #
class TestOuterEncodings:
@pytest.mark.parametrize("which", ["base64", "base64url", "hex"])
def test_round_trip(self, which):
data = bytes(range(64))
encoded = blob_decode.outer_encode(data, which)
assert blob_decode.outer_decode(encoded, which) == data
def test_unknown_encoding_raises(self):
with pytest.raises(ValueError):
blob_decode.outer_encode(b"x", "rot13")
def test_undecodable_hex_returns_empty_not_an_exception(self):
"""The search calls this on candidate encodings; a raise would abort it."""
assert blob_decode.outer_decode("zzzz", "hex") == b""
def test_whitespace_and_newlines_are_stripped_before_decoding(self):
encoded = blob_decode.outer_encode(b"hello world", "base64")
wrapped = "\n".join([encoded[:4], encoded[4:]])
assert blob_decode.outer_decode(wrapped, "base64") == b"hello world"
def test_sniff_offers_all_three_encodings(self):
guesses = blob_decode.sniff_outer("aGVsbG8gd29ybGQgdGhpcyBpcyBhIHRlc3Q=")
assert set(guesses) >= {"base64", "base64url", "hex"}
def test_sniff_puts_hex_first_for_hex_shaped_text(self):
assert blob_decode.sniff_outer("deadbeef" * 8)[0] == "hex"
# --------------------------------------------------------------------------- #
# inner transforms
# --------------------------------------------------------------------------- #
class TestInnerTransforms:
def test_inflate_raw_round_trips_through_raw_deflate(self):
body = blob_decode.inflate_raw(PAYLOAD)
assert zlib.decompress(body, -15) == PAYLOAD
def test_inflate_raw_output_has_no_zlib_header(self):
"""A raw stream is what the rotation search needs: no header to mis-align."""
body = blob_decode.inflate_raw(b"x" * 200)
with pytest.raises(zlib.error):
zlib.decompress(body) # default wbits expects a zlib header
@pytest.mark.parametrize("name,blob", [
("raw", zlib.compressobj(9, zlib.DEFLATED, -15)),
("zlib", zlib.compressobj(9, zlib.DEFLATED, 15)),
("gzip", zlib.compressobj(9, zlib.DEFLATED, 31)),
])
def test_try_inflate_names_each_inner_transform(self, name, blob):
body = blob.compress(PAYLOAD) + blob.flush()
got = blob_decode.try_inflate(body)
assert got is not None
assert got[0] == name
assert got[1] == PAYLOAD
def test_try_inflate_returns_none_for_noise(self):
assert blob_decode.try_inflate(bytes(range(256))) is None
def test_gzip_is_accepted_via_the_stdlib_helper(self):
got = blob_decode.try_inflate(gzip.compress(PAYLOAD))
assert got is not None and got[0] == "gzip" and got[1] == PAYLOAD
# --------------------------------------------------------------------------- #
# scoring
# --------------------------------------------------------------------------- #
class TestScoring:
def test_structured_text_scores_above_random_bytes(self):
structured = blob_decode.score(PAYLOAD)
noise = blob_decode.score(bytes(range(256)) * 4)
assert structured > noise
def test_a_json_header_is_rewarded(self):
assert blob_decode.score(b'{"a": 1}') > blob_decode.score(b"aaaaaaaa")
def test_empty_output_scores_negative(self):
assert blob_decode.score(b"") < 0
# --------------------------------------------------------------------------- #
# the rotation search
# --------------------------------------------------------------------------- #
def rotate(body, cut):
return body[-cut:] + body[:-cut] if cut else body
class TestRotationSearch:
def _hits(self, blob, max_skip=0):
return blob_decode.search(blob, max_skip, lambda _msg: None)
def test_search_recovers_a_rotated_raw_stream(self):
body = blob_decode.inflate_raw(PAYLOAD)
cut = 7
hits = self._hits(rotate(body, cut))
matching = [h for h in hits if h["data"] == PAYLOAD]
assert matching, "the rotation with cut=%d was not recovered" % cut
assert matching[0]["cut"] == cut
assert matching[0]["inner"] == "raw"
assert matching[0]["size"] == len(PAYLOAD)
def test_search_recovers_a_prefixed_and_rotated_stream(self):
"""The `skip` arm: a short header in front of the compressed stream."""
body = blob_decode.inflate_raw(PAYLOAD)
blob = b"\x01\x02" + rotate(body, 5)
hits = self._hits(blob, max_skip=4)
matching = [h for h in hits if h["data"] == PAYLOAD]
assert matching, "skip=2 + cut=5 was not recovered"
assert matching[0]["skip"] == 2
def test_search_on_noise_returns_no_hits(self):
assert self._hits(bytes(range(64))) == []
def test_scores_are_reported_rounded(self):
body = blob_decode.inflate_raw(PAYLOAD)
hits = self._hits(body)
assert hits
assert all(isinstance(h["score"], float) for h in hits)
def test_the_shortest_accepted_body_is_eight_bytes(self):
"""Documented floor: `m < 8` breaks the loop, so tiny blobs yield nothing."""
assert self._hits(b"\x01\x02\x03") == []
# --------------------------------------------------------------------------- #
# the CLI-level write-back contract
# --------------------------------------------------------------------------- #
class TestEncodePath:
def test_encode_then_decode_recovers_the_payload(self, tmp_path):
"""The write-back line the tool prints must actually round-trip."""
payload_file = tmp_path / "payload.bin"
payload_file.write_bytes(PAYLOAD)
encoded_file = tmp_path / "value.txt"
enc = run_script("blob_decode.py",
["--encode", "--file", str(payload_file), "--inner", "raw",
"--outer", "base64", "--cut", "0", "--out", str(encoded_file)])
assert enc.returncode == 0, enc.output
back = run_script("blob_decode.py",
["--file", str(encoded_file), "--outer", "base64",
"--min-size", "1", "--out", str(tmp_path / "back.bin")])
assert back.returncode == 0, back.output
assert (tmp_path / "back.bin").read_bytes() == PAYLOAD
def test_an_undecodable_blob_exits_one(self, tmp_path):
value = tmp_path / "value.txt"
value.write_text("not-a-blob-at-all-just-prose-text\n", encoding="utf-8")
result = run_script("blob_decode.py", ["--file", str(value), "--outer", "hex"])
assert result.returncode == 1, result.output
assert not result.has_traceback
def test_empty_stdin_is_reported_as_invalid_input(self):
"""Exit code 2, not 1: an empty input is invalid, not a failed decode."""
result = run_script("blob_decode.py", [], stdin=" ")
assert result.returncode == 2, result.output
assert "empty input" in result.output

View File

@@ -0,0 +1,90 @@
"""The budget gate must reach the same verdict whether or not `tiktoken` is installed.
This is a small test for a defect that cost two red CI runs. `check_budget.py` measures the
always-loaded body in tokens, preferring `tiktoken` and falling back to `characters / N` when it is
absent. The first version's `N` was guessed (3.6) instead of calibrated, so a machine with tiktoken
saw ~10,900 tokens and passed while a machine without it saw ~13,200 and warned -- and this
repository's gate runner (CI's 3.11 job, and any contributor's box) is the one without it.
A gate whose answer depends on which packages the runner happens to have installed is not a gate, so
both paths are forced here and compared.
"""
from __future__ import annotations
import importlib
import sys
from pathlib import Path
import pytest
from conftest import REPO_ROOT
pytestmark = pytest.mark.unit
@pytest.fixture
def budget():
sys.path.insert(0, str(REPO_ROOT))
return importlib.import_module("check_budget")
def _has_tiktoken():
"""Whether a real tokenizer is available, reported rather than assumed.
The comparison needs *two* implementations; where there is only one, the honest outcome is a
skip with the install command, not a failure and not a silent pass. CI installs it, so the
property is verified where it matters; a contributor without it has both gates agreeing anyway,
because the fallback is calibrated to the same constant.
"""
try:
import tiktoken # noqa: F401
return True
except Exception:
return False
pytestmark = pytest.mark.unit
def test_the_fallback_estimate_agrees_with_the_real_tokenizer(budget):
"""Both measurement paths must land on the same side of the soft budget."""
if not _has_tiktoken():
pytest.skip("tiktoken is not installed here, so there is no measurement to compare against; "
"install it with `python -m pip install tiktoken`")
text = (REPO_ROOT / "skills" / "apk-reverse" / "SKILL.md").read_text(encoding="utf-8")
real, how_real = budget.token_estimate(text)
assert "tiktoken" in how_real, how_real
# Force the fallback path by handing the module a tokenizer that cannot be imported.
saved = sys.modules.get("tiktoken")
sys.modules["tiktoken"] = None # `import tiktoken` raises here
try:
fallback, how_fallback = budget.token_estimate(text)
finally:
if saved is not None:
sys.modules["tiktoken"] = saved
else:
sys.modules.pop("tiktoken", None)
assert "estimate" in how_fallback, how_fallback
drift = abs(real - fallback) / max(1, real)
assert drift < 0.05, (
"the fallback is %.1f%% away from tiktoken (%d vs %d): recalibrate TOKENS_PER_CHAR "
"against a fresh measurement rather than a guess" % (100 * drift, fallback, real))
assert (real <= budget.TOKEN_SOFT) == (fallback <= budget.TOKEN_SOFT), (
"the two paths straddle the soft budget (%d real / %d fallback vs %d): the gate would "
"answer differently on CI than on a developer machine"
% (real, fallback, budget.TOKEN_SOFT))
def test_the_fallback_constant_is_the_calibrated_one(budget):
"""Pin the constant to a measurement: `chars / tokens` on SKILL.md, recomputed here."""
text = (REPO_ROOT / "skills" / "apk-reverse" / "SKILL.md").read_text(encoding="utf-8")
real, how = budget.token_estimate(text)
if "tiktoken" not in how:
pytest.skip("no tiktoken available, so there is no measurement to calibrate against")
expected = len(text) / real
assert abs(budget.TOKENS_PER_CHAR - expected) < 0.25, (
"TOKENS_PER_CHAR is %.3f but this SKILL.md measures %.3f characters per token -- the "
"document changed, so recalibrate" % (budget.TOKENS_PER_CHAR, expected))

View File

@@ -0,0 +1,216 @@
"""The established regression for scripts/dex_patch_bytes.py.
This is the repository's pre-existing red line, not a test invented here: the
l1l3 benchmark row recorded that patching the first root check in
`MainActivity.onCreate` produces a specific file, and any refactor of the dex
reader, the spec matcher, the header fixer or the writer must reproduce it
byte for byte.
`tools/_work/bench/**` is git-ignored, so these tests skip with a printed reason
on a fresh checkout. That is deliberate: a regression marker that turns red
because a sample is absent is a marker people learn to ignore.
"""
from __future__ import annotations
import hashlib
import json
import pytest
from conftest import L1L3_PATCHED_SHA256, load_script, require_path, run_script
dexutil = load_script("dexutil.py")
pytestmark = pytest.mark.unit
SPEC_NAME = "l1_root.spec.json"
SITE_OFF = 0x9D0
OLD_BYTES = "39000e00"
NEW_BYTES = "00000000"
@pytest.fixture
def sample(bench_l1l3):
"""Paths to the recorded input, spec and expected output."""
dex = require_path(bench_l1l3 / "dex" / "classes.dex",
"git-ignored bench tree")
spec = require_path(bench_l1l3 / SPEC_NAME, "git-ignored bench tree")
expected = require_path(bench_l1l3 / "dex" / "classes.patched.dex",
"git-ignored bench tree")
return {"dex": dex, "spec": spec, "expected": expected}
def patch(sample, tmp_path, extra=()):
out = tmp_path / "out.patched.dex"
report = tmp_path / "report.json"
result = run_script("dex_patch_bytes.py",
[str(sample["dex"]), "--spec", str(sample["spec"]),
"-o", str(out), "--report", str(report)] + list(extra))
return result, out, report
# --------------------------------------------------------------------------- #
# the hash marker
# --------------------------------------------------------------------------- #
class TestRecordedRegression:
def test_the_patch_runs_and_exits_zero(self, sample, tmp_path):
result, _out, _report = patch(sample, tmp_path)
assert not result.timed_out, repr(result)
assert not result.has_traceback, result.output
assert result.returncode == 0, result.output
def test_output_matches_the_recorded_sha256(self, sample, tmp_path):
_result, out, _report = patch(sample, tmp_path)
digest = hashlib.sha256(out.read_bytes()).hexdigest()
assert digest.startswith(L1L3_PATCHED_SHA256), (
"the recorded marker moved: got %s, expected prefix %s"
% (digest, L1L3_PATCHED_SHA256))
def test_output_is_byte_identical_to_the_stored_expectation(self, sample, tmp_path):
_result, out, _report = patch(sample, tmp_path)
assert out.read_bytes() == sample["expected"].read_bytes()
def test_only_the_four_patch_bytes_and_the_header_change(self, sample, tmp_path):
"""Pin *where* the difference is, so a stray write cannot pass as a hash match.
The allowed window is bytes 8..32 (checksum + signature, recomputed) plus
the four bytes of the patched instruction. Two of those four keep their
original value (`0e00` -> `0000` clears one byte and leaves the zero
byte), and a recomputed checksum may keep a byte by coincidence, so the
assertion is containment plus the changes that must occur.
"""
_result, out, _report = patch(sample, tmp_path)
before = sample["dex"].read_bytes()
after = out.read_bytes()
assert len(before) == len(after), "an equal-length patch must not resize the file"
diffs = {i for i, (a, b) in enumerate(zip(before, after)) if a != b}
window = set(range(8, 32)) | {SITE_OFF, SITE_OFF + 1, SITE_OFF + 2, SITE_OFF + 3}
assert diffs <= window, "bytes changed outside the owned window: %r" % sorted(
diffs - window)
# The two bytes that differ between 39000e00 and 00000000 must have moved.
assert {SITE_OFF, SITE_OFF + 2} <= diffs
# Both header fields are recomputed; each must have changed somewhere.
assert diffs & set(range(8, 12)), "no checksum byte changed"
assert diffs & set(range(12, 32)), "no signature byte changed"
def test_the_written_dex_self_verifies(self, sample, tmp_path):
_result, out, _report = patch(sample, tmp_path)
data = bytearray(out.read_bytes())
assert dexutil.verify_dex_header(data) == (True, True)
def test_the_result_still_parses_and_decodes_cleanly(self, sample, tmp_path):
_result, out, _report = patch(sample, tmp_path)
dex, _entry = dexutil.load_dex(str(out))
assert dex.check() == []
found = dex.find_method("Lsg/vantagepoint/uncrackable1/MainActivity;",
"onCreate", "(Landroid/os/Bundle;)V")
assert found
_section, _idx, code_off = found
_insns, clean, expected = dex.decode_all(code_off)
assert clean, "the patched method must still decode to exactly insns_size units"
# --------------------------------------------------------------------------- #
# the report the tool writes
# --------------------------------------------------------------------------- #
class TestPatchReport:
def test_report_records_the_site_and_the_polarity_check(self, sample, tmp_path):
_result, _out, report = patch(sample, tmp_path)
reports = json.loads(report.read_text(encoding="utf-8"))
assert len(reports) == 1
entry = reports[0]
assert entry["ok"] is True
assert entry["site_off"] == SITE_OFF
assert entry["old_bytes"] == OLD_BYTES
assert entry["new_bytes"] == NEW_BYTES
assert entry["expect_next_ok"] is True
assert entry["size_delta"] == 0
def test_report_confirms_the_post_edit_instruction_boundary(self, sample, tmp_path):
_result, _out, report = patch(sample, tmp_path)
entry = json.loads(report.read_text(encoding="utf-8"))[0]
assert entry["post_decode_clean"] is True
assert entry["verifier_edges"] == 0
def test_stdout_states_the_polarity_check_passed(self, sample, tmp_path):
result, _out, _report = patch(sample, tmp_path)
assert "polarity: expect_next satisfied" in result.stdout
def test_dry_run_reports_without_writing(self, sample, tmp_path):
out = tmp_path / "should-not-exist.dex"
result = run_script("dex_patch_bytes.py",
[str(sample["dex"]), "--spec", str(sample["spec"]),
"-o", str(out), "--dry-run"])
assert result.returncode == 0, result.output
assert "dry run: nothing written" in result.stdout
assert not out.exists()
def test_default_output_name_is_derived_from_the_input(self, sample, tmp_path):
"""Run on a copy: the bench tree is evidence and is never written by this suite."""
local = tmp_path / "classes.dex"
local.write_bytes(sample["dex"].read_bytes())
result = run_script("dex_patch_bytes.py",
[str(local), "--spec", str(sample["spec"])])
assert result.returncode == 0, result.output
derived = tmp_path / "classes.patched.dex"
assert derived.exists(), result.output
assert hashlib.sha256(derived.read_bytes()).hexdigest() == \
hashlib.sha256(sample["expected"].read_bytes()).hexdigest()
# --------------------------------------------------------------------------- #
# refusal paths: a wrong spec must fail loudly, not write a plausible file
# --------------------------------------------------------------------------- #
class TestRefusals:
def test_a_dry_run_against_a_missing_match_fails(self, sample, tmp_path):
spec = tmp_path / "bad.spec.json"
spec.write_text(json.dumps([{
"name": "no such site",
"class": "Lsg/vantagepoint/uncrackable1/MainActivity;",
"method": "onCreate",
"desc": "(Landroid/os/Bundle;)V",
"match": {"kind": "if-testz", "bytes": "deadbeef"},
"replace": {"kind": "nops"},
}]), encoding="utf-8")
result = run_script("dex_patch_bytes.py",
[str(sample["dex"]), "--spec", str(spec), "--dry-run"])
assert result.returncode == 1, result.output
assert "no instruction matched" in result.output
def test_a_wrong_polarity_pin_is_refused(self, sample, tmp_path):
"""expect_next exists to make the wrong side of a branch impossible to patch."""
spec = tmp_path / "bad-polarity.spec.json"
spec.write_text(json.dumps([{
"name": "wrong expected neighbour",
"class": "Lsg/vantagepoint/uncrackable1/MainActivity;",
"method": "onCreate",
"desc": "(Landroid/os/Bundle;)V",
"match": {"kind": "if-testz", "bytes": OLD_BYTES},
"expect_next": {"kind": "return"},
"replace": {"kind": "nops"},
}]), encoding="utf-8")
result = run_script("dex_patch_bytes.py",
[str(sample["dex"]), "--spec", str(spec), "--dry-run"])
assert result.returncode == 1, result.output
assert "expect_next not satisfied" in result.output
def test_a_missing_dex_is_a_usage_error(self, tmp_path, sample):
result = run_script("dex_patch_bytes.py",
[str(tmp_path / "nope.dex"), "--spec", str(sample["spec"])])
assert result.returncode == 2, result.output
assert "no such file" in result.output
def test_a_missing_spec_argument_is_a_usage_error(self, sample):
result = run_script("dex_patch_bytes.py", [str(sample["dex"])])
assert result.returncode == 2, result.output
def test_a_non_dex_input_is_refused_with_code_1(self, tmp_path, sample):
"""Structurally invalid input is a target-side failure (1), not a usage error (2)."""
bogus = tmp_path / "bogus.dex"
bogus.write_bytes(b"not a dex at all" * 8)
result = run_script("dex_patch_bytes.py",
[str(bogus), "--spec", str(sample["spec"]), "--dry-run"])
assert result.returncode == 1, result.output
assert "not a dex" in result.output

View File

@@ -0,0 +1,393 @@
"""Unit tests for skills/apk-reverse/scripts/dexutil.py.
The dex format table is the one place where a wrong entry silently shifts every
offset after it, so widths are asserted opcode by opcode against the cases the
script's own docstring calls out. No device and no sample file are needed for
the table itself; the sample-backed assertions skip when the git-ignored bench
tree is absent.
"""
from __future__ import annotations
import hashlib
import zlib
import pytest
from conftest import load_script, require_path
dexutil = load_script("dexutil.py")
pytestmark = pytest.mark.unit
# --------------------------------------------------------------------------- #
# little-endian scalar readers
# --------------------------------------------------------------------------- #
class TestScalarReaders:
def test_u16_and_u32_are_little_endian(self):
assert dexutil.u16(b"\x01\x02", 0) == 0x0201
assert dexutil.u32(b"\x01\x02\x03\x04", 0) == 0x04030201
def test_u16_and_u32_read_from_an_offset(self):
buf = b"\xff\xff\x34\x12\x78\x56\x34\x12"
assert dexutil.u16(buf, 2) == 0x1234
assert dexutil.u32(buf, 4) == 0x12345678
@pytest.mark.parametrize("value,expected", [
(0x0000, 0),
(0x7FFF, 0x7FFF),
(0x8000, -0x8000),
(0xFFFF, -1),
])
def test_s16_boundaries(self, value, expected):
assert dexutil.s16(value) == expected
def test_read_uleb_single_byte(self):
assert dexutil.read_uleb(b"\x7f", 0) == (0x7F, 1)
def test_read_uleb_two_bytes_and_offset(self):
value, off = dexutil.read_uleb(b"\x00\x80\x01", 1)
assert value == 128
assert off == 3
def test_read_uleb_multi_byte(self):
# 0xE5 0x8E 0x26 -> 624485, the example from the format specification
value, off = dexutil.read_uleb(b"\xe5\x8e\x26", 0)
assert value == 624485
assert off == 3
# --------------------------------------------------------------------------- #
# instruction width table
# --------------------------------------------------------------------------- #
# Each case is a trap the module docstring names explicitly.
WIDTH_CASES = [
(0x00, 1, "plain nop is one unit"),
(0x02, 2, "move/from16"),
(0x03, 3, "move/16"),
(0x13, 2, "const/16"),
(0x14, 3, "const"),
(0x16, 2, "const-wide/16"),
(0x17, 3, "const-wide/32"),
(0x18, 5, "const-wide"),
(0x1A, 2, "const-string (21c) carries a 16-bit index"),
(0x1B, 3, "const-string/jumbo (31c) carries a 32-bit index"),
(0x1C, 2, "const-class"),
(0x1E, 1, "monitor-exit breaks the same-family reading"),
(0x20, 2, "instance-of"),
(0x21, 1, "array-length breaks the same-family reading"),
(0x27, 1, "throw"),
(0x28, 1, "goto (10t) is one unit"),
(0x29, 2, "goto/16 (20t)"),
(0x2A, 3, "goto/32 (30t)"),
(0x32, 2, "if-test (22t) is two units, not one"),
(0x37, 2, "last if-test"),
(0x38, 2, "if-testz (21t) is two units, not one"),
(0x3D, 2, "last if-testz"),
(0x6E, 3, "invoke-kind (35c)"),
(0x74, 3, "invoke-kind/range (3rc)"),
(0x7B, 1, "neg-int"),
(0x90, 2, "add-int (23x)"),
(0xD0, 2, "add-int/lit16 (22s)"),
(0xFA, 4, "invoke-polymorphic (45cc)"),
(0xFC, 3, "invoke-custom (35c)"),
]
class TestInstructionWidths:
@pytest.mark.parametrize("op,units,why", WIDTH_CASES,
ids=["%02x" % c[0] for c in WIDTH_CASES])
def test_width(self, op, units, why):
assert dexutil.insn_units(op, b"\x00" * 32, 0, 32) == units, why
def test_table_and_accessor_agree_for_every_listed_opcode(self):
"""insn_units must not diverge from OP_UNITS for any allocated opcode."""
buf = b"\x00" * 32
drifted = []
for op, units in sorted(dexutil.OP_UNITS.items()):
got = dexutil.insn_units(op, buf, 0, len(buf))
if got != units:
drifted.append((hex(op), units, got))
assert drifted == []
@pytest.mark.parametrize("op", [0x3E, 0x43, 0x73, 0x79, 0x7A, 0xE3, 0xF9])
def test_unallocated_opcodes_still_terminate(self, op):
"""An unlisted opcode must advance by one unit, not run off the end."""
assert dexutil.insn_units(op, b"\x00" * 8, 0, 8) == 1
def test_packed_switch_payload_width(self):
# 00 01 | size=3 -> 4 + 3*2 = 10 units
buf = bytes([0x00, 0x01]) + (3).to_bytes(2, "little") + b"\x00" * 12
assert dexutil.insn_units(0x00, buf, 0, len(buf)) == 10
def test_sparse_switch_payload_width(self):
# 00 02 | size=3 -> 2 + 3*4 = 14 units
buf = bytes([0x00, 0x02]) + (3).to_bytes(2, "little") + b"\x00" * 24
assert dexutil.insn_units(0x00, buf, 0, len(buf)) == 14
def test_fill_array_data_payload_width(self):
# 00 03 | element_width=4 | size=30 -> 4 + ceil(30*4/2) = 64 units.
# The measured case named in the docstring: counting this as 5 units is
# what desynchronises the rest of the walk.
buf = bytes([0x00, 0x03]) + (4).to_bytes(2, "little") + (30).to_bytes(4, "little")
assert dexutil.insn_units(0x00, buf, 0, len(buf)) == 64
def test_fill_array_data_payload_pads_to_an_even_byte_count(self):
# element_width=1, size=3 -> 3 bytes pads to 4 -> 4 + 2 = 6 units
buf = bytes([0x00, 0x03]) + (1).to_bytes(2, "little") + (3).to_bytes(4, "little")
assert dexutil.insn_units(0x00, buf, 0, len(buf)) == 6
def test_plain_nop_is_not_treated_as_a_payload(self):
assert dexutil.insn_units(0x00, b"\x00\x00\x00\x00", 0, 4) == 1
# --------------------------------------------------------------------------- #
# header parsing and the structural check
# --------------------------------------------------------------------------- #
# Verified dex header offsets, as declared by dexutil.OFFSETS. Asserted rather
# than assumed: these are the values a hand-written reader most often gets wrong.
HEADER_OFFSETS = {
"file_size": 0x20, "header_size": 0x24, "endian_tag": 0x28,
"map_off": 0x34, "string_ids_off": 0x3C, "type_ids_off": 0x44,
"proto_ids_off": 0x4C, "field_ids_off": 0x54, "method_ids_off": 0x5C,
"class_defs_off": 0x64,
}
# The six table pointers dex.check() requires to be inside the file.
TABLE_POINTERS = ("string_ids_off", "type_ids_off", "proto_ids_off",
"field_ids_off", "method_ids_off", "class_defs_off")
def synthetic_dex(size=512):
"""A dex-shaped buffer whose header is internally consistent.
Enough for the structural reader: magic, a matching file_size, and the six
table pointers inside the file. It is deliberately not a runnable dex, so
nothing here claims parse-level validity of a real image.
"""
buf = bytearray(size)
def put(offset, value, width=4):
buf[offset:offset + width] = value.to_bytes(width, "little")
buf[0:8] = b"dex\n035\x00"
put(HEADER_OFFSETS["file_size"], size)
put(HEADER_OFFSETS["header_size"], 0x70)
put(HEADER_OFFSETS["endian_tag"], 0x12345678)
put(HEADER_OFFSETS["map_off"], 0x70)
for key in TABLE_POINTERS:
# size 0, pointer 0x70: every pointer lands inside the file.
put(HEADER_OFFSETS[key] - 4, 0)
put(HEADER_OFFSETS[key], 0x70)
return buf
class TestHeaderParsing:
def test_the_offset_table_matches_the_format_specification(self):
for key, expected in HEADER_OFFSETS.items():
assert dexutil.OFFSETS[key] == expected, key
def test_header_fields_are_parsed_out_of_the_buffer(self):
dex = dexutil.Dex(bytes(synthetic_dex(512)))
assert dex.header["file_size"] == 512
assert dex.header["header_size"] == 0x70
assert dex.header["string_ids_off"] == 0x70
def test_magic_is_parsed(self):
dex = dexutil.Dex(bytes(synthetic_dex(256)))
assert dex.d[:4] == b"dex\n"
def test_consistent_header_reports_no_problems(self):
dex = dexutil.Dex(bytes(synthetic_dex(512)))
assert dex.check() == []
def test_bad_magic_is_reported(self):
buf = synthetic_dex(512)
buf[0:4] = b"zip\n"
assert any("not a dex" in p for p in dexutil.Dex(bytes(buf)).check())
def test_file_size_mismatch_is_reported(self):
buf = synthetic_dex(512)
buf[HEADER_OFFSETS["file_size"]:HEADER_OFFSETS["file_size"] + 4] = \
(999).to_bytes(4, "little")
assert any("file_size" in p for p in dexutil.Dex(bytes(buf)).check())
def test_out_of_range_table_pointer_is_reported(self):
buf = synthetic_dex(512)
at = HEADER_OFFSETS["string_ids_off"]
buf[at:at + 4] = (0).to_bytes(4, "little")
assert any("string_ids_off" in p for p in dexutil.Dex(bytes(buf)).check())
def test_string_safe_never_raises_on_a_junk_index(self):
"""A desynced decode passes junk indices; the reader must not throw."""
dex = dexutil.Dex(bytes(synthetic_dex(512)))
assert dex.string_safe(10**6).startswith("<string_idx")
# --------------------------------------------------------------------------- #
# branch targets
# --------------------------------------------------------------------------- #
def fake_dex(buf):
"""A Dex instance over raw bytes, without header parsing.
branch_target needs only self.d plus the instruction dict, so this keeps the
displacement arithmetic testable without a whole valid dex image.
"""
dex = dexutil.Dex.__new__(dexutil.Dex)
dex.d = bytes(buf)
dex.name = "<synthetic>"
return dex
def insn_at_zero(buf, op, raw_len):
return {"off": 0, "op": op, "raw": bytes(buf[:raw_len]),
"units": raw_len // 2, "name": "synthetic"}
class TestBranchTargets:
def test_if_testz_negative_displacement(self):
buf = bytes([0x38, 0x00]) + (-1).to_bytes(2, "little", signed=True)
assert fake_dex(buf).branch_target(insn_at_zero(buf, 0x38, 4)) == -2
def test_if_testz_positive_displacement(self):
buf = bytes([0x3D, 0x01]) + (2).to_bytes(2, "little", signed=True)
assert fake_dex(buf).branch_target(insn_at_zero(buf, 0x3D, 4)) == 4
def test_if_test_two_registers(self):
buf = bytes([0x32, 0x10]) + (-4).to_bytes(2, "little", signed=True)
assert fake_dex(buf).branch_target(insn_at_zero(buf, 0x32, 4)) == -8
def test_goto_signed_byte(self):
# 10t: op, int8. 0xFE -> -2 units -> -4 bytes from the instruction.
buf = bytes([0x28, 0xFE])
assert fake_dex(buf).branch_target(insn_at_zero(buf, 0x28, 2)) == -4
def test_goto16(self):
buf = bytes([0x29, 0x00]) + (3).to_bytes(2, "little", signed=True)
assert fake_dex(buf).branch_target(insn_at_zero(buf, 0x29, 4)) == 6
def test_goto32_negative(self):
buf = bytes([0x2A, 0x00]) + (-1).to_bytes(4, "little", signed=True)
assert fake_dex(buf).branch_target(insn_at_zero(buf, 0x2A, 6)) == -2
def test_non_branch_returns_none(self):
buf = bytes([0x0E, 0x00])
assert fake_dex(buf).branch_target(insn_at_zero(buf, 0x0E, 2)) is None
def test_branch_regs_if_testz_reads_one_register(self):
buf = bytes([0x38, 0x07]) + (-1).to_bytes(2, "little", signed=True)
regs, kind = fake_dex(buf).branch_regs(insn_at_zero(buf, 0x38, 4))
assert regs == [7]
assert kind == "if-testz"
def test_branch_regs_if_test_reads_two_registers(self):
buf = bytes([0x32, 0x21]) + (-1).to_bytes(2, "little", signed=True)
regs, kind = fake_dex(buf).branch_regs(insn_at_zero(buf, 0x32, 4))
assert regs == [2, 1]
assert kind == "if-test"
def test_branch_regs_is_empty_for_a_non_branch(self):
buf = bytes([0x0E, 0x00])
assert fake_dex(buf).branch_regs(insn_at_zero(buf, 0x0E, 2)) == ([], None)
# --------------------------------------------------------------------------- #
# header integrity: signature before checksum
# --------------------------------------------------------------------------- #
class TestHeaderIntegrity:
def test_fix_then_verify_round_trips(self):
data = bytearray(synthetic_dex(512))
dexutil.fix_dex_header(data)
assert dexutil.verify_dex_header(data) == (True, True)
def test_signature_covers_the_bytes_after_the_signature_field(self):
data = bytearray(synthetic_dex(512))
dexutil.fix_dex_header(data)
assert bytes(data[12:32]) == hashlib.sha1(bytes(data[32:])).digest()
def test_checksum_covers_the_signature_field(self):
"""The order requirement: adler32 over d[12:] includes the written signature."""
data = bytearray(synthetic_dex(512))
dexutil.fix_dex_header(data)
stored = int.from_bytes(data[8:12], "little")
assert stored == zlib.adler32(bytes(data[12:])) & 0xFFFFFFFF
def test_checksum_taken_over_a_zeroed_signature_does_not_verify(self):
"""The failure mode the module docstring names: the header never verifies."""
data = bytearray(synthetic_dex(512))
data[12:32] = b"\x00" * 20
data[8:12] = (zlib.adler32(bytes(data[12:])) & 0xFFFFFFFF).to_bytes(4, "little")
data[12:32] = hashlib.sha1(bytes(data[32:])).digest()
checksum_ok, _signature_ok = dexutil.verify_dex_header(data)
assert checksum_ok is False
def test_a_tampered_body_fails_verification(self):
data = bytearray(synthetic_dex(512))
dexutil.fix_dex_header(data)
data[-1] ^= 0xFF
assert dexutil.verify_dex_header(data) == (False, False)
def test_fix_reports_before_and_after_values(self):
data = bytearray(synthetic_dex(512))
report = dexutil.fix_dex_header(data)
assert set(report) == {"before_checksum", "after_checksum",
"before_signature", "after_signature"}
assert report["before_checksum"] != report["after_checksum"]
# --------------------------------------------------------------------------- #
# sample-backed assertions (skip when the git-ignored bench tree is absent)
# --------------------------------------------------------------------------- #
SAMPLE_CLASS = "Lsg/vantagepoint/uncrackable1/MainActivity;"
SAMPLE_METHOD = ("onCreate", "(Landroid/os/Bundle;)V")
SAMPLE_SITE = 0x9D0
SAMPLE_SITE_BYTES = "39000e00"
class TestAgainstTheRecordedSample:
"""The l1l3 fixture is git-ignored, so these skip in CI by design."""
@pytest.fixture(autouse=True)
def _need_sample(self, bench_l1l3):
self.dex_path = require_path(
bench_l1l3 / "dex" / "classes.dex",
"the l1l3 sample is a download into the git-ignored bench tree")
def _method(self):
dex, _entry = dexutil.load_dex(str(self.dex_path))
found = dex.find_method(SAMPLE_CLASS, *SAMPLE_METHOD)
assert found, "the recorded fixture must still contain MainActivity.onCreate"
_section, _idx, code_off = found
return dex, code_off
def test_sample_dex_parses_cleanly(self):
dex, _entry = dexutil.load_dex(str(self.dex_path))
assert dex.check() == []
def test_decode_walks_exactly_insns_size_units(self):
dex, code_off = self._method()
insns, clean, expected = dex.decode_all(code_off)
assert clean, "decode did not land on 0x%x" % expected
assert insns
def test_the_recorded_root_check_branch_is_at_0x9d0(self):
dex, code_off = self._method()
target = dex.insn_at(code_off, SAMPLE_SITE)
assert target is not None, "instruction boundary at 0x%x disappeared" % SAMPLE_SITE
assert target["raw"].hex() == SAMPLE_SITE_BYTES
assert target["op"] in dexutil.IF_TESTZ
def test_branch_target_resolves_to_an_instruction_boundary(self):
dex, code_off = self._method()
target = dex.insn_at(code_off, SAMPLE_SITE)
dest = dex.branch_target(target)
assert dest is not None
assert dex.insn_at(code_off, dest) is not None, (
"branch target 0x%x is not an instruction boundary" % dest)
def test_every_branch_target_is_an_instruction_boundary(self):
"""A decode desync shows up here first: a branch into the middle of an insn."""
dex, code_off = self._method()
boundaries = {i["off"] for i in dex.decode(code_off)}
stray = sorted(t for t in dex.all_targets(code_off) if t not in boundaries)
assert stray == [], "branch targets outside the instruction stream: %r" % stray

View File

@@ -0,0 +1,192 @@
"""Unit tests for skills/apk-reverse/scripts/protobuf_decode_raw.py.
Two properties are pinned here, because both are claims the reference material
makes and neither is visible from reading the decoder:
* the built-in known-answer fixture set passes in full (26/26), and
* a decode -> re-encode round trip is byte-identical for every fixture except
the one fixture that is deliberately non-canonical, which must instead
report *why* it differs rather than silently normalising.
"""
from __future__ import annotations
import pytest
from conftest import load_script, run_script
pb = load_script("protobuf_decode_raw.py")
pytestmark = pytest.mark.unit
# 12 fixtures x (fixture check + round trip) + 2 wire-format trap checks.
EXPECTED_FIXTURE_COUNT = 12
EXPECTED_TOTAL_CHECKS = 26
@pytest.fixture(scope="module")
def fixtures():
return pb._fixtures()
# --------------------------------------------------------------------------- #
# varint primitives
# --------------------------------------------------------------------------- #
class TestVarintPrimitives:
def test_canonical_example(self):
assert pb.encode_varint(150) == bytes.fromhex("9601")
def test_read_varint_returns_info_next_index_and_no_error(self):
info, nxt, err = pb.read_varint(bytes.fromhex("9601"), 0, 2)
assert err is None
assert info["value"] == 150
assert info["bytes"] == 2
assert nxt == 2
def test_a_truncated_varint_is_an_error_not_a_value(self):
info, nxt, err = pb.read_varint(bytes.fromhex("96"), 0, 1)
assert info is None
assert err and "truncated" in err
assert nxt == 0
def test_a_non_canonical_encoding_is_flagged(self):
"""A padded varint must be reported: it is why re-encode cannot be assumed equal."""
info, _nxt, _err = pb.read_varint(bytes.fromhex("8000"), 0, 2)
assert info["value"] == 0
assert info["non_canonical"] is True
assert info["canonical_bytes"] == 1
def test_varint_len_matches_the_encoding(self):
for value in (0, 1, 127, 128, 300, 2 ** 31, 2 ** 63 - 1):
assert pb.varint_len(value) == len(pb.encode_varint(value))
def test_zigzag(self):
assert pb.zigzag_decode(0) == 0
assert pb.zigzag_decode(1) == -1
assert pb.zigzag_decode(2) == 1
assert pb.zigzag_decode(4294967294) == 2147483647
def test_int_views_wrap(self):
assert pb.int32_view(0xFFFFFFFF) == -1
assert pb.int64_view(0xFFFFFFFFFFFFFFFF) == -1
# --------------------------------------------------------------------------- #
# the built-in fixture set
# --------------------------------------------------------------------------- #
class TestFixtureSet:
def test_the_selftest_function_passes_end_to_end(self, capsys):
assert pb.selftest(None) == 0
out = capsys.readouterr().out
assert "selftest: %d/%d PASS" % (EXPECTED_TOTAL_CHECKS, EXPECTED_TOTAL_CHECKS) in out
assert "FAIL" not in out
def test_fixture_count_is_stable(self, fixtures):
"""The 26/26 headline is 12 fixtures x 2 + 2; a silent drop would hide."""
assert len(fixtures) == EXPECTED_FIXTURE_COUNT
def test_every_fixture_has_a_name_payload_and_expectations(self, fixtures):
for name, payload, expects, _rt in fixtures:
assert isinstance(name, str) and name
assert isinstance(payload, (bytes, bytearray)) and payload
assert expects, "fixture %s has no assertions" % name
def test_each_declared_expectation_holds(self, fixtures):
failures = []
for name, payload, expects, _rt in fixtures:
ctx = pb.Ctx(max_depth=6)
frames = pb._fixture_frames(name, payload)
fields = []
for start, end, _p in frames:
found, _n, _s = pb.walk(payload, start, end, 0, ctx, "f", analyse=True)
fields.extend(found)
for label, num, expected in expects:
got = pb._selftest_lookup(fields, frames, payload, label, num)
if got != expected:
failures.append((name, label, expected, got))
assert failures == []
# --------------------------------------------------------------------------- #
# round trip
# --------------------------------------------------------------------------- #
class TestRoundTrip:
def _reencode(self, name, payload):
frames = pb._fixture_frames(name, payload)
tree = {"frames": [{"fields": pb.walk(payload, s, e, 0, pb.Ctx(6), "f",
analyse=True)[0]}
for s, e, _p in frames]}
report = {"fields": 0, "identical": 0, "mismatched": []}
out = pb.reencode_tree(tree, report)
expected = b"".join(payload[s:e] for s, e, _p in frames)
return out, expected, report
def test_canonical_fixtures_reencode_byte_identically(self, fixtures):
differing = []
for name, payload, _expects, rt in fixtures:
if rt == "non-canonical":
continue
out, expected, _report = self._reencode(name, payload)
if out != expected:
differing.append((name, expected.hex(), out.hex()))
assert differing == []
def test_the_non_canonical_fixture_reports_why_it_differs(self, fixtures):
"""A normalising re-encode must say so, not return different bytes quietly."""
targets = [f for f in fixtures if f[3] == "non-canonical"]
assert targets, "the non-canonical fixture disappeared from the set"
for name, payload, _expects, _rt in targets:
out, expected, report = self._reencode(name, payload)
assert out != expected
assert out.hex() == "089601"
assert report["mismatched"], "the mismatch must be recorded"
assert "non-canonical" in report["mismatched"][0]["reason"]
def test_reencode_accounts_for_every_field(self, fixtures):
for name, payload, _expects, rt in fixtures:
_out, _expected, report = self._reencode(name, payload)
assert report["fields"] > 0, name
# --------------------------------------------------------------------------- #
# the two wire-format traps, asserted rather than narrated
# --------------------------------------------------------------------------- #
class TestWireFormatTraps:
def test_explicit_zero_is_not_the_same_bytes_as_absent(self):
zero = pb.tag_0(5, 0) + pb.encode_varint(0)
assert zero != b""
def test_explicit_zero_decodes_to_value_zero(self):
zero = pb.tag_0(5, 0) + pb.encode_varint(0)
fields = pb.walk(zero, 0, len(zero), 0, pb.Ctx(6), "f")[0]
assert fields[0]["value"] == 0
def test_absent_field_decodes_to_no_fields(self):
assert pb.walk(b"", 0, 0, 0, pb.Ctx(6), "f")[0] == []
def test_a_packed_payload_offers_both_readings(self):
"""Three varints are also a valid nested message; only a schema decides."""
packed = b"".join(pb.encode_varint(v) for v in (3, 270, 86942))
cands, view = pb.analyse_payload(packed, 0, pb.Ctx(6), "f", 0)
live = sorted(c["kind"] for c in cands if c["confidence"] > 0.0)
all_kinds = sorted(c["kind"] for c in cands)
assert "packed_varint" in live
assert "nested_message" in all_kinds
assert view == "packed_varint"
# --------------------------------------------------------------------------- #
# the subprocess contract
# --------------------------------------------------------------------------- #
class TestSelftestCli:
def test_selftest_subcommand_exits_zero_and_prints_the_tally(self):
result = run_script("protobuf_decode_raw.py", ["--selftest"], timeout=180)
assert not result.timed_out
assert not result.has_traceback, result.output
assert result.returncode == 0, result.output
assert "selftest: %d/%d PASS" % (EXPECTED_TOTAL_CHECKS, EXPECTED_TOTAL_CHECKS) \
in result.stdout
def test_help_lists_the_selftest_flag(self):
result = run_script("protobuf_decode_raw.py", ["--help"])
assert result.returncode == 0
assert "--selftest" in result.stdout

View File

@@ -0,0 +1,198 @@
"""Unit tests for skills/apk-reverse/scripts/scan_leaks.py.
Decoys are built in a temporary directory at run time. No real target identity
is committed here, and the token-shaped decoys are assembled from fragments so
that this test file itself cannot become a finding if the scanner is ever
pointed at the tests tree.
"""
from __future__ import annotations
import json
import pytest
from conftest import load_script, run_script
scan_leaks = load_script("scan_leaks.py")
pytestmark = pytest.mark.unit
# Synthetic identifiers: reverse-domain-shaped, not derived from any real target.
DECOY_PACKAGE = "com." + "acme" + ".targetapp"
DECOY_TOKEN = "AbCdEf" + "0123456789" + "XyZ"
DECOY_USER = "mkessler"
def write(root, name, body):
path = root / name
path.write_text(body, encoding="utf-8")
return path
def scan(root, *extra, timeout=120):
"""Run the scanner over a temporary root and parse its JSON report."""
result = run_script("scan_leaks.py",
["--root", str(root), "--format", "json"] + list(extra),
timeout=timeout)
assert not result.timed_out, "scan_leaks.py did not finish: %r" % result
assert not result.has_traceback, result.output
# The JSON document is printed first, then the RESULT token line.
lines = result.stdout.splitlines()
token = lines[-1] if lines else ""
doc = json.loads(result.stdout[:result.stdout.rindex("}") + 1])
return result, doc, token
# --------------------------------------------------------------------------- #
# the exemption list as data
# --------------------------------------------------------------------------- #
class TestExemptions:
@pytest.mark.parametrize("value", [
"com.android.internal",
"androidx.core.app",
"com.google.firebase",
"org.example.demo",
"<pkg>",
"${PKG}",
"sg.vantagepoint.uncrackable1",
])
def test_benign_values_carry_an_exemption_reason(self, value):
assert scan_leaks._benign_package(value) != "", value
@pytest.mark.parametrize("value", [
DECOY_PACKAGE,
"com.unknownvendor.someapp",
])
def test_unknown_reverse_domain_values_are_reportable(self, value):
assert scan_leaks._benign_package(value) == "", value
def test_loopback_and_documentation_addresses_are_distinguished(self):
"""Loopback is exempt; an RFC 5737 documentation address is reported on purpose."""
assert scan_leaks._benign_ip("127.0.0.1:8080") != ""
assert scan_leaks._benign_ip("192.0.2.10:8080") == ""
def test_placeholder_user_paths_are_exempt(self):
assert scan_leaks._benign_path("C:\\Users\\<user>\\work") != ""
assert scan_leaks._benign_path("/home/" + DECOY_USER + "/work") == ""
# --------------------------------------------------------------------------- #
# decoys that must be reported
# --------------------------------------------------------------------------- #
class TestDecoyDetection:
def test_strong_decoys_are_reported(self, tmp_path):
write(tmp_path, "notes.md", "\n".join([
'package="%s"' % DECOY_PACKAGE,
"Authorization: Bearer " + DECOY_TOKEN,
"/home/%s/work/build/classes.dex" % DECOY_USER,
]) + "\n")
result, doc, token = scan(tmp_path)
rules = sorted({f["rule"] for f in doc["findings"]})
assert "bundle_pkg_attr" in rules
assert "token_authorization_header" in rules
assert "user_path_posix" in rules
assert all(f["strength"] in ("strong", "certain") for f in doc["findings"])
assert token == "RESULT=leaks_found"
assert result.returncode == 1, result.output
def test_findings_carry_context_not_just_a_line_number(self, tmp_path):
write(tmp_path, "notes.md", 'package="%s"\n' % DECOY_PACKAGE)
_result, doc, _token = scan(tmp_path)
finding = doc["findings"][0]
assert finding["context"].strip()
assert finding["match"] == DECOY_PACKAGE
assert finding["line"] == 1
assert finding["file"] == "notes.md"
def test_device_serial_shaped_token_is_reported(self, tmp_path):
# 16 uppercase alphanumerics with at least one letter, next to a device word.
serial = "R58M" + "12ABCDEF" + "9K7Q"
assert len(serial) == 16
write(tmp_path, "adb.txt", "device %s\n" % serial)
_result, doc, token = scan(tmp_path)
assert any(f["rule"].startswith("device_serial") for f in doc["findings"]), doc
assert token == "RESULT=leaks_found"
# --------------------------------------------------------------------------- #
# decoys that must not be reported
# --------------------------------------------------------------------------- #
class TestExemptDecoysStaySilent:
def test_benign_identifiers_produce_no_findings(self, tmp_path):
write(tmp_path, "notes.md", "\n".join([
'package="com.android.internal"',
'package="com.google.firebase.messaging"',
'package="<pkg>"',
"endpoint 127.0.0.1:8080",
"path C:\\Users\\<user>\\work\\x.dex",
"public crackme sg.vantagepoint.uncrackable1",
]) + "\n")
result, doc, token = scan(tmp_path)
assert doc["findings"] == [], doc["findings"]
assert token == "RESULT=clean"
assert result.returncode == 0
def test_exempted_hits_are_kept_and_auditable_with_show_exempt(self, tmp_path):
"""The exemption list must be inspectable, not just silent."""
write(tmp_path, "notes.md", 'package="com.android.internal"\n')
_result, doc, _token = scan(tmp_path, "--show-exempt")
assert doc["findings"] == []
assert doc["exempted"], "an exempted hit must still be recorded"
assert doc["exempted"][0]["exempt_reason"]
def test_a_weak_only_hit_keeps_the_default_gate_green(self, tmp_path):
"""Documented behaviour: an RFC 5737 address is reported but does not fail --fail-on strong.
Note the measured detail: `ip:port` satisfies both endpoint rules, so one
address is reported twice (once as the `ip:port` form, once as the bare
literal). Recorded here rather than smoothed over -- it is a report-volume
question, not a missed or invented finding.
"""
write(tmp_path, "notes.md", "endpoint 192.0.2.10:8080\n")
result, doc, token = scan(tmp_path)
assert {f["strength"] for f in doc["findings"]} == {"weak"}
assert {f["rule"] for f in doc["findings"]} == {"plain_addr", "plain_addr_ipliteral"}
assert token == "RESULT=leaks_found_strong_only"
assert result.returncode == 0
strict, _doc2, strict_token = scan(tmp_path, "--fail-on", "any")
assert strict_token == "RESULT=leaks_found"
assert strict.returncode == 1
# --------------------------------------------------------------------------- #
# the machine-readable contract
# --------------------------------------------------------------------------- #
class TestReportContract:
def test_json_report_has_the_documented_keys(self, tmp_path):
write(tmp_path, "notes.md", 'package="%s"\n' % DECOY_PACKAGE)
_result, doc, _token = scan(tmp_path)
assert set(doc) >= {"root", "files_scanned", "findings", "exempted",
"errors", "result", "fail_on"}
assert doc["result"] == "leaks_found"
assert doc["fail_on"] == "strong"
def test_result_token_is_always_the_last_line(self, tmp_path):
write(tmp_path, "notes.md", 'package="%s"\n' % DECOY_PACKAGE)
result, _doc, token = scan(tmp_path)
assert result.stdout.splitlines()[-1] == token
assert token.startswith("RESULT=")
def test_list_rules_prints_the_table_and_exits_zero(self):
result = run_script("scan_leaks.py", ["--list-rules"])
assert result.returncode == 0, result.output
assert not result.has_traceback
for rule_id in ("bundle_pkg_attr", "device_serial", "token_github_pat",
"appkey_assignment", "user_path_posix"):
assert rule_id in result.stdout
def test_unknown_category_is_a_usage_error(self, tmp_path):
result = run_script("scan_leaks.py",
["--root", str(tmp_path), "--only", "nosuchcategory"])
assert result.returncode == 2, result.output