agent (unreleased): fixes after an independent review — one lock for the writers of agent.json, the trust run serialised and bounded, the CA copy keeps its previous root, the profile stanza from the wrapper's answer, a stale give-up ends, both boot tokens from one read
gates / gates (push) Successful in 48s
gates / gates (push) Successful in 48s
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
1 parent
61feb95c06
commit
ec7e699fd7
18 files changed
+333
-59
No files matched your search
@@ -100,49 +100,66 @@ func TestProfileRefresh_RefusedAndFailed(t *testing.T) {
|
||||
}
|
||||
|
||||
// R-1383 (release 3): the refresh restarted a failed service on the N100 and only the box's journal knew. The stanza
|
||||
// that rides the host report: nothing before any refresh (an UNTYPED nil — the report omits the field); after a
|
||||
// refresh, the root wrapper's own record (so it outlives an agent restart) and how this process's last ask ended.
|
||||
// that rides the host report: nothing before any ask (an UNTYPED nil — the report omits the field); after a refresh,
|
||||
// everything; after an agent RESTART, the kernel and time of that refresh again — from the wrapper's answer to the new
|
||||
// process's ask, because the wrapper's record FILE may be root-only on a box (its folder is 0700 where a signed job
|
||||
// made it first). Where the file can be read, it adds the detail back.
|
||||
// COMPANION RED-PROOFS (observed, felhom.eu audits/night-build-2026-11-14/B/redproof-agent-b3.txt).
|
||||
func TestProfileRefreshStanza_RecordAndLastAsk(t *testing.T) {
|
||||
w := &fakeWrapper{t: t, profileRep: refreshed()}
|
||||
l, _ := newLeg(t, w, &hub.WireOSUpdate{Ring: 1, Enabled: true})
|
||||
rec := filepath.Join(t.TempDir(), "profile-refresh.json")
|
||||
l.ProfileRecordPath = rec
|
||||
unreadable := filepath.Join(t.TempDir(), "root-only", "profile-refresh.json") // does not exist: as a 0700 folder reads
|
||||
l.ProfileRecordPath = unreadable
|
||||
now := time.Date(2026, 11, 14, 11, 30, 11, 0, time.UTC)
|
||||
l.Now = func() time.Time { return now }
|
||||
if st := l.ProfileRefreshStanza(); st != nil {
|
||||
t.Fatalf("before any refresh the stanza is %#v, want an untyped nil", st)
|
||||
t.Fatalf("before any ask the stanza is %#v, want an untyped nil", st)
|
||||
}
|
||||
// the wrapper refreshes and writes ITS record (the fake wrapper does not, so the test writes what the real one does)
|
||||
if got := l.ProfileRefresh(context.Background(), "agent start", nil, 1, time.Second); got != "refreshed" {
|
||||
t.Fatalf("outcome = %q", got)
|
||||
}
|
||||
b, _ := json.Marshal(l.ProfileRefreshStanza())
|
||||
if string(b) != `{"kernel":"7.0.14-28-pve","at":"2026-11-14T11:30:11Z","rewritten":1,"restarted":["chrony.service"],"last_ask":"refreshed","last_ask_at":"2026-11-14T11:30:11Z"}` {
|
||||
t.Fatalf("stanza after a refresh, record file unreadable = %s", b)
|
||||
}
|
||||
// an agent restart: a new process asks, the wrapper answers "not needed" with ITS record of the last refresh
|
||||
l2, _ := newLeg(t, &fakeWrapper{t: t, profileRep: &WrapperReport{ProfileRefresh: json.RawMessage(
|
||||
`{"outcome":"not_needed","reason":"this kernel was done","kernel":"7.0.14-28-pve","last_refresh_kernel":"7.0.14-28-pve","last_refresh_at":"2026-11-14T11:30:11Z"}`)}},
|
||||
&hub.WireOSUpdate{Ring: 1, Enabled: true})
|
||||
l2.ProfileRecordPath = unreadable
|
||||
l2.Now = func() time.Time { return now.Add(3 * time.Hour) }
|
||||
if st := l2.ProfileRefreshStanza(); st != nil {
|
||||
t.Fatalf("a new process that has not asked and cannot read the record: %#v, want nil", st)
|
||||
}
|
||||
if got := l2.ProfileRefresh(context.Background(), "agent start", nil, 1, time.Second); got != "not_needed" {
|
||||
t.Fatalf("outcome = %q", got)
|
||||
}
|
||||
b, _ = json.Marshal(l2.ProfileRefreshStanza())
|
||||
if string(b) != `{"kernel":"7.0.14-28-pve","at":"2026-11-14T11:30:11Z","last_ask":"not_needed","last_ask_at":"2026-11-14T14:30:11Z"}` {
|
||||
t.Fatalf("stanza after an agent restart (the wrapper's answer, no detail) = %s", b)
|
||||
}
|
||||
// where the record file CAN be read, the detail is back — and a name that is not a unit name is dropped
|
||||
rec := filepath.Join(t.TempDir(), "profile-refresh.json")
|
||||
if err := os.WriteFile(rec, []byte(`{"at":"2026-11-14T11:30:11Z","kernel":"7.0.14-28-pve","restarted":["chrony.service","bad name; rm -rf"],`+
|
||||
`"rewritten":["f6da0254.0/usr.sbin.chronyd","f6da0254.0/lxc-containers"]}`), 0o644); err != nil {
|
||||
`"rewritten":["a","b"]}`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, _ := json.Marshal(l.ProfileRefreshStanza())
|
||||
if string(b) != `{"kernel":"7.0.14-28-pve","at":"2026-11-14T11:30:11Z","rewritten":2,"restarted":["chrony.service"],"last_ask":"refreshed","last_ask_at":"2026-11-14T11:30:11Z"}` {
|
||||
t.Fatalf("stanza after a refresh = %s", b)
|
||||
}
|
||||
// an agent restart: a new process, the same record — the box's last refresh is still told
|
||||
l2, _ := newLeg(t, &fakeWrapper{t: t}, &hub.WireOSUpdate{Ring: 1, Enabled: true})
|
||||
l2.ProfileRecordPath = rec
|
||||
b, _ = json.Marshal(l2.ProfileRefreshStanza())
|
||||
if string(b) != `{"kernel":"7.0.14-28-pve","at":"2026-11-14T11:30:11Z","rewritten":2,"restarted":["chrony.service"]}` {
|
||||
t.Fatalf("stanza after an agent restart = %s", b)
|
||||
if string(b) != `{"kernel":"7.0.14-28-pve","at":"2026-11-14T11:30:11Z","rewritten":2,"restarted":["chrony.service"],"last_ask":"not_needed","last_ask_at":"2026-11-14T14:30:11Z"}` {
|
||||
t.Fatalf("stanza with a readable record = %s", b)
|
||||
}
|
||||
// a failed ask is told, beside the older record
|
||||
l2.Now = func() time.Time { return now.Add(time.Hour) }
|
||||
l2.Runner = (&fakeWrapper{t: t, profileRep: &WrapperReport{Failed: json.RawMessage(`{"code":"F1","reason":"apparmor_parser failed"}`)}})
|
||||
l2.Now = func() time.Time { return now.Add(4 * time.Hour) }
|
||||
if got := l2.ProfileRefresh(context.Background(), "agent start", nil, 1, time.Second); got != "failed" {
|
||||
t.Fatalf("outcome = %q", got)
|
||||
}
|
||||
b, _ = json.Marshal(l2.ProfileRefreshStanza())
|
||||
if !strings.Contains(string(b), `"last_ask":"failed","last_ask_at":"2026-11-14T12:30:11Z"`) || !strings.Contains(string(b), `"kernel":"7.0.14-28-pve"`) {
|
||||
if !strings.Contains(string(b), `"last_ask":"failed","last_ask_at":"2026-11-14T15:30:11Z"`) || !strings.Contains(string(b), `"kernel":"7.0.14-28-pve"`) {
|
||||
t.Fatalf("stanza after a failed ask = %s", b)
|
||||
}
|
||||
// a record that is not JSON, or huge, says nothing and breaks nothing
|
||||
// a record that is not JSON says nothing and breaks nothing
|
||||
_ = os.WriteFile(rec, []byte("not json"), 0o644)
|
||||
l3, _ := newLeg(t, &fakeWrapper{t: t}, &hub.WireOSUpdate{Ring: 1, Enabled: true})
|
||||
l3.ProfileRecordPath = rec
|
||||
|
||||
Reference in new issue
Block a user