mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-24 15:41:40 +00:00
fix: harden config write auditing
This commit is contained in:
@@ -768,10 +768,8 @@ struct DebugSettings: View {
|
||||
}
|
||||
|
||||
private func loadSessionStorePath() {
|
||||
let url = self.configURL()
|
||||
let parsed = OpenClawConfigFile.loadDict()
|
||||
guard
|
||||
let data = try? Data(contentsOf: url),
|
||||
let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let session = parsed["session"] as? [String: Any],
|
||||
let path = session["store"] as? String
|
||||
else {
|
||||
@@ -783,28 +781,14 @@ struct DebugSettings: View {
|
||||
|
||||
private func saveSessionStorePath() {
|
||||
let trimmed = self.sessionStorePath.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
var root: [String: Any] = [:]
|
||||
let url = self.configURL()
|
||||
if let data = try? Data(contentsOf: url),
|
||||
let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
||||
{
|
||||
root = parsed
|
||||
}
|
||||
var root = OpenClawConfigFile.loadDict()
|
||||
|
||||
var session = root["session"] as? [String: Any] ?? [:]
|
||||
session["store"] = trimmed.isEmpty ? SessionLoader.defaultStorePath : trimmed
|
||||
root["session"] = session
|
||||
|
||||
do {
|
||||
let data = try JSONSerialization.data(withJSONObject: root, options: [.prettyPrinted, .sortedKeys])
|
||||
try FileManager().createDirectory(
|
||||
at: url.deletingLastPathComponent(),
|
||||
withIntermediateDirectories: true)
|
||||
try data.write(to: url, options: [.atomic])
|
||||
self.sessionStoreSaveError = nil
|
||||
} catch {
|
||||
self.sessionStoreSaveError = error.localizedDescription
|
||||
}
|
||||
OpenClawConfigFile.saveDict(root)
|
||||
self.sessionStoreSaveError = nil
|
||||
}
|
||||
|
||||
private var bindingOverride: Binding<String> {
|
||||
@@ -828,10 +812,6 @@ struct DebugSettings: View {
|
||||
private var canRestartGateway: Bool {
|
||||
self.state.connectionMode == .local
|
||||
}
|
||||
|
||||
private func configURL() -> URL {
|
||||
OpenClawPaths.configURL
|
||||
}
|
||||
}
|
||||
|
||||
extension DebugSettings {
|
||||
|
||||
@@ -44,6 +44,7 @@ enum OpenClawConfigFile {
|
||||
let previousData = try? Data(contentsOf: url)
|
||||
let previousRoot = previousData.flatMap { self.parseConfigData($0) }
|
||||
let previousBytes = previousData?.count
|
||||
let previousAttributes = try? FileManager().attributesOfItem(atPath: url.path)
|
||||
let hadMetaBefore = self.hasMeta(previousRoot)
|
||||
let gatewayModeBefore = self.gatewayMode(previousRoot)
|
||||
|
||||
@@ -57,6 +58,7 @@ enum OpenClawConfigFile {
|
||||
withIntermediateDirectories: true)
|
||||
try data.write(to: url, options: [.atomic])
|
||||
let nextBytes = data.count
|
||||
let nextAttributes = try? FileManager().attributesOfItem(atPath: url.path)
|
||||
let gatewayModeAfter = self.gatewayMode(output)
|
||||
let suspicious = self.configWriteSuspiciousReasons(
|
||||
existsBefore: previousData != nil,
|
||||
@@ -74,6 +76,18 @@ enum OpenClawConfigFile {
|
||||
"existsBefore": previousData != nil,
|
||||
"previousBytes": previousBytes ?? NSNull(),
|
||||
"nextBytes": nextBytes,
|
||||
"previousDev": self.fileSystemNumber(previousAttributes?[.systemNumber]) ?? NSNull(),
|
||||
"nextDev": self.fileSystemNumber(nextAttributes?[.systemNumber]) ?? NSNull(),
|
||||
"previousIno": self.fileSystemNumber(previousAttributes?[.systemFileNumber]) ?? NSNull(),
|
||||
"nextIno": self.fileSystemNumber(nextAttributes?[.systemFileNumber]) ?? NSNull(),
|
||||
"previousMode": self.posixMode(previousAttributes?[.posixPermissions]) ?? NSNull(),
|
||||
"nextMode": self.posixMode(nextAttributes?[.posixPermissions]) ?? NSNull(),
|
||||
"previousNlink": self.fileAttributeInt(previousAttributes?[.referenceCount]) ?? NSNull(),
|
||||
"nextNlink": self.fileAttributeInt(nextAttributes?[.referenceCount]) ?? NSNull(),
|
||||
"previousUid": self.fileAttributeInt(previousAttributes?[.ownerAccountID]) ?? NSNull(),
|
||||
"nextUid": self.fileAttributeInt(nextAttributes?[.ownerAccountID]) ?? NSNull(),
|
||||
"previousGid": self.fileAttributeInt(previousAttributes?[.groupOwnerAccountID]) ?? NSNull(),
|
||||
"nextGid": self.fileAttributeInt(nextAttributes?[.groupOwnerAccountID]) ?? NSNull(),
|
||||
"hasMetaBefore": hadMetaBefore,
|
||||
"hasMetaAfter": self.hasMeta(output),
|
||||
"gatewayModeBefore": gatewayModeBefore ?? NSNull(),
|
||||
@@ -384,6 +398,23 @@ enum OpenClawConfigFile {
|
||||
return date.timeIntervalSince1970 * 1000
|
||||
}
|
||||
|
||||
private static func fileAttributeInt(_ value: Any?) -> Int? {
|
||||
if let number = value as? NSNumber { return number.intValue }
|
||||
if let number = value as? Int { return number }
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func fileSystemNumber(_ value: Any?) -> String? {
|
||||
if let number = value as? NSNumber { return number.stringValue }
|
||||
if let number = value as? Int { return String(number) }
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func posixMode(_ value: Any?) -> Int? {
|
||||
guard let mode = self.fileAttributeInt(value) else { return nil }
|
||||
return mode & 0o777
|
||||
}
|
||||
|
||||
private static func configFingerprint(
|
||||
data: Data,
|
||||
root: [String: Any]?,
|
||||
@@ -396,6 +427,12 @@ enum OpenClawConfigFile {
|
||||
"bytes": data.count,
|
||||
"mtimeMs": self.fileTimestampMs(attributes?[.modificationDate]) ?? NSNull(),
|
||||
"ctimeMs": self.fileTimestampMs(attributes?[.creationDate]) ?? NSNull(),
|
||||
"dev": self.fileSystemNumber(attributes?[.systemNumber]) ?? NSNull(),
|
||||
"ino": self.fileSystemNumber(attributes?[.systemFileNumber]) ?? NSNull(),
|
||||
"mode": self.posixMode(attributes?[.posixPermissions]) ?? NSNull(),
|
||||
"nlink": self.fileAttributeInt(attributes?[.referenceCount]) ?? NSNull(),
|
||||
"uid": self.fileAttributeInt(attributes?[.ownerAccountID]) ?? NSNull(),
|
||||
"gid": self.fileAttributeInt(attributes?[.groupOwnerAccountID]) ?? NSNull(),
|
||||
"hasMeta": self.hasMeta(root),
|
||||
"gatewayMode": self.gatewayMode(root) ?? NSNull(),
|
||||
"observedAt": observedAt,
|
||||
@@ -408,6 +445,12 @@ enum OpenClawConfigFile {
|
||||
(left["bytes"] as? Int) == (right["bytes"] as? Int) &&
|
||||
(left["mtimeMs"] as? Double) == (right["mtimeMs"] as? Double) &&
|
||||
(left["ctimeMs"] as? Double) == (right["ctimeMs"] as? Double) &&
|
||||
(left["dev"] as? String) == (right["dev"] as? String) &&
|
||||
(left["ino"] as? String) == (right["ino"] as? String) &&
|
||||
(left["mode"] as? Int) == (right["mode"] as? Int) &&
|
||||
(left["nlink"] as? Int) == (right["nlink"] as? Int) &&
|
||||
(left["uid"] as? Int) == (right["uid"] as? Int) &&
|
||||
(left["gid"] as? Int) == (right["gid"] as? Int) &&
|
||||
(left["hasMeta"] as? Bool) == (right["hasMeta"] as? Bool) &&
|
||||
(left["gatewayMode"] as? String) == (right["gatewayMode"] as? String)
|
||||
}
|
||||
@@ -509,6 +552,12 @@ enum OpenClawConfigFile {
|
||||
"bytes": current["bytes"] ?? NSNull(),
|
||||
"mtimeMs": current["mtimeMs"] ?? NSNull(),
|
||||
"ctimeMs": current["ctimeMs"] ?? NSNull(),
|
||||
"dev": current["dev"] ?? NSNull(),
|
||||
"ino": current["ino"] ?? NSNull(),
|
||||
"mode": current["mode"] ?? NSNull(),
|
||||
"nlink": current["nlink"] ?? NSNull(),
|
||||
"uid": current["uid"] ?? NSNull(),
|
||||
"gid": current["gid"] ?? NSNull(),
|
||||
"hasMeta": current["hasMeta"] ?? false,
|
||||
"gatewayMode": current["gatewayMode"] ?? NSNull(),
|
||||
"suspicious": suspicious,
|
||||
@@ -516,11 +565,23 @@ enum OpenClawConfigFile {
|
||||
"lastKnownGoodBytes": lastKnownGood?["bytes"] ?? NSNull(),
|
||||
"lastKnownGoodMtimeMs": lastKnownGood?["mtimeMs"] ?? NSNull(),
|
||||
"lastKnownGoodCtimeMs": lastKnownGood?["ctimeMs"] ?? NSNull(),
|
||||
"lastKnownGoodDev": lastKnownGood?["dev"] ?? NSNull(),
|
||||
"lastKnownGoodIno": lastKnownGood?["ino"] ?? NSNull(),
|
||||
"lastKnownGoodMode": lastKnownGood?["mode"] ?? NSNull(),
|
||||
"lastKnownGoodNlink": lastKnownGood?["nlink"] ?? NSNull(),
|
||||
"lastKnownGoodUid": lastKnownGood?["uid"] ?? NSNull(),
|
||||
"lastKnownGoodGid": lastKnownGood?["gid"] ?? NSNull(),
|
||||
"lastKnownGoodGatewayMode": lastKnownGood?["gatewayMode"] ?? NSNull(),
|
||||
"backupHash": backup?["hash"] ?? NSNull(),
|
||||
"backupBytes": backup?["bytes"] ?? NSNull(),
|
||||
"backupMtimeMs": backup?["mtimeMs"] ?? NSNull(),
|
||||
"backupCtimeMs": backup?["ctimeMs"] ?? NSNull(),
|
||||
"backupDev": backup?["dev"] ?? NSNull(),
|
||||
"backupIno": backup?["ino"] ?? NSNull(),
|
||||
"backupMode": backup?["mode"] ?? NSNull(),
|
||||
"backupNlink": backup?["nlink"] ?? NSNull(),
|
||||
"backupUid": backup?["uid"] ?? NSNull(),
|
||||
"backupGid": backup?["gid"] ?? NSNull(),
|
||||
"backupGatewayMode": backup?["gatewayMode"] ?? NSNull(),
|
||||
"clobberedPath": clobberedPath ?? NSNull(),
|
||||
])
|
||||
|
||||
@@ -133,6 +133,10 @@ struct OpenClawConfigFileTests {
|
||||
#expect(auditRoot?["event"] as? String == "config.write")
|
||||
#expect(auditRoot?["result"] as? String == "success")
|
||||
#expect(auditRoot?["configPath"] as? String == configPath.path)
|
||||
#expect(auditRoot?["previousMode"] is NSNumber)
|
||||
#expect(auditRoot?["nextMode"] is NSNumber)
|
||||
#expect(auditRoot?["previousIno"] as? String != nil)
|
||||
#expect(auditRoot?["nextIno"] as? String != nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,6 +192,10 @@ struct OpenClawConfigFileTests {
|
||||
let auditRoot = try JSONSerialization.jsonObject(with: Data(observeLine.utf8)) as? [String: Any]
|
||||
#expect(auditRoot?["source"] as? String == "macos-openclaw-config-file")
|
||||
#expect(auditRoot?["configPath"] as? String == configPath.path)
|
||||
#expect(auditRoot?["mode"] is NSNumber)
|
||||
#expect(auditRoot?["ino"] as? String != nil)
|
||||
#expect(auditRoot?["lastKnownGoodMode"] is NSNumber)
|
||||
#expect(auditRoot?["backupMode"] is NSNumber)
|
||||
let suspicious = auditRoot?["suspicious"] as? [String] ?? []
|
||||
#expect(suspicious.contains("gateway-mode-missing-vs-last-good"))
|
||||
#expect(suspicious.contains("update-channel-only-root"))
|
||||
|
||||
@@ -76,6 +76,10 @@ describe("config io observe", () => {
|
||||
expect(observe?.source).toBe("config-io");
|
||||
expect(observe?.configPath).toBe(configPath);
|
||||
expect(observe?.valid).toBe(true);
|
||||
expect(observe?.mode).toBeTypeOf("number");
|
||||
expect(observe?.ino).toBeTypeOf("string");
|
||||
expect(observe?.lastKnownGoodMode).toBeTypeOf("number");
|
||||
expect(observe?.backupMode).toBeTypeOf("number");
|
||||
expect(observe?.suspicious).toEqual(
|
||||
expect.arrayContaining(["gateway-mode-missing-vs-last-good", "update-channel-only-root"]),
|
||||
);
|
||||
@@ -157,6 +161,8 @@ describe("config io observe", () => {
|
||||
|
||||
expect(observe).toBeDefined();
|
||||
expect(observe?.backupHash).toBeTypeOf("string");
|
||||
expect(observe?.backupIno).toBeTypeOf("string");
|
||||
expect(observe?.lastKnownGoodIno ?? null).toBeNull();
|
||||
expect(observe?.suspicious).toEqual(
|
||||
expect.arrayContaining(["gateway-mode-missing-vs-last-good", "update-channel-only-root"]),
|
||||
);
|
||||
|
||||
149
src/config/io.ts
149
src/config/io.ts
@@ -110,6 +110,18 @@ type ConfigWriteAuditRecord = {
|
||||
nextHash: string | null;
|
||||
previousBytes: number | null;
|
||||
nextBytes: number | null;
|
||||
previousDev: string | null;
|
||||
nextDev: string | null;
|
||||
previousIno: string | null;
|
||||
nextIno: string | null;
|
||||
previousMode: number | null;
|
||||
nextMode: number | null;
|
||||
previousNlink: number | null;
|
||||
nextNlink: number | null;
|
||||
previousUid: number | null;
|
||||
nextUid: number | null;
|
||||
previousGid: number | null;
|
||||
nextGid: number | null;
|
||||
changedPathCount: number | null;
|
||||
hasMetaBefore: boolean;
|
||||
hasMetaAfter: boolean;
|
||||
@@ -125,6 +137,12 @@ type ConfigHealthFingerprint = {
|
||||
bytes: number;
|
||||
mtimeMs: number | null;
|
||||
ctimeMs: number | null;
|
||||
dev: string | null;
|
||||
ino: string | null;
|
||||
mode: number | null;
|
||||
nlink: number | null;
|
||||
uid: number | null;
|
||||
gid: number | null;
|
||||
hasMeta: boolean;
|
||||
gatewayMode: string | null;
|
||||
observedAt: string;
|
||||
@@ -156,6 +174,12 @@ type ConfigObserveAuditRecord = {
|
||||
bytes: number | null;
|
||||
mtimeMs: number | null;
|
||||
ctimeMs: number | null;
|
||||
dev: string | null;
|
||||
ino: string | null;
|
||||
mode: number | null;
|
||||
nlink: number | null;
|
||||
uid: number | null;
|
||||
gid: number | null;
|
||||
hasMeta: boolean;
|
||||
gatewayMode: string | null;
|
||||
suspicious: string[];
|
||||
@@ -163,11 +187,23 @@ type ConfigObserveAuditRecord = {
|
||||
lastKnownGoodBytes: number | null;
|
||||
lastKnownGoodMtimeMs: number | null;
|
||||
lastKnownGoodCtimeMs: number | null;
|
||||
lastKnownGoodDev: string | null;
|
||||
lastKnownGoodIno: string | null;
|
||||
lastKnownGoodMode: number | null;
|
||||
lastKnownGoodNlink: number | null;
|
||||
lastKnownGoodUid: number | null;
|
||||
lastKnownGoodGid: number | null;
|
||||
lastKnownGoodGatewayMode: string | null;
|
||||
backupHash: string | null;
|
||||
backupBytes: number | null;
|
||||
backupMtimeMs: number | null;
|
||||
backupCtimeMs: number | null;
|
||||
backupDev: string | null;
|
||||
backupIno: string | null;
|
||||
backupMode: number | null;
|
||||
backupNlink: number | null;
|
||||
backupUid: number | null;
|
||||
backupGid: number | null;
|
||||
backupGatewayMode: string | null;
|
||||
clobberedPath: string | null;
|
||||
};
|
||||
@@ -596,6 +632,33 @@ function resolveConfigHealthStatePath(env: NodeJS.ProcessEnv, homedir: () => str
|
||||
return path.join(resolveStateDir(env, homedir), "logs", CONFIG_HEALTH_STATE_FILENAME);
|
||||
}
|
||||
|
||||
function normalizeStatNumber(value: number | null | undefined): number | null {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
function normalizeStatId(value: number | bigint | null | undefined): string | null {
|
||||
if (typeof value === "bigint") {
|
||||
return value.toString();
|
||||
}
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
return String(value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveConfigStatMetadata(
|
||||
stat: fs.Stats | null,
|
||||
): Pick<ConfigHealthFingerprint, "dev" | "ino" | "mode" | "nlink" | "uid" | "gid"> {
|
||||
return {
|
||||
dev: normalizeStatId(stat?.dev ?? null),
|
||||
ino: normalizeStatId(stat?.ino ?? null),
|
||||
mode: normalizeStatNumber(stat ? stat.mode & 0o777 : null),
|
||||
nlink: normalizeStatNumber(stat?.nlink ?? null),
|
||||
uid: normalizeStatNumber(stat?.uid ?? null),
|
||||
gid: normalizeStatNumber(stat?.gid ?? null),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveConfigWriteSuspiciousReasons(params: {
|
||||
existsBefore: boolean;
|
||||
previousBytes: number | null;
|
||||
@@ -788,6 +851,7 @@ async function readConfigFingerprintForPath(
|
||||
bytes: Buffer.byteLength(raw, "utf-8"),
|
||||
mtimeMs: stat?.mtimeMs ?? null,
|
||||
ctimeMs: stat?.ctimeMs ?? null,
|
||||
...resolveConfigStatMetadata(stat),
|
||||
hasMeta: hasConfigMeta(parsed),
|
||||
gatewayMode: resolveGatewayMode(parsed),
|
||||
observedAt: new Date().toISOString(),
|
||||
@@ -811,6 +875,7 @@ function readConfigFingerprintForPathSync(
|
||||
bytes: Buffer.byteLength(raw, "utf-8"),
|
||||
mtimeMs: stat?.mtimeMs ?? null,
|
||||
ctimeMs: stat?.ctimeMs ?? null,
|
||||
...resolveConfigStatMetadata(stat),
|
||||
hasMeta: hasConfigMeta(parsed),
|
||||
gatewayMode: resolveGatewayMode(parsed),
|
||||
observedAt: new Date().toISOString(),
|
||||
@@ -874,6 +939,12 @@ function sameFingerprint(
|
||||
left.bytes === right.bytes &&
|
||||
left.mtimeMs === right.mtimeMs &&
|
||||
left.ctimeMs === right.ctimeMs &&
|
||||
left.dev === right.dev &&
|
||||
left.ino === right.ino &&
|
||||
left.mode === right.mode &&
|
||||
left.nlink === right.nlink &&
|
||||
left.uid === right.uid &&
|
||||
left.gid === right.gid &&
|
||||
left.hasMeta === right.hasMeta &&
|
||||
left.gatewayMode === right.gatewayMode
|
||||
);
|
||||
@@ -894,6 +965,7 @@ async function observeConfigSnapshot(
|
||||
bytes: Buffer.byteLength(snapshot.raw, "utf-8"),
|
||||
mtimeMs: stat?.mtimeMs ?? null,
|
||||
ctimeMs: stat?.ctimeMs ?? null,
|
||||
...resolveConfigStatMetadata(stat),
|
||||
hasMeta: hasConfigMeta(snapshot.parsed),
|
||||
gatewayMode: resolveGatewayMode(snapshot.resolved),
|
||||
observedAt: now,
|
||||
@@ -963,6 +1035,12 @@ async function observeConfigSnapshot(
|
||||
bytes: current.bytes,
|
||||
mtimeMs: current.mtimeMs,
|
||||
ctimeMs: current.ctimeMs,
|
||||
dev: current.dev,
|
||||
ino: current.ino,
|
||||
mode: current.mode,
|
||||
nlink: current.nlink,
|
||||
uid: current.uid,
|
||||
gid: current.gid,
|
||||
hasMeta: current.hasMeta,
|
||||
gatewayMode: current.gatewayMode,
|
||||
suspicious,
|
||||
@@ -970,11 +1048,23 @@ async function observeConfigSnapshot(
|
||||
lastKnownGoodBytes: entry.lastKnownGood?.bytes ?? null,
|
||||
lastKnownGoodMtimeMs: entry.lastKnownGood?.mtimeMs ?? null,
|
||||
lastKnownGoodCtimeMs: entry.lastKnownGood?.ctimeMs ?? null,
|
||||
lastKnownGoodDev: entry.lastKnownGood?.dev ?? null,
|
||||
lastKnownGoodIno: entry.lastKnownGood?.ino ?? null,
|
||||
lastKnownGoodMode: entry.lastKnownGood?.mode ?? null,
|
||||
lastKnownGoodNlink: entry.lastKnownGood?.nlink ?? null,
|
||||
lastKnownGoodUid: entry.lastKnownGood?.uid ?? null,
|
||||
lastKnownGoodGid: entry.lastKnownGood?.gid ?? null,
|
||||
lastKnownGoodGatewayMode: entry.lastKnownGood?.gatewayMode ?? null,
|
||||
backupHash: backup?.hash ?? null,
|
||||
backupBytes: backup?.bytes ?? null,
|
||||
backupMtimeMs: backup?.mtimeMs ?? null,
|
||||
backupCtimeMs: backup?.ctimeMs ?? null,
|
||||
backupDev: backup?.dev ?? null,
|
||||
backupIno: backup?.ino ?? null,
|
||||
backupMode: backup?.mode ?? null,
|
||||
backupNlink: backup?.nlink ?? null,
|
||||
backupUid: backup?.uid ?? null,
|
||||
backupGid: backup?.gid ?? null,
|
||||
backupGatewayMode: backup?.gatewayMode ?? null,
|
||||
clobberedPath,
|
||||
});
|
||||
@@ -1001,6 +1091,7 @@ function observeConfigSnapshotSync(
|
||||
bytes: Buffer.byteLength(snapshot.raw, "utf-8"),
|
||||
mtimeMs: stat?.mtimeMs ?? null,
|
||||
ctimeMs: stat?.ctimeMs ?? null,
|
||||
...resolveConfigStatMetadata(stat),
|
||||
hasMeta: hasConfigMeta(snapshot.parsed),
|
||||
gatewayMode: resolveGatewayMode(snapshot.resolved),
|
||||
observedAt: now,
|
||||
@@ -1070,6 +1161,12 @@ function observeConfigSnapshotSync(
|
||||
bytes: current.bytes,
|
||||
mtimeMs: current.mtimeMs,
|
||||
ctimeMs: current.ctimeMs,
|
||||
dev: current.dev,
|
||||
ino: current.ino,
|
||||
mode: current.mode,
|
||||
nlink: current.nlink,
|
||||
uid: current.uid,
|
||||
gid: current.gid,
|
||||
hasMeta: current.hasMeta,
|
||||
gatewayMode: current.gatewayMode,
|
||||
suspicious,
|
||||
@@ -1077,11 +1174,23 @@ function observeConfigSnapshotSync(
|
||||
lastKnownGoodBytes: entry.lastKnownGood?.bytes ?? null,
|
||||
lastKnownGoodMtimeMs: entry.lastKnownGood?.mtimeMs ?? null,
|
||||
lastKnownGoodCtimeMs: entry.lastKnownGood?.ctimeMs ?? null,
|
||||
lastKnownGoodDev: entry.lastKnownGood?.dev ?? null,
|
||||
lastKnownGoodIno: entry.lastKnownGood?.ino ?? null,
|
||||
lastKnownGoodMode: entry.lastKnownGood?.mode ?? null,
|
||||
lastKnownGoodNlink: entry.lastKnownGood?.nlink ?? null,
|
||||
lastKnownGoodUid: entry.lastKnownGood?.uid ?? null,
|
||||
lastKnownGoodGid: entry.lastKnownGood?.gid ?? null,
|
||||
lastKnownGoodGatewayMode: entry.lastKnownGood?.gatewayMode ?? null,
|
||||
backupHash: backup?.hash ?? null,
|
||||
backupBytes: backup?.bytes ?? null,
|
||||
backupMtimeMs: backup?.mtimeMs ?? null,
|
||||
backupCtimeMs: backup?.ctimeMs ?? null,
|
||||
backupDev: backup?.dev ?? null,
|
||||
backupIno: backup?.ino ?? null,
|
||||
backupMode: backup?.mode ?? null,
|
||||
backupNlink: backup?.nlink ?? null,
|
||||
backupUid: backup?.uid ?? null,
|
||||
backupGid: backup?.gid ?? null,
|
||||
backupGatewayMode: backup?.gatewayMode ?? null,
|
||||
clobberedPath,
|
||||
});
|
||||
@@ -1777,6 +1886,9 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
const previousBytes =
|
||||
typeof snapshot.raw === "string" ? Buffer.byteLength(snapshot.raw, "utf-8") : null;
|
||||
const nextBytes = Buffer.byteLength(json, "utf-8");
|
||||
const previousStat = snapshot.exists
|
||||
? await deps.fs.promises.stat(configPath).catch(() => null)
|
||||
: null;
|
||||
const hasMetaBefore = hasConfigMeta(snapshot.parsed);
|
||||
const hasMetaAfter = hasConfigMeta(stampedOutputConfig);
|
||||
const gatewayModeBefore = resolveGatewayMode(snapshot.resolved);
|
||||
@@ -1842,6 +1954,18 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
nextHash,
|
||||
previousBytes,
|
||||
nextBytes,
|
||||
previousDev: resolveConfigStatMetadata(previousStat).dev,
|
||||
nextDev: null,
|
||||
previousIno: resolveConfigStatMetadata(previousStat).ino,
|
||||
nextIno: null,
|
||||
previousMode: resolveConfigStatMetadata(previousStat).mode,
|
||||
nextMode: null,
|
||||
previousNlink: resolveConfigStatMetadata(previousStat).nlink,
|
||||
nextNlink: null,
|
||||
previousUid: resolveConfigStatMetadata(previousStat).uid,
|
||||
nextUid: null,
|
||||
previousGid: resolveConfigStatMetadata(previousStat).gid,
|
||||
nextGid: null,
|
||||
changedPathCount: typeof changedPathCount === "number" ? changedPathCount : null,
|
||||
hasMetaBefore,
|
||||
hasMetaAfter,
|
||||
@@ -1849,7 +1973,11 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
gatewayModeAfter,
|
||||
suspicious: suspiciousReasons,
|
||||
};
|
||||
const appendWriteAudit = async (result: ConfigWriteAuditResult, err?: unknown) => {
|
||||
const appendWriteAudit = async (
|
||||
result: ConfigWriteAuditResult,
|
||||
err?: unknown,
|
||||
nextStat?: fs.Stats | null,
|
||||
) => {
|
||||
const errorCode =
|
||||
err && typeof err === "object" && "code" in err && typeof err.code === "string"
|
||||
? err.code
|
||||
@@ -1858,11 +1986,18 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
err && typeof err === "object" && "message" in err && typeof err.message === "string"
|
||||
? err.message
|
||||
: undefined;
|
||||
const nextMetadata = resolveConfigStatMetadata(nextStat ?? null);
|
||||
await appendConfigAuditRecord(deps, {
|
||||
...auditRecordBase,
|
||||
result,
|
||||
nextHash: result === "failed" ? null : auditRecordBase.nextHash,
|
||||
nextBytes: result === "failed" ? null : auditRecordBase.nextBytes,
|
||||
nextDev: result === "failed" ? null : nextMetadata.dev,
|
||||
nextIno: result === "failed" ? null : nextMetadata.ino,
|
||||
nextMode: result === "failed" ? null : nextMetadata.mode,
|
||||
nextNlink: result === "failed" ? null : nextMetadata.nlink,
|
||||
nextUid: result === "failed" ? null : nextMetadata.uid,
|
||||
nextGid: result === "failed" ? null : nextMetadata.gid,
|
||||
errorCode,
|
||||
errorMessage,
|
||||
});
|
||||
@@ -1898,7 +2033,11 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
});
|
||||
logConfigOverwrite();
|
||||
logConfigWriteAnomalies();
|
||||
await appendWriteAudit("copy-fallback");
|
||||
await appendWriteAudit(
|
||||
"copy-fallback",
|
||||
undefined,
|
||||
await deps.fs.promises.stat(configPath).catch(() => null),
|
||||
);
|
||||
return;
|
||||
}
|
||||
await deps.fs.promises.unlink(tmp).catch(() => {
|
||||
@@ -1908,7 +2047,11 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
}
|
||||
logConfigOverwrite();
|
||||
logConfigWriteAnomalies();
|
||||
await appendWriteAudit("rename");
|
||||
await appendWriteAudit(
|
||||
"rename",
|
||||
undefined,
|
||||
await deps.fs.promises.stat(configPath).catch(() => null),
|
||||
);
|
||||
} catch (err) {
|
||||
await appendWriteAudit("failed", err);
|
||||
throw err;
|
||||
|
||||
@@ -541,6 +541,10 @@ describe("config io write", () => {
|
||||
expect(last.hasMetaAfter).toBe(true);
|
||||
expect(last.previousHash).toBeTypeOf("string");
|
||||
expect(last.nextHash).toBeTypeOf("string");
|
||||
expect(last.previousMode).toBeTypeOf("number");
|
||||
expect(last.nextMode).toBeTypeOf("number");
|
||||
expect(last.previousIno).toBeTypeOf("string");
|
||||
expect(last.nextIno).toBeTypeOf("string");
|
||||
expect(last.result === "rename" || last.result === "copy-fallback").toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user