From e47b7613a37770c817e56e56efe97085e64d83cd Mon Sep 17 00:00:00 2001 From: James Stracey Date: Tue, 3 Feb 2026 13:48:22 +1100 Subject: [PATCH 1/2] Add Santa integration: SantaCollector and basic tests --- artifacts/SantaModule.swift | 81 ++++++++++++++++++++++++++++++++ tests/aftermath/SantaTests.swift | 11 +++++ 2 files changed, 92 insertions(+) create mode 100644 artifacts/SantaModule.swift create mode 100644 tests/aftermath/SantaTests.swift diff --git a/artifacts/SantaModule.swift b/artifacts/SantaModule.swift new file mode 100644 index 0000000..22a5a03 --- /dev/null +++ b/artifacts/SantaModule.swift @@ -0,0 +1,81 @@ +import Foundation + +public struct SantaStatus { + public let version: String? + public let status: String? + public let exportedConfigPath: String? + public let logFiles: [String] + + public init(version: String?, status: String?, exportedConfigPath: String?, logFiles: [String]) { + self.version = version + self.status = status + self.exportedConfigPath = exportedConfigPath + self.logFiles = logFiles + } +} + +public enum SantaError: Error { + case executionFailed(String) +} + +public final class SantaCollector { + static func runCommand(_ arguments: [String]) -> (output: String?, exitCode: Int32) { + let process = Process() + process.launchPath = "/usr/bin/env" + process.arguments = arguments + + let outPipe = Pipe() + process.standardOutput = outPipe + process.standardError = outPipe + + do { + try process.run() + } catch { + return ("", -1) + } + + process.waitUntilExit() + let data = outPipe.fileHandleForReading.readDataToEndOfFile() + let output = String(data: data, encoding: .utf8) + return (output?.trimmingCharacters(in: .whitespacesAndNewlines), process.terminationStatus) + } + + public static func version() -> String? { + let res = runCommand(["santactl", "version"]) // PATH lookup via /usr/bin/env; returns output if available + return res.output + } + + public static func status() -> String? { + let res = runCommand(["santactl", "status"]) + return res.output + } + + @discardableResult + public static func exportConfiguration(to path: String) -> Bool { + let res = runCommand(["santactl", "rule", "--export", path]) + return res.exitCode == 0 + } + + public static func listLogFiles(atPath path: String = "/var/db/santa") -> [String] { + let fm = FileManager.default + do { + let items = try fm.contentsOfDirectory(atPath: path) + return items.map { (path as NSString).appendingPathComponent($0) } + } catch { + return [] + } + } + + public static func collect(exportConfigTo configPath: String? = nil) -> SantaStatus { + let ver = version() + let stat = status() + var exportedPath: String? = nil + if let p = configPath { + if exportConfiguration(to: p) { + exportedPath = p + } + } + let logs = listLogFiles() + return SantaStatus(version: ver, status: stat, exportedConfigPath: exportedPath, logFiles: logs) + } +} diff --git a/tests/aftermath/SantaTests.swift b/tests/aftermath/SantaTests.swift new file mode 100644 index 0000000..197e3f6 --- /dev/null +++ b/tests/aftermath/SantaTests.swift @@ -0,0 +1,11 @@ +import XCTest +@testable import aftermath + +final class SantaTests: XCTestCase { + func testCollectorAPIsDoNotCrash() { + // These calls are safe if `santactl` is missing — they should return nil/empty rather than crash. + _ = SantaCollector.version() + _ = SantaCollector.status() + _ = SantaCollector.listLogFiles() + } +} From fecee00016d89740f29b59c630fc6219a326ff9f Mon Sep 17 00:00:00 2001 From: James Stracey Date: Tue, 3 Feb 2026 15:49:33 +1100 Subject: [PATCH 2/2] Format timestamps without trailing Z to match tests --- aftermath/Aftermath.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aftermath/Aftermath.swift b/aftermath/Aftermath.swift index 0fec49c..dfc96bc 100644 --- a/aftermath/Aftermath.swift +++ b/aftermath/Aftermath.swift @@ -56,7 +56,7 @@ class Aftermath { let dateFormatter = DateFormatter() dateFormatter.locale = Locale(identifier: "en_US") - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) let dateString = dateFormatter.string(from: date as Date) @@ -72,7 +72,7 @@ class Aftermath { dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) if let date = dateFormatter.date(from: timeStamp) { - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" let dateString = dateFormatter.string(from: date as Date) return dateString } @@ -80,7 +80,7 @@ class Aftermath { dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" if let date = dateFormatter.date(from: timeStamp) { - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" let dateString = dateFormatter.string(from: date as Date) return dateString } else {