package console import ( "context" "net/http" "strings" "testing" "time" "github.com/carlosframework/platform/internal/objstore" "github.com/carlosframework/platform/internal/release" ) const systemAntivirusURL = "/api/cli/system/antivirus" // systemArmed is a console with an engine wired and an operator who may act. func systemArmed(t *testing.T, code string) (*consoleFixture, string) { t.Helper() fx, _, token, _ := storeArmed(t, code) fx.srv.cfg.StoreScanner = objstore.NewMemScanner() return fx, token } // Authority here is an ADDRESS, not a role. Admin reaches almost everything; // enabling deployment-wide scanning starts reading tenant bytes across every // store, and that is the deployment owner's call. func TestSystemAntivirusNeedsOperator(t *testing.T) { fx, token := systemArmed(t, "code-sys-operator") fx.srv.cfg.StoreOperators = []string{"someone-else@keymail.dev"} status, out := postJSON(t, fx.ts.URL+systemAntivirusURL, map[string]any{"enabled": true}, token) if status != http.StatusForbidden { t.Fatalf("enable as a non-operator = %d %v, want 403", status, out) } if msg := str(t, out, "error"); !strings.Contains(msg, "CARLOS_STORE_OPERATORS") { t.Fatalf("error = %q, want the operator list named", msg) } status, out = getJSON(t, fx.ts.URL+systemAntivirusURL, token) if status != http.StatusForbidden { t.Fatalf("status as a non-operator = %d %v, want 403", status, out) } } // Enabling with no engine wired would record a control that cannot run, and // `scan status` would then report health for a deployment where no sweep can // happen — the class of lie this whole feature is careful about. func TestSystemAntivirusEnableRefusesWithoutScanner(t *testing.T) { fx, token := systemArmed(t, "code-sys-nowire") fx.srv.cfg.StoreScanner = nil status, out := postJSON(t, fx.ts.URL+systemAntivirusURL, map[string]any{"enabled": true}, token) if status != http.StatusServiceUnavailable { t.Fatalf("enable with no engine = %d %v, want 503", status, out) } if msg := str(t, out, "error"); !strings.Contains(msg, "CARLOS_CLAMD_ADDR") { t.Fatalf("error = %q, want the wiring variable named", msg) } if av, _, err := release.GetAntivirus(context.Background(), fx.rel); err != nil || av != nil { t.Fatalf("a refused enable still wrote a record: %+v, %v", av, err) } // Disabling is always allowed, engine or not: it is the way out of a bad // state, and a door that refused would strand whoever needed it. if s, o := postJSON(t, fx.ts.URL+systemAntivirusURL, map[string]any{"enabled": false}, token); s != http.StatusOK { t.Fatalf("disable with no engine = %d %v, want 200", s, o) } } // Wiring and policy are reported as separate facts, because they fail // separately. Collapsing them into one "healthy" is how an operator ends up // debugging the wrong half. func TestSystemAntivirusStatusSeparatesWiringFromPolicy(t *testing.T) { fx, token := systemArmed(t, "code-sys-status") // Wired, no record: not enabled, and the reply says which is which. status, out := getJSON(t, fx.ts.URL+systemAntivirusURL, token) if status != http.StatusOK { t.Fatalf("status = %d %v", status, out) } if wired, _ := out["wired"].(bool); !wired { t.Fatalf("wired = %v, want true", out["wired"]) } if enabled, _ := out["enabled"].(bool); enabled { t.Fatalf("enabled = %v with no record, want false", out["enabled"]) } if str(t, out, "signature") == "" { t.Fatal("a wired engine reported no signature database") } // Enabled, engine gone: still enabled, but the engine error is visible. if s, o := postJSON(t, fx.ts.URL+systemAntivirusURL, map[string]any{"enabled": true}, token); s != http.StatusOK { t.Fatalf("enable = %d %v", s, o) } fx.srv.cfg.StoreScanner = nil _, out = getJSON(t, fx.ts.URL+systemAntivirusURL, token) if enabled, _ := out["enabled"].(bool); !enabled { t.Fatal("unwiring the engine changed the recorded policy") } if wired, _ := out["wired"].(bool); wired { t.Fatal("wired stayed true with no scanner") } } func TestSystemAntivirusStatusReportsQueueAppsWithWork(t *testing.T) { fx, token := systemArmed(t, "code-sys-queue-depth") fx.srv.scanQueue.Bump(objstore.AppRef{Account: fixtureAcct, App: fixtureApp}) fx.srv.scanQueue.Bump(objstore.AppRef{Account: fixtureAcct, App: "second"}) status, out := getJSON(t, fx.ts.URL+systemAntivirusURL, token) if status != http.StatusOK { t.Fatalf("status = %d %v", status, out) } if got := int(out["queue_apps_with_work"].(float64)); got != 2 { t.Fatalf("queue_apps_with_work = %d, want 2", got) } } // The record is the audit trail: who turned it on, and when. func TestSystemAntivirusRecordsWhoAndWhen(t *testing.T) { fx, token := systemArmed(t, "code-sys-audit") before := time.Now().Add(-time.Second) if s, o := postJSON(t, fx.ts.URL+systemAntivirusURL, map[string]any{"enabled": true}, token); s != http.StatusOK { t.Fatalf("enable = %d %v", s, o) } av, _, err := release.GetAntivirus(context.Background(), fx.rel) if err != nil || av == nil { t.Fatalf("record = %+v, %v", av, err) } if av.By != "alice@keymail.dev" { t.Fatalf("by = %q, want the operator who did it", av.By) } if av.At.Before(before) { t.Fatalf("at = %v, not stamped", av.At) } } // A body that says neither is a 400, not a guess. There is no safe default // for a switch that decides whether tenant bytes get read. func TestSystemAntivirusNeedsAnExplicitState(t *testing.T) { fx, token := systemArmed(t, "code-sys-explicit") status, out := postJSON(t, fx.ts.URL+systemAntivirusURL, map[string]any{}, token) if status != http.StatusBadRequest { t.Fatalf("empty body = %d %v, want 400", status, out) } } // A record this build cannot parse already stops every sweep — so `disable` // refusing over it strands the operator in a state no door can leave. The one // verb whose whole job is "stop" must not refuse for the reason it is needed, // which is the same argument apiscan.go makes for revoke and enforce-off. // // `enable` still refuses: repairing by overwriting is a way OUT of a bad // state, never a way into an enabled one. func TestSystemAntivirusDisableRepairsAnUnreadableRecord(t *testing.T) { ctx := context.Background() fx, token := systemArmed(t, "code-sys-repair") if _, err := fx.rel.Put(ctx, release.SystemKey(release.AntivirusName), []byte(`{"schema":99,"enabled":true}`), ""); err != nil { t.Fatal(err) } if s, o := postJSON(t, fx.ts.URL+systemAntivirusURL, map[string]any{"enabled": true}, token); s != http.StatusBadGateway { t.Fatalf("enable over an unreadable record = %d %v, want 502 — enabling must never guess", s, o) } if s, o := postJSON(t, fx.ts.URL+systemAntivirusURL, map[string]any{"enabled": false}, token); s != http.StatusOK { t.Fatalf("disable over an unreadable record = %d %v, want 200", s, o) } av, _, err := release.GetAntivirus(ctx, fx.rel) if err != nil || av == nil || av.Enabled { t.Fatalf("record after the repair = %+v, %v", av, err) } if av.By != "alice@keymail.dev" { t.Fatalf("by = %q, want the operator who repaired it", av.By) } }