Harden CLI local vault spool durability
This commit is contained in:
+22
-2
@@ -1051,6 +1051,12 @@ func (a *app) localVaultSpool(args []string) error {
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(*spoolFile) == "" {
|
||||
return errors.New("--spool-file is required")
|
||||
}
|
||||
if strings.TrimSpace(*file) == "" {
|
||||
return errors.New("--file is required")
|
||||
}
|
||||
raw, err := os.ReadFile(*file)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1066,12 +1072,26 @@ func (a *app) localVaultSpool(args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fh.Close()
|
||||
canonical, err := attesto.CanonicalJSON(obj)
|
||||
if err != nil {
|
||||
_ = fh.Close()
|
||||
return err
|
||||
}
|
||||
if _, err := fh.Write(append(canonical, '\n')); err != nil {
|
||||
line := append(canonical, '\n')
|
||||
written, err := fh.Write(line)
|
||||
if err != nil {
|
||||
_ = fh.Close()
|
||||
return err
|
||||
}
|
||||
if written != len(line) {
|
||||
_ = fh.Close()
|
||||
return fmt.Errorf("short spool write: wrote %d of %d bytes", written, len(line))
|
||||
}
|
||||
if err := fh.Sync(); err != nil {
|
||||
_ = fh.Close()
|
||||
return err
|
||||
}
|
||||
if err := fh.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return a.write(map[string]any{"ok": true, "spoolFile": *spoolFile})
|
||||
|
||||
@@ -158,6 +158,54 @@ func TestConfigSetRedactsSecrets(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalVaultSpoolAndStatus(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
eventFile := filepath.Join(dir, "event.json")
|
||||
spoolFile := filepath.Join(dir, "spool", "events.jsonl")
|
||||
if err := os.WriteFile(eventFile, []byte(`{"z":2,"a":1}`), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var stdout, stderr bytes.Buffer
|
||||
code := run([]string{"--json", "local-vault", "spool", "--spool-file", spoolFile, "--file", eventFile}, &stdout, &stderr, testEnv(t, nil))
|
||||
if code != 0 {
|
||||
t.Fatalf("exit=%d stderr=%s", code, stderr.String())
|
||||
}
|
||||
raw, err := os.ReadFile(spoolFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(raw) != "{\"a\":1,\"z\":2}\n" {
|
||||
t.Fatalf("spool must write canonical JSONL, got %q", raw)
|
||||
}
|
||||
info, err := os.Stat(spoolFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if info.Mode().Perm() != 0o600 {
|
||||
t.Fatalf("spool mode = %o, want 600", info.Mode().Perm())
|
||||
}
|
||||
stdout.Reset()
|
||||
stderr.Reset()
|
||||
code = run([]string{"--json", "local-vault", "status", "--spool-file", spoolFile}, &stdout, &stderr, testEnv(t, nil))
|
||||
if code != 0 {
|
||||
t.Fatalf("exit=%d stderr=%s", code, stderr.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), `"events": 1`) || !strings.Contains(stdout.String(), `"ok": true`) {
|
||||
t.Fatalf("unexpected status output: %s", stdout.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalVaultSpoolRequiresPaths(t *testing.T) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
code := run([]string{"--json", "local-vault", "spool", "--spool-file", filepath.Join(t.TempDir(), "events.jsonl")}, &stdout, &stderr, testEnv(t, nil))
|
||||
if code == 0 {
|
||||
t.Fatal("missing --file must fail")
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "--file is required") {
|
||||
t.Fatalf("missing required path error: %s", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamsCreateCallsAPI(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v2/streams" || r.Method != http.MethodPost {
|
||||
|
||||
Reference in New Issue
Block a user