Repositories / agent-snapshot.git
agent-snapshot.git
Clone (read-only): git clone http://git.guha-anderson.com/git/agent-snapshot.git
@@ -171,7 +171,7 @@ bool owned_by_other_and_not_writable(const fs::path& path) { struct stat st {}; if (lstat(path.c_str(), &st) != 0) return false; if (st.st_uid == tracer_uid) return false; - if (st.st_mode & (S_IWUSR | S_IWGRP | S_IWOTH)) return false; + if (access(path.c_str(), W_OK) == 0) return false; return true; } @@ -369,6 +369,7 @@ void handle_syscall_entry(pid_t pid, ProcState& proc, const user_regs_struct& re case SYS_open: p.path_a = resolve_path(proc, AT_FDCWD, read_tracee_string(pid, p.args[0])).string(); p.flags = static_cast<int>(p.args[1]); + if (is_write_open(p.flags)) record_observation(p.path_a, "write"); break; case SYS_openat: #ifdef SYS_openat2 @@ -377,10 +378,12 @@ void handle_syscall_entry(pid_t pid, ProcState& proc, const user_regs_struct& re p.dirfd = static_cast<int>(p.args[0]); p.path_a = resolve_path(proc, p.dirfd, read_tracee_string(pid, p.args[1])).string(); p.flags = static_cast<int>(p.args[2]); + if (is_write_open(p.flags)) record_observation(p.path_a, "write"); break; case SYS_creat: p.path_a = resolve_path(proc, AT_FDCWD, read_tracee_string(pid, p.args[0])).string(); p.flags = O_CREAT | O_WRONLY | O_TRUNC; + record_observation(p.path_a, "write"); break; case SYS_stat: case SYS_lstat: @@ -400,11 +403,13 @@ void handle_syscall_entry(pid_t pid, ProcState& proc, const user_regs_struct& re case SYS_unlink: case SYS_rmdir: p.path_a = resolve_path(proc, AT_FDCWD, read_tracee_string(pid, p.args[0])).string(); + record_observation(p.path_a, "delete"); break; case SYS_unlinkat: case SYS_mkdirat: p.dirfd = static_cast<int>(p.args[0]); p.path_a = resolve_path(proc, p.dirfd, read_tracee_string(pid, p.args[1])).string(); + if (p.nr == SYS_unlinkat) record_observation(p.path_a, "delete"); break; case SYS_mkdir: case SYS_chdir: @@ -414,6 +419,8 @@ void handle_syscall_entry(pid_t pid, ProcState& proc, const user_regs_struct& re case SYS_rename: p.path_a = resolve_path(proc, AT_FDCWD, read_tracee_string(pid, p.args[0])).string(); p.path_b = resolve_path(proc, AT_FDCWD, read_tracee_string(pid, p.args[1])).string(); + record_observation(p.path_a, "delete"); + record_observation(p.path_b, "write"); break; case SYS_renameat: #ifdef SYS_renameat2 @@ -421,6 +428,8 @@ void handle_syscall_entry(pid_t pid, ProcState& proc, const user_regs_struct& re #endif p.path_a = resolve_path(proc, static_cast<int>(p.args[0]), read_tracee_string(pid, p.args[1])).string(); p.path_b = resolve_path(proc, static_cast<int>(p.args[2]), read_tracee_string(pid, p.args[3])).string(); + record_observation(p.path_a, "delete"); + record_observation(p.path_b, "write"); break; case SYS_getdents: case SYS_getdents64: @@ -656,8 +665,15 @@ void restore_snapshot(const fs::path& dir) { if (!after.contains("blob")) continue; fs::create_directories(path.parent_path()); - fs::copy_file(dir / "blobs" / after.at("blob").get<std::string>(), path, - fs::copy_options::overwrite_existing); + const std::string expected_blob = after.at("blob").get<std::string>(); + if (!(fs::exists(path) && fs::is_regular_file(path) && + fnv1a_file_digest(path) == expected_blob)) { + fs::path tmp = path; + tmp += ".agent-snapshot.tmp"; + fs::copy_file(dir / "blobs" / expected_blob, tmp, + fs::copy_options::overwrite_existing); + fs::rename(tmp, path); + } if (after.contains("mode")) { fs::permissions(path, static_cast<fs::perms>(after.at("mode").get<unsigned>() & 07777), fs::perm_options::replace);
@@ -11,6 +11,7 @@ ROOT = Path(__file__).resolve().parents[1] BUILD = ROOT / "build" / "pytest" BIN = BUILD / "agent-snapshot" TESTDATA = ROOT / "testdata" +PYTHON = "/usr/bin/python3" def run(cmd, **kwargs): @@ -56,7 +57,7 @@ def capture(tmp_path: Path, *command: str) -> Snapshot: def test_clean_git_tracked_read_records_repo_without_blob(tmp_path): - snap = capture(tmp_path, "python3", "test_programs/read_clean.py") + snap = capture(tmp_path, PYTHON, "test_programs/read_clean.py") clean = snap.file(TESTDATA / "clean.txt") assert "read" in clean["operations"] @@ -72,7 +73,7 @@ def test_dirty_untracked_created_and_deleted_files_are_captured(tmp_path): (TESTDATA / "untracked_runtime.txt").write_text("untracked input\n") (TESTDATA / "deleted_by_program.txt").write_text("delete me\n") - snap = capture(tmp_path, "python3", "test_programs/dirty_untracked_write.py") + snap = capture(tmp_path, PYTHON, "test_programs/dirty_untracked_write.py") dirty = snap.file(TESTDATA / "dirty.txt") assert dirty["git"]["tracked"] is True @@ -95,7 +96,7 @@ def test_dirty_untracked_created_and_deleted_files_are_captured(tmp_path): def test_fork_usr_and_directory_traversal(tmp_path): - snap = capture(tmp_path, "python3", "test_programs/fork_and_usr.py") + snap = capture(tmp_path, PYTHON, "test_programs/fork_and_usr.py") child = snap.file(TESTDATA / "child_output.txt") assert "write" in child["operations"] @@ -112,8 +113,9 @@ def test_fork_usr_and_directory_traversal(tmp_path): def test_restore_applies_final_state(tmp_path): (TESTDATA / "dirty.txt").write_text("changed before capture\n") + (TESTDATA / "untracked_runtime.txt").write_text("untracked input\n") (TESTDATA / "deleted_by_program.txt").write_text("delete me\n") - snap = capture(tmp_path, "python3", "test_programs/dirty_untracked_write.py") + snap = capture(tmp_path, PYTHON, "test_programs/dirty_untracked_write.py") shutil.rmtree(TESTDATA) TESTDATA.mkdir()
@@ -0,0 +1,79 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "agent-snapshot" +version = "0.1.0" +source = { virtual = "." } + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=8.0" }] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +]