Files
2026-09-17 00:50:07 +03:00

178 lines
5.9 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import hashlib
import json
import sys
import zipfile
from pathlib import Path
from typing import Any
def read_text(zf: zipfile.ZipFile, name: str) -> str:
return zf.read(name).decode("utf-8")
def read_json(zf: zipfile.ZipFile, name: str) -> Any:
return json.loads(read_text(zf, name))
def read_jsonl(zf: zipfile.ZipFile, name: str) -> list[dict[str, Any]]:
rows = []
for line in read_text(zf, name).splitlines():
line = line.strip()
if line:
rows.append(json.loads(line))
return rows
def derive_key(raw_answers: list[str]) -> bytes:
return hashlib.sha256("|".join(raw_answers).encode("utf-8")).digest()
def xor_stream(key: bytes, length: int) -> bytes:
stream = bytearray()
counter = 0
while len(stream) < length:
stream.extend(hashlib.sha256(key + counter.to_bytes(4, "big")).digest())
counter += 1
return bytes(stream[:length])
def decrypt_closeout(note: bytes, raw_answers: list[str]) -> dict[str, Any]:
wrapped = json.loads(note.decode("utf-8"))
key = derive_key(raw_answers)
nonce = bytes.fromhex(wrapped["nonce"])
ciphertext = bytes.fromhex(wrapped["ciphertext"])
expected_mac = hashlib.sha256(key + nonce + ciphertext).hexdigest()
if wrapped["mac"] != expected_mac:
raise ValueError("closeout note MAC mismatch; one of Q01-Q09 is wrong")
stream = xor_stream(hashlib.sha256(key + nonce).digest(), len(ciphertext))
plaintext = bytes(a ^ b for a, b in zip(ciphertext, stream))
return json.loads(plaintext.decode("utf-8"))
def wrap_answer(value: str) -> str:
if value.startswith("caplag{"):
return value
return f"caplag{{{value}}}"
def recover_raw_answers(zf: zipfile.ZipFile) -> list[str]:
raw: list[str] = []
audit = read_jsonl(zf, "github/audit_log.jsonl")
issue_event = next(
row
for row in audit
if row.get("event") == "issue_comment.created"
and "release-triage" in row.get("labels", [])
and "agent_route" in row
)
raw.append(issue_event["agent_route"]["source_key"])
agent = read_jsonl(zf, "agent/triage_assistant.jsonl")
tool_call = next(
row
for row in agent
if row.get("event") == "tool_call"
and row.get("tool") == "deploy_plan.render"
and row.get("input", {}).get("from_issue") == raw[0]
)
raw.append(tool_call["tool_call_id"])
run = read_json(zf, "github/actions/run_8716443.json")
if run["source"]["tool_call_id"] != raw[1]:
raise ValueError("run source does not match recovered tool call")
raw.append(run["tuple"])
lockfile = read_json(zf, "repo/package-lock.after.json")
suspicious_pkg = None
for package_path, package_info in lockfile["packages"].items():
if not package_path.startswith("node_modules/"):
continue
if package_info.get("hasInstallScript"):
package_name = package_path.removeprefix("node_modules/")
suspicious_pkg = f"{package_name}@{package_info['version']}"
break
if suspicious_pkg is None:
raise ValueError("no install-script package found")
raw.append(suspicious_pkg)
npm_entry = read_json(zf, "npm/_cacache/index-v5/ledger-seal-entry.json")
if f"{npm_entry['package']}@{npm_entry['version']}" != raw[3]:
raise ValueError("npm cache entry does not match lockfile package")
raw.append(npm_entry["cache_path"])
runner_events = read_jsonl(zf, "runner/process_events.jsonl")
run_id = int(raw[2].split("/", 1)[0].removeprefix("run-"))
check_run_id = int(raw[2].split("/", 1)[1].removeprefix("check-"))
runner_event = next(
row
for row in runner_events
if row.get("run_id") == run_id
and row.get("check_run_id") == check_run_id
and raw[4] in row.get("cmd", "")
)
raw.append(runner_event["host"])
oidc_tokens = read_jsonl(zf, "cloud/oidc_tokens.jsonl")
job_id = run["jobs"][0]["job_id"]
oidc = next(
row
for row in oidc_tokens
if row.get("run_id") == run_id
and row.get("check_run_id") == check_run_id
and row.get("job_id") == job_id
)
raw.append(oidc["sub"])
cloudtrail = read_jsonl(zf, "cloud/cloudtrail.jsonl")
put_object = next(
row
for row in cloudtrail
if row.get("eventName") == "PutObject"
and row.get("requestParameters", {}).get("x-amz-meta-run-id") == str(run_id)
and row.get("requestParameters", {}).get("x-amz-meta-check-run-id") == str(check_run_id)
)
raw.append(put_object["responseElements"]["object_locator"])
kube_audit = read_jsonl(zf, "kubernetes/audit.jsonl")
deploy_patch = next(
row
for row in kube_audit
if row.get("verb") == "patch"
and row.get("objectRef", {}).get("resource") == "deployments"
and row.get("objectRef", {}).get("name") == "recon-ledger"
and row.get("requestObject", {})
.get("metadata", {})
.get("annotations", {})
.get("northstar.io/source-object")
== raw[7]
)
image = deploy_patch["requestObject"]["spec"]["template"]["spec"]["containers"][0]["image"]
raw.append(image)
return raw
def solve_bundle(bundle_path: str | Path) -> list[str]:
with zipfile.ZipFile(bundle_path) as zf:
raw = recover_raw_answers(zf)
closeout = decrypt_closeout(zf.read("cloud/final_closeout.note"), raw)
return [wrap_answer(value) for value in raw] + [wrap_answer(closeout["final_flag"])]
def main(argv: list[str]) -> int:
if len(argv) != 2:
print(f"usage: {Path(argv[0]).name} public/runner_ashfall_case.zip", file=sys.stderr)
return 2
answers = solve_bundle(argv[1])
for idx, answer in enumerate(answers, 1):
print(f"Q{idx:02d}: {answer}")
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv))