178 lines
5.9 KiB
Python
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))
|