mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-04 07:47:27 +01:00
Bins, careful/freeze hooks, setup, upgrade migrations, browse/src, design, ios-qa daemon, lib and scripts resolve the state root through bin/gstack-state-root.sh (bash) or lib/state-root.ts (TS). Bins source the twin and stop with a reinstall message when it is missing; hooks source it and never spawn gstack-paths. browse/src/config.ts and lib/cso/state.ts delegate to resolveStateRoot. Analytics writers and readers move together so the usage log stays one file. gstack-uninstall deletes state only at ~/.gstack, refuses (exit 2) when it resolves to /, $HOME or an ancestor, the checkout or the git root, and leaves any other resolved root in place with the removal command. Fixtures that copy single bins now copy the twin.
94 lines
3.4 KiB
TypeScript
94 lines
3.4 KiB
TypeScript
/**
|
|
* redact-audit-log — append-only forensic trail for the Phase 4.5a semantic
|
|
* review (D5). Records WHETHER the semantic pass marked a body clean/flagged and
|
|
* WHICH categories fired — never the body content. A body_sha256 lets a later
|
|
* investigation confirm "the pass saw this exact draft and called it clean."
|
|
*
|
|
* The file (`~/.gstack/security/semantic-reviews.jsonl`) is sensitive metadata,
|
|
* not "safe": it leaks repo names, timing, and a membership oracle via the hash.
|
|
* Written 0600. Local-only — no third-party egress.
|
|
*
|
|
* Usable two ways:
|
|
* - CLI: bun lib/redact-audit-log.ts '<json-line-without-ts/hash>' [body-file]
|
|
* (the skill passes the outcome JSON + a path to the scanned body; we
|
|
* stamp ts + body_sha256 and append.)
|
|
* - import { appendSemanticReview } from "./redact-audit-log";
|
|
*/
|
|
import * as fs from "fs";
|
|
import * as os from "os";
|
|
import * as path from "path";
|
|
import { createHash } from "crypto";
|
|
import { appendJsonl } from "./jsonl-store";
|
|
import { resolveStateRoot } from "./state-root";
|
|
|
|
export interface SemanticReviewEntry {
|
|
ts: string;
|
|
spec_archive_path?: string;
|
|
repo_visibility: string;
|
|
outcome: "clean" | "flagged";
|
|
categories_flagged: string[];
|
|
body_sha256: string;
|
|
}
|
|
|
|
function securityDir(): string {
|
|
const home = resolveStateRoot();
|
|
return path.join(home, "security");
|
|
}
|
|
|
|
export function sha256(s: string): string {
|
|
return createHash("sha256").update(s, "utf8").digest("hex");
|
|
}
|
|
|
|
/** Append one entry. Best-effort: never throws into the caller's flow. */
|
|
export function appendSemanticReview(entry: SemanticReviewEntry): void {
|
|
try {
|
|
const dir = securityDir();
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
const file = path.join(dir, "semantic-reviews.jsonl");
|
|
// 0600 at create via appendJsonl's mode opt; the chmod backstop covers
|
|
// files created looser by pre-mode versions.
|
|
appendJsonl(file, entry, { mode: 0o600 });
|
|
try {
|
|
fs.chmodSync(file, 0o600);
|
|
} catch {
|
|
// chmod can fail on some filesystems; the append still happened.
|
|
}
|
|
} catch {
|
|
// audit log is best-effort, not the security boundary
|
|
}
|
|
}
|
|
|
|
// ── CLI ───────────────────────────────────────────────────────────────────────
|
|
|
|
function now(): string {
|
|
// Date is allowed here (CLI process, not a resumable workflow).
|
|
return new Date().toISOString();
|
|
}
|
|
|
|
if (import.meta.main) {
|
|
const json = process.argv[2];
|
|
const bodyFile = process.argv[3];
|
|
if (!json) {
|
|
process.stderr.write(
|
|
'usage: redact-audit-log \'{"repo_visibility":"public","outcome":"flagged","categories_flagged":["legal"],"spec_archive_path":"..."}\' [body-file]\n',
|
|
);
|
|
process.exit(1);
|
|
}
|
|
let partial: Partial<SemanticReviewEntry>;
|
|
try {
|
|
partial = JSON.parse(json);
|
|
} catch {
|
|
process.stderr.write("redact-audit-log: invalid JSON\n");
|
|
process.exit(1);
|
|
}
|
|
const body = bodyFile && fs.existsSync(bodyFile) ? fs.readFileSync(bodyFile, "utf8") : "";
|
|
appendSemanticReview({
|
|
ts: now(),
|
|
repo_visibility: partial.repo_visibility ?? "unknown",
|
|
outcome: partial.outcome === "flagged" ? "flagged" : "clean",
|
|
categories_flagged: partial.categories_flagged ?? [],
|
|
body_sha256: sha256(body),
|
|
...(partial.spec_archive_path ? { spec_archive_path: partial.spec_archive_path } : {}),
|
|
});
|
|
}
|