Init. Commit
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
#!/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))
|
||||
Reference in New Issue
Block a user