mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 01:41:42 +02:00
ci: retain exact staged daemon holder evidence
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
2ebf3795eb
commit
84b3a9ea6b
3 files changed
+224
No files matched your search
@@ -0,0 +1,87 @@
|
||||
"""Read-only Linux evidence for the exact staged Runner executable; never signal."""
|
||||
import argparse
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import stat
|
||||
import time
|
||||
|
||||
|
||||
def identity(path):
|
||||
value = path.stat()
|
||||
return {"dev": str(value.st_dev), "ino": str(value.st_ino),
|
||||
"size": value.st_size, "mode": stat.S_IMODE(value.st_mode)}
|
||||
|
||||
|
||||
def process_identity(path):
|
||||
# comm may contain spaces or parentheses. Do not retain it, argv, or env.
|
||||
value = (path / "stat").read_text()
|
||||
fields = value[value.rindex(")") + 2:].split()
|
||||
return {"pid": int(path.name), "state": fields[0], "ppid": int(fields[1]),
|
||||
"processGroup": int(fields[2]), "startTimeTicks": fields[19]}
|
||||
|
||||
|
||||
def inspect(target, proc=Path("/proc"), max_pids=32768, seconds=8):
|
||||
target = Path(os.path.abspath(target))
|
||||
begin = time.monotonic()
|
||||
result = {"schema": "paperclip.exact-executable-holders/v1",
|
||||
"observedAt": datetime.now(timezone.utc).isoformat(),
|
||||
"target": str(target), "targetBefore": None, "holders": [],
|
||||
"permissionDenied": 0, "racedMatches": 0, "scanned": 0,
|
||||
"complete": True, "signalsSent": 0}
|
||||
try:
|
||||
if target.is_symlink() or not stat.S_ISREG(target.stat().st_mode):
|
||||
raise ValueError("Staged executable is not a regular non-symlink file")
|
||||
result["targetBefore"] = identity(target)
|
||||
except FileNotFoundError:
|
||||
pass # The first typecheck has not staged the daemon yet.
|
||||
for entry in proc.iterdir():
|
||||
if not entry.name.isdecimal():
|
||||
continue
|
||||
if result["scanned"] >= max_pids or time.monotonic() - begin >= seconds:
|
||||
result.update(complete=False, stopReason="bounded_scan_limit")
|
||||
break
|
||||
result["scanned"] += 1
|
||||
matched = False
|
||||
try:
|
||||
exe = entry / "exe"
|
||||
link = os.readlink(exe)
|
||||
executable = identity(exe)
|
||||
pinned = result["targetBefore"]
|
||||
by_inode = bool(pinned and all(executable[k] == pinned[k] for k in ("dev", "ino")))
|
||||
by_path = link in (str(target), str(target) + " (deleted)")
|
||||
if not (by_inode or by_path):
|
||||
continue
|
||||
matched = True
|
||||
before = process_identity(entry)
|
||||
after = process_identity(entry)
|
||||
if before != after or identity(exe) != executable or os.readlink(exe) != link:
|
||||
result["racedMatches"] += 1
|
||||
continue
|
||||
result["holders"].append({**before, "executable": executable,
|
||||
"matchesTargetInode": by_inode,
|
||||
"matchesTargetPath": by_path,
|
||||
"deletedExecutable": link.endswith(" (deleted)")})
|
||||
except FileNotFoundError:
|
||||
if matched:
|
||||
result["racedMatches"] += 1
|
||||
except PermissionError:
|
||||
result["permissionDenied"] += 1
|
||||
try:
|
||||
result["targetAfter"] = identity(target)
|
||||
except FileNotFoundError:
|
||||
result["targetAfter"] = None
|
||||
result["targetChangedDuringScan"] = result["targetBefore"] != result["targetAfter"]
|
||||
result["complete"] = (result["complete"] and not result["permissionDenied"]
|
||||
and not result["racedMatches"] and not result["targetChangedDuringScan"])
|
||||
result["holders"].sort(key=lambda row: row["pid"])
|
||||
result["elapsedSeconds"] = round(time.monotonic() - begin, 6)
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--target", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
print(json.dumps(inspect(args.target), sort_keys=True))
|
||||
Reference in new issue
Block a user