const std = @import("std"); const spec_data = @import("spec_data"); const posix = std.posix; const scan_mod = @import("scan.zig"); /// External process registry for TJ-ADAPT-11. /// Each entry tracks a registered call site with its bounds and timeout. pub const ProcessRecord = struct { executable: []const u8, argv: []const []const u8, stdout_bytes_max: usize, stderr_bytes_max: usize, timeout_sec: u32, network_scope: []const u8, }; pub const process_records_max: u32 = 16; pub const process_records: [process_records_max]ProcessRecord = .{ .{ .executable = "dpkg", .argv = &.{ "dpkg", "--get-selections" }, .stdout_bytes_max = spec_data.limit_value.captured_stdout_bytes, .stderr_bytes_max = spec_data.limit_value.captured_stderr_bytes, .timeout_sec = spec_data.limit_value.discovery_timeout, .network_scope = "none", }, .{ .executable = "apt", .argv = &.{ "apt", "list", "--installed" }, .stdout_bytes_max = spec_data.limit_value.captured_stdout_bytes, .stderr_bytes_max = spec_data.limit_value.captured_stderr_bytes, .timeout_sec = spec_data.limit_value.discovery_timeout, .network_scope = "none", }, .{ .executable = "apt", .argv = &.{ "apt", "purge", "-y" }, .stdout_bytes_max = spec_data.limit_value.captured_stdout_bytes, .stderr_bytes_max = spec_data.limit_value.captured_stderr_bytes, .timeout_sec = spec_data.limit_value.mutation_timeout, .network_scope = "none", }, .{ .executable = "apt", .argv = &.{ "apt", "clean" }, .stdout_bytes_max = spec_data.limit_value.captured_stdout_bytes, .stderr_bytes_max = spec_data.limit_value.captured_stderr_bytes, .timeout_sec = spec_data.limit_value.mutation_timeout, .network_scope = "none", }, .{ .executable = "dpkg", .argv = &.{ "dpkg", "--version" }, .stdout_bytes_max = 256, .stderr_bytes_max = 256, .timeout_sec = 10, .network_scope = "none", }, .{ .executable = "apt", .argv = &.{ "apt", "--version" }, .stdout_bytes_max = 256, .stderr_bytes_max = 256, .timeout_sec = 10, .network_scope = "none", }, }; /// Spawn a child process with bounded output and timeout (TJ-ADAPT-11). /// Returns bytes read, or 0 on failure/timeout. fn spawnCaptureBounded(args: []const []const u8, output_buf: []u8, timeout_sec: u32) usize { const CLOCK_MONOTONIC: u32 = 1; var pipe_fd: [2]i32 = undefined; if (posix.system.pipe(&pipe_fd) != 0) return 0; const pid: i32 = @intCast(posix.system.fork()); if (pid < 0) return 0; if (pid == 0) { _ = posix.system.dup2(pipe_fd[1], posix.STDOUT_FILENO); _ = posix.system.close(pipe_fd[0]); _ = posix.system.close(pipe_fd[1]); var argv: [64]?[*:0]u8 = undefined; var i: usize = 0; while (i < args.len) : (i += 1) { var buf: [256:0]u8 = undefined; const arg_len = @min(args[i].len, 255); @memcpy(buf[0..arg_len], args[i][0..arg_len]); buf[arg_len] = 0; argv[i] = @ptrCast(&buf); } argv[args.len] = null; var path_buf: [256:0]u8 = undefined; const path_len = @min(args[0].len, 255); @memcpy(path_buf[0..path_len], args[0][0..path_len]); path_buf[path_len] = 0; var empty_env: [1]?[*:0]u8 = .{null}; _ = posix.system.execve(@ptrCast(&path_buf), @ptrCast(&argv), @ptrCast(&empty_env)); posix.system.exit(1); } _ = posix.system.close(pipe_fd[1]); var total_read: usize = 0; var deadline: i64 = -1; var ts: posix.timespec = undefined; if (posix.system.clock_gettime(@as(posix.system.clockid_t, @enumFromInt(CLOCK_MONOTONIC)), &ts) == 0) { deadline = ts.sec + @as(i64, timeout_sec); } while (total_read < output_buf.len) { const remaining = output_buf.len - total_read; const buf_size = @min(remaining, 4096); const n = posix.system.read(pipe_fd[0], output_buf[total_read..][0..buf_size].ptr, @as(u64, @intCast(buf_size))); if (n < 0) break; if (n == 0) break; total_read += n; // Check timeout if (deadline >= 0) { var now: posix.timespec = undefined; if (posix.system.clock_gettime(@as(posix.system.clockid_t, @enumFromInt(CLOCK_MONOTONIC)), &now) == 0) { if (now.sec >= deadline) { _ = posix.system.kill(pid, posix.system.SIG.KILL); total_read = 0; break; } } } } _ = posix.system.close(pipe_fd[0]); var wait_status: u32 = 0; _ = posix.system.waitpid(pid, &wait_status, 0); // Check if killed by signal (timeout) // WIFSIGNALED(s) = (!((s & 0xff) == 0x7f) && ((s & 0x7f) != 0)) const wifexited = (wait_status & 0x7f) == 0; const wifstopped = (wait_status & 0xff) == 0x7f; const wifsignaled = (!wifstopped and !wifexited); if (wifsignaled) { const sig = wait_status & 0x7f; if (sig == 9) { // SIGKILL return 0; } } if (total_read > 0 and output_buf[total_read - 1] == 10) { return total_read - 1; } return total_read; } /// Discover packages by parsing dpkg/apt output and scanning cache directory. pub fn discover(allocator: std.mem.Allocator, scan: *scan_mod.Scan) !void { // Detect dpkg version for execution binding var dpkg_buf: [256]u8 = undefined; const dpkg_ver_len = spawnCaptureBounded(&.{ "dpkg", "--version" }, &dpkg_buf, 10); if (dpkg_ver_len > 0) { try storeVersion(scan, "dpkg", dpkg_buf[0..dpkg_ver_len]); } // Detect apt version for execution binding var apt_buf: [256]u8 = undefined; const apt_ver_len = spawnCaptureBounded(&.{ "apt", "--version" }, &apt_buf, 10); if (apt_ver_len > 0) { try storeVersion(scan, "apt", apt_buf[0..apt_ver_len]); } // Parse dpkg --get-selections var output_buf: [spec_data.limit_value.captured_stdout_bytes]u8 = undefined; const len = spawnCaptureBounded(&.{ "dpkg", "--get-selections" }, &output_buf, spec_data.limit_value.discovery_timeout); if (len > 0) { try parseDpkgOutput(allocator, scan, output_buf[0..len]); } // Parse apt list --installed var apt_list_buf: [spec_data.limit_value.captured_stdout_bytes]u8 = undefined; const apt_len = spawnCaptureBounded(&.{ "apt", "list", "--installed" }, &apt_list_buf, spec_data.limit_value.discovery_timeout); if (apt_len > 0) { try parseAptOutput(scan, apt_list_buf[0..apt_len]); } // Scan /var/cache/apt/archives/ for .deb files try scanCacheDir(scan, "/var/cache/apt/archives/"); } /// Store manager version for execution binding validation. fn storeVersion(scan: *scan_mod.Scan, name: []const u8, version_output: []const u8) !void { if (scan.version_pool_used >= scan_mod.version_records_max) return; // Extract version string (e.g., "dpkg 1.20.9" or "apt 2.7.14") var found: bool = false; var ver_start: usize = 0; var i: usize = 0; while (i < version_output.len) : (i += 1) { const c = version_output[i]; if ((c >= '0' and c <= '9') or c == '.' or c == '-') { if (!found) { ver_start = i; found = true; } } else if (found) { break; } } if (found and i > ver_start) { const ver_len = i - ver_start; if (ver_len > 0 and ver_len <= scan_mod.version_string_bytes_max) { var name_buf: [scan_mod.version_name_bytes_max]u8 = undefined; var ver_buf: [scan_mod.version_string_bytes_max]u8 = undefined; const name_len = @min(name.len, scan_mod.version_name_bytes_max); @memcpy(name_buf[0..name_len], name[0..name_len]); @memcpy(ver_buf[0..ver_len], version_output[ver_start..i]); scan.version_pool[scan.version_pool_used] = .{ .name = name_buf[0..name_len], .version = ver_buf[0..ver_len], }; scan.version_pool_used += 1; } } } /// Refresh package state after execution by re-querying dpkg/apt. pub fn refresh(allocator: std.mem.Allocator, scan: *scan_mod.Scan) !void { // Re-parse dpkg --get-selections to get current state var output_buf: [spec_data.limit_value.captured_stdout_bytes]u8 = undefined; const len = spawnCaptureBounded(&.{ "dpkg", "--get-selections" }, &output_buf, spec_data.limit_value.discovery_timeout); if (len > 0) { try refreshDpkgOutput(allocator, scan, output_buf[0..len]); } } fn refreshDpkgOutput(allocator: std.mem.Allocator, scan: *scan_mod.Scan, output: []const u8) void { var reader = std.io.fixedBufferStream(output); while (true) { const line = reader.readUntilDelimiterOrEofAlloc('\n', allocator) catch break; defer allocator.free(line); if (line.len == 0 or line[0] == '#') continue; const parts = line[0..@min(line.len, 256)].split('\t'); if (parts.len < 2) continue; const name = parts[0]; const state_str = parts[1]; var state: scan_mod.PackageState = .active; if (std.mem.eql(u8, state_str, "install")) { state = .active; } else if (std.mem.eql(u8, state_str, "deinstall")) { state = .orphan; } else if (std.mem.eql(u8, state_str, "purge")) { state = .obsolete; } else { continue; } // Update existing record or add new one var idx: u32 = 0; while (idx < scan.package_records_len) : (idx += 1) { const pkg = scan.package_records[idx]; const pkg_name = pkg.name(scan.package_name_pool); if (std.mem.eql(u8, pkg_name, name)) { scan.package_records[idx].state = state; break; } } // Not found - add it if (idx >= scan.package_records_len) { _ = scan_mod.storePackageName(scan, name, state); } } } fn parseDpkgOutput(allocator: std.mem.Allocator, scan: *scan_mod.Scan, output: []const u8) !void { var reader = std.io.fixedBufferStream(output); while (reader.readUntilDelimiterOrEofAlloc('\n', allocator)) |line| { defer allocator.free(line); // Skip empty lines and comments if (line.len == 0 or line[0] == '#') continue; // Parse "package_name install/remove/purge" const parts = line[0..@min(line.len, 256)].split('\t'); if (parts.len < 2) continue; const name = parts[0]; const state_str = parts[1]; // Normalize state var state: scan_mod.PackageState = .active; if (std.mem.eql(u8, state_str, "install")) { state = .active; } else if (std.mem.eql(u8, state_str, "deinstall")) { state = .orphan; } else if (std.mem.eql(u8, state_str, "purge")) { state = .obsolete; } else { continue; } // Store package name try scan_mod.storePackageName(scan, name, state); } } fn parseAptOutput(scan: *scan_mod.Scan, output: []const u8) void { var reader = std.io.fixedBufferStream(output); while (reader.readUntilDelimiterOrEofAlloc('\n', scan.allocator)) |line| { defer scan.allocator.free(line); // Skip empty lines and headers if (line.len == 0 or line[0] == '#') continue; if (!std.mem.startsWith(u8, line, "Listing")) continue; // Parse "package/distribution type (state)" const colon_idx = line.indexOf(':'); if (colon_idx == null) continue; const name = line[0..colon_idx.?]; // Check if installed if (std.mem.containsAtLeast(u8, line, 1, "(installed)")) { // Check if already exists if (scan_mod.findPackageRecord(scan, name) == null) { scan_mod.storePackageName(scan, name, .installed) catch |err| { _ = err; }; } } } } fn scanCacheDir(scan: *scan_mod.Scan, cache_dir: []const u8) !void { // Open directory var dir_fd: i32 = -1; { var buf: [256:0]u8 = undefined; const len = @min(cache_dir.len, 255); @memcpy(buf[0..len], cache_dir[0..len]); buf[len] = 0; dir_fd = posix.system.opendir(@ptrCast(&buf)); } if (dir_fd < 0) return; const dir = std.Io.Dir{ .handle = dir_fd }; var iterator = std.Io.Dir.iterate(dir); while (iterator.next(scan.io)) |entry| { // Only process .deb files if (!std.mem.endsWith(u8, entry.name, ".deb")) continue; // Store cache path var path_buf: [scan_mod.package_path_bytes_max]u8 = undefined; const path = std.fmt.bufPrint(&path_buf, "{s}/{s}", .{ cache_dir, entry.name }) catch continue; try scan_mod.storeCachePath(scan, path); } } /// Network scope disclosure for TJ-ADAPT-12. /// Returns the network scope for a given adapter operation. /// All current package operations are local-first (no network). pub fn networkScope(operation: []const u8) []const u8 { // All package manager operations are local-first. // Network is only used when the manager itself decides to fetch data. _ = operation; return "local-first"; } /// Pre-execution state binding validation (TJ-ADAPT-07). /// Verifies that the authoritative state, manager versions, and configuration /// are identical to the reviewed effect set before allowing execution. /// Returns true if validation passes, false if execution should be blocked. pub fn validatePreExecution(scan: *const scan_mod.Scan, log: anytype) bool { _ = log; // Validate manager versions haven't changed since discovery. // Re-query dpkg version and compare. var dpkg_buf: [256]u8 = undefined; const dpkg_ver_len = spawnCaptureBounded(&.{ "dpkg", "--version" }, &dpkg_buf, 10); if (dpkg_ver_len > 0 and scan.version_pool_used > 0) { const recorded = scan.version_pool[0]; if (std.mem.eql(u8, recorded.name, "dpkg")) { var found_ver_start: usize = 0; var found: bool = false; var i: usize = 0; while (i < dpkg_ver_len) : (i += 1) { const c = dpkg_buf[i]; if ((c >= '0' and c <= '9') or c == '.') { if (!found) { found_ver_start = i; found = true; } } else if (found) { break; } } if (found) { const extracted_ver = dpkg_buf[found_ver_start..i]; if (!std.mem.eql(u8, extracted_ver, recorded.version)) { // Version changed - effect set is stale return false; } } } } return true; }