ctfy.sdk.admin_resources.records
client.admin.records — archived instance forensics (admin).
1"""``client.admin.records`` — archived instance forensics (admin).""" 2 3from __future__ import annotations 4 5import builtins 6from typing import Any 7 8from ctfy.sdk._helpers import _extract_items, _raise_for_status 9from ctfy.sdk.base import BaseHttpClient 10from ctfy.server.models import InstanceRecordDetail, InstanceRecordInfo 11 12 13class AdminRecordsResource: 14 """Post-mortem forensics over terminal (archived) instance records.""" 15 16 def __init__(self, http: BaseHttpClient) -> None: 17 self._http = http 18 19 def list( 20 self, 21 *, 22 team_id: str = "", 23 challenge_id: str = "", 24 node_id: str = "", 25 status: str = "", 26 since_ts: float | None = None, 27 offset: int = 0, 28 limit: int = 50, 29 ) -> builtins.list[InstanceRecordInfo]: 30 """Archived (terminal) instance records — the post-mortem 31 forensics index. ``since_ts`` is a UNIX epoch lower bound.""" 32 params: dict[str, Any] = {"offset": offset, "limit": limit} 33 if team_id: 34 params["team_id"] = team_id 35 if challenge_id: 36 params["challenge_id"] = challenge_id 37 if node_id: 38 params["node_id"] = node_id 39 if status: 40 params["status"] = status 41 if since_ts is not None: 42 params["since_ts"] = since_ts 43 resp = self._http.request("GET", "/admin/instance-records", params=params) 44 _raise_for_status(resp) 45 return _extract_items(resp.json(), InstanceRecordInfo) 46 47 def get(self, instance_id: str) -> InstanceRecordDetail: 48 """Full archived record for one instance (lifecycle, node, 49 artifact availability flags).""" 50 resp = self._http.request("GET", f"/admin/instance-records/{instance_id}") 51 _raise_for_status(resp) 52 return InstanceRecordDetail.model_validate(resp.json()) 53 54 def container_log(self, instance_id: str) -> str: 55 """Combined container stdout/stderr captured at teardown. 56 Returns ``""`` when archival was disabled or nothing was kept.""" 57 resp = self._http.request("GET", f"/admin/instance-records/{instance_id}/container-log") 58 _raise_for_status(resp) 59 return str(resp.json().get("logs") or "") 60 61 def events( 62 self, instance_id: str, offset: int = 0, limit: int = 50 63 ) -> builtins.list[dict[str, Any]]: 64 """Lifecycle event rows for one archived instance.""" 65 resp = self._http.request( 66 "GET", 67 f"/admin/instance-records/{instance_id}/events", 68 params={"offset": offset, "limit": limit}, 69 ) 70 _raise_for_status(resp) 71 items: builtins.list[dict[str, Any]] = resp.json()["items"] 72 return items 73 74 def submissions( 75 self, instance_id: str, offset: int = 0, limit: int = 50 76 ) -> builtins.list[dict[str, Any]]: 77 """Submission rows recorded against one archived instance.""" 78 resp = self._http.request( 79 "GET", 80 f"/admin/instance-records/{instance_id}/submissions", 81 params={"offset": offset, "limit": limit}, 82 ) 83 _raise_for_status(resp) 84 items: builtins.list[dict[str, Any]] = resp.json()["items"] 85 return items 86 87 def traffic( 88 self, instance_id: str, offset: int = 0, limit: int = 50 89 ) -> builtins.list[dict[str, Any]]: 90 """Persisted mitmproxy flows for one archived instance.""" 91 resp = self._http.request( 92 "GET", 93 f"/admin/instance-records/{instance_id}/traffic", 94 params={"offset": offset, "limit": limit}, 95 ) 96 _raise_for_status(resp) 97 items: builtins.list[dict[str, Any]] = resp.json()["items"] 98 return items 99 100 def pcap(self, instance_id: str) -> bytes: 101 """Archived tcpdump capture for one instance. Returns empty 102 ``bytes`` when no capture is on file.""" 103 resp = self._http.request("GET", f"/admin/instance-records/{instance_id}/pcap") 104 if resp.status_code == 404: 105 return b"" 106 _raise_for_status(resp) 107 return resp.content
class
AdminRecordsResource:
14class AdminRecordsResource: 15 """Post-mortem forensics over terminal (archived) instance records.""" 16 17 def __init__(self, http: BaseHttpClient) -> None: 18 self._http = http 19 20 def list( 21 self, 22 *, 23 team_id: str = "", 24 challenge_id: str = "", 25 node_id: str = "", 26 status: str = "", 27 since_ts: float | None = None, 28 offset: int = 0, 29 limit: int = 50, 30 ) -> builtins.list[InstanceRecordInfo]: 31 """Archived (terminal) instance records — the post-mortem 32 forensics index. ``since_ts`` is a UNIX epoch lower bound.""" 33 params: dict[str, Any] = {"offset": offset, "limit": limit} 34 if team_id: 35 params["team_id"] = team_id 36 if challenge_id: 37 params["challenge_id"] = challenge_id 38 if node_id: 39 params["node_id"] = node_id 40 if status: 41 params["status"] = status 42 if since_ts is not None: 43 params["since_ts"] = since_ts 44 resp = self._http.request("GET", "/admin/instance-records", params=params) 45 _raise_for_status(resp) 46 return _extract_items(resp.json(), InstanceRecordInfo) 47 48 def get(self, instance_id: str) -> InstanceRecordDetail: 49 """Full archived record for one instance (lifecycle, node, 50 artifact availability flags).""" 51 resp = self._http.request("GET", f"/admin/instance-records/{instance_id}") 52 _raise_for_status(resp) 53 return InstanceRecordDetail.model_validate(resp.json()) 54 55 def container_log(self, instance_id: str) -> str: 56 """Combined container stdout/stderr captured at teardown. 57 Returns ``""`` when archival was disabled or nothing was kept.""" 58 resp = self._http.request("GET", f"/admin/instance-records/{instance_id}/container-log") 59 _raise_for_status(resp) 60 return str(resp.json().get("logs") or "") 61 62 def events( 63 self, instance_id: str, offset: int = 0, limit: int = 50 64 ) -> builtins.list[dict[str, Any]]: 65 """Lifecycle event rows for one archived instance.""" 66 resp = self._http.request( 67 "GET", 68 f"/admin/instance-records/{instance_id}/events", 69 params={"offset": offset, "limit": limit}, 70 ) 71 _raise_for_status(resp) 72 items: builtins.list[dict[str, Any]] = resp.json()["items"] 73 return items 74 75 def submissions( 76 self, instance_id: str, offset: int = 0, limit: int = 50 77 ) -> builtins.list[dict[str, Any]]: 78 """Submission rows recorded against one archived instance.""" 79 resp = self._http.request( 80 "GET", 81 f"/admin/instance-records/{instance_id}/submissions", 82 params={"offset": offset, "limit": limit}, 83 ) 84 _raise_for_status(resp) 85 items: builtins.list[dict[str, Any]] = resp.json()["items"] 86 return items 87 88 def traffic( 89 self, instance_id: str, offset: int = 0, limit: int = 50 90 ) -> builtins.list[dict[str, Any]]: 91 """Persisted mitmproxy flows for one archived instance.""" 92 resp = self._http.request( 93 "GET", 94 f"/admin/instance-records/{instance_id}/traffic", 95 params={"offset": offset, "limit": limit}, 96 ) 97 _raise_for_status(resp) 98 items: builtins.list[dict[str, Any]] = resp.json()["items"] 99 return items 100 101 def pcap(self, instance_id: str) -> bytes: 102 """Archived tcpdump capture for one instance. Returns empty 103 ``bytes`` when no capture is on file.""" 104 resp = self._http.request("GET", f"/admin/instance-records/{instance_id}/pcap") 105 if resp.status_code == 404: 106 return b"" 107 _raise_for_status(resp) 108 return resp.content
Post-mortem forensics over terminal (archived) instance records.
def
list( self, *, team_id: str = '', challenge_id: str = '', node_id: str = '', status: str = '', since_ts: float | None = None, offset: int = 0, limit: int = 50) -> list[ctfy.server.models.InstanceRecordInfo]:
20 def list( 21 self, 22 *, 23 team_id: str = "", 24 challenge_id: str = "", 25 node_id: str = "", 26 status: str = "", 27 since_ts: float | None = None, 28 offset: int = 0, 29 limit: int = 50, 30 ) -> builtins.list[InstanceRecordInfo]: 31 """Archived (terminal) instance records — the post-mortem 32 forensics index. ``since_ts`` is a UNIX epoch lower bound.""" 33 params: dict[str, Any] = {"offset": offset, "limit": limit} 34 if team_id: 35 params["team_id"] = team_id 36 if challenge_id: 37 params["challenge_id"] = challenge_id 38 if node_id: 39 params["node_id"] = node_id 40 if status: 41 params["status"] = status 42 if since_ts is not None: 43 params["since_ts"] = since_ts 44 resp = self._http.request("GET", "/admin/instance-records", params=params) 45 _raise_for_status(resp) 46 return _extract_items(resp.json(), InstanceRecordInfo)
Archived (terminal) instance records — the post-mortem
forensics index. since_ts is a UNIX epoch lower bound.
48 def get(self, instance_id: str) -> InstanceRecordDetail: 49 """Full archived record for one instance (lifecycle, node, 50 artifact availability flags).""" 51 resp = self._http.request("GET", f"/admin/instance-records/{instance_id}") 52 _raise_for_status(resp) 53 return InstanceRecordDetail.model_validate(resp.json())
Full archived record for one instance (lifecycle, node, artifact availability flags).
def
container_log(self, instance_id: str) -> str:
55 def container_log(self, instance_id: str) -> str: 56 """Combined container stdout/stderr captured at teardown. 57 Returns ``""`` when archival was disabled or nothing was kept.""" 58 resp = self._http.request("GET", f"/admin/instance-records/{instance_id}/container-log") 59 _raise_for_status(resp) 60 return str(resp.json().get("logs") or "")
Combined container stdout/stderr captured at teardown.
Returns "" when archival was disabled or nothing was kept.
def
events( self, instance_id: str, offset: int = 0, limit: int = 50) -> list[dict[str, typing.Any]]:
62 def events( 63 self, instance_id: str, offset: int = 0, limit: int = 50 64 ) -> builtins.list[dict[str, Any]]: 65 """Lifecycle event rows for one archived instance.""" 66 resp = self._http.request( 67 "GET", 68 f"/admin/instance-records/{instance_id}/events", 69 params={"offset": offset, "limit": limit}, 70 ) 71 _raise_for_status(resp) 72 items: builtins.list[dict[str, Any]] = resp.json()["items"] 73 return items
Lifecycle event rows for one archived instance.
def
submissions( self, instance_id: str, offset: int = 0, limit: int = 50) -> list[dict[str, typing.Any]]:
75 def submissions( 76 self, instance_id: str, offset: int = 0, limit: int = 50 77 ) -> builtins.list[dict[str, Any]]: 78 """Submission rows recorded against one archived instance.""" 79 resp = self._http.request( 80 "GET", 81 f"/admin/instance-records/{instance_id}/submissions", 82 params={"offset": offset, "limit": limit}, 83 ) 84 _raise_for_status(resp) 85 items: builtins.list[dict[str, Any]] = resp.json()["items"] 86 return items
Submission rows recorded against one archived instance.
def
traffic( self, instance_id: str, offset: int = 0, limit: int = 50) -> list[dict[str, typing.Any]]:
88 def traffic( 89 self, instance_id: str, offset: int = 0, limit: int = 50 90 ) -> builtins.list[dict[str, Any]]: 91 """Persisted mitmproxy flows for one archived instance.""" 92 resp = self._http.request( 93 "GET", 94 f"/admin/instance-records/{instance_id}/traffic", 95 params={"offset": offset, "limit": limit}, 96 ) 97 _raise_for_status(resp) 98 items: builtins.list[dict[str, Any]] = resp.json()["items"] 99 return items
Persisted mitmproxy flows for one archived instance.
def
pcap(self, instance_id: str) -> bytes:
101 def pcap(self, instance_id: str) -> bytes: 102 """Archived tcpdump capture for one instance. Returns empty 103 ``bytes`` when no capture is on file.""" 104 resp = self._http.request("GET", f"/admin/instance-records/{instance_id}/pcap") 105 if resp.status_code == 404: 106 return b"" 107 _raise_for_status(resp) 108 return resp.content
Archived tcpdump capture for one instance. Returns empty
bytes when no capture is on file.