| 1 | package console |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "net/http" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | "time" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/objstore" |
| 11 | "github.com/carlosframework/platform/internal/release" |
| 12 | ) |
| 13 | |
| 14 | const systemAntivirusURL = "/api/cli/system/antivirus" |
| 15 | |
| 16 | // systemArmed is a console with an engine wired and an operator who may act. |
| 17 | func systemArmed(t *testing.T, code string) (*consoleFixture, string) { |
| 18 | t.Helper() |
| 19 | fx, _, token, _ := storeArmed(t, code) |
| 20 | fx.srv.cfg.StoreScanner = objstore.NewMemScanner() |
| 21 | return fx, token |
| 22 | } |
| 23 | |
| 24 | // Authority here is an ADDRESS, not a role. Admin reaches almost everything; |
| 25 | // enabling deployment-wide scanning starts reading tenant bytes across every |
| 26 | // store, and that is the deployment owner's call. |
| 27 | func TestSystemAntivirusNeedsOperator(t *testing.T) { |
| 28 | fx, token := systemArmed(t, "code-sys-operator") |
| 29 | fx.srv.cfg.StoreOperators = []string{"someone-else@keymail.dev"} |
| 30 | |
| 31 | status, out := postJSON(t, fx.ts.URL+systemAntivirusURL, map[string]any{"enabled": true}, token) |
| 32 | if status != http.StatusForbidden { |
| 33 | t.Fatalf("enable as a non-operator = %d %v, want 403", status, out) |
| 34 | } |
| 35 | if msg := str(t, out, "error"); !strings.Contains(msg, "CARLOS_STORE_OPERATORS") { |
| 36 | t.Fatalf("error = %q, want the operator list named", msg) |
| 37 | } |
| 38 | |
| 39 | status, out = getJSON(t, fx.ts.URL+systemAntivirusURL, token) |
| 40 | if status != http.StatusForbidden { |
| 41 | t.Fatalf("status as a non-operator = %d %v, want 403", status, out) |
| 42 | } |
| 43 | } |
| 44 | |
| 45 | // Enabling with no engine wired would record a control that cannot run, and |
| 46 | // `scan status` would then report health for a deployment where no sweep can |
| 47 | // happen — the class of lie this whole feature is careful about. |
| 48 | func TestSystemAntivirusEnableRefusesWithoutScanner(t *testing.T) { |
| 49 | fx, token := systemArmed(t, "code-sys-nowire") |
| 50 | fx.srv.cfg.StoreScanner = nil |
| 51 | |
| 52 | status, out := postJSON(t, fx.ts.URL+systemAntivirusURL, map[string]any{"enabled": true}, token) |
| 53 | if status != http.StatusServiceUnavailable { |
| 54 | t.Fatalf("enable with no engine = %d %v, want 503", status, out) |
| 55 | } |
| 56 | if msg := str(t, out, "error"); !strings.Contains(msg, "CARLOS_CLAMD_ADDR") { |
| 57 | t.Fatalf("error = %q, want the wiring variable named", msg) |
| 58 | } |
| 59 | if av, _, err := release.GetAntivirus(context.Background(), fx.rel); err != nil || av != nil { |
| 60 | t.Fatalf("a refused enable still wrote a record: %+v, %v", av, err) |
| 61 | } |
| 62 | |
| 63 | // Disabling is always allowed, engine or not: it is the way out of a bad |
| 64 | // state, and a door that refused would strand whoever needed it. |
| 65 | if s, o := postJSON(t, fx.ts.URL+systemAntivirusURL, map[string]any{"enabled": false}, token); s != http.StatusOK { |
| 66 | t.Fatalf("disable with no engine = %d %v, want 200", s, o) |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | // Wiring and policy are reported as separate facts, because they fail |
| 71 | // separately. Collapsing them into one "healthy" is how an operator ends up |
| 72 | // debugging the wrong half. |
| 73 | func TestSystemAntivirusStatusSeparatesWiringFromPolicy(t *testing.T) { |
| 74 | fx, token := systemArmed(t, "code-sys-status") |
| 75 | |
| 76 | // Wired, no record: not enabled, and the reply says which is which. |
| 77 | status, out := getJSON(t, fx.ts.URL+systemAntivirusURL, token) |
| 78 | if status != http.StatusOK { |
| 79 | t.Fatalf("status = %d %v", status, out) |
| 80 | } |
| 81 | if wired, _ := out["wired"].(bool); !wired { |
| 82 | t.Fatalf("wired = %v, want true", out["wired"]) |
| 83 | } |
| 84 | if enabled, _ := out["enabled"].(bool); enabled { |
| 85 | t.Fatalf("enabled = %v with no record, want false", out["enabled"]) |
| 86 | } |
| 87 | if str(t, out, "signature") == "" { |
| 88 | t.Fatal("a wired engine reported no signature database") |
| 89 | } |
| 90 | |
| 91 | // Enabled, engine gone: still enabled, but the engine error is visible. |
| 92 | if s, o := postJSON(t, fx.ts.URL+systemAntivirusURL, map[string]any{"enabled": true}, token); s != http.StatusOK { |
| 93 | t.Fatalf("enable = %d %v", s, o) |
| 94 | } |
| 95 | fx.srv.cfg.StoreScanner = nil |
| 96 | _, out = getJSON(t, fx.ts.URL+systemAntivirusURL, token) |
| 97 | if enabled, _ := out["enabled"].(bool); !enabled { |
| 98 | t.Fatal("unwiring the engine changed the recorded policy") |
| 99 | } |
| 100 | if wired, _ := out["wired"].(bool); wired { |
| 101 | t.Fatal("wired stayed true with no scanner") |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | func TestSystemAntivirusStatusReportsQueueAppsWithWork(t *testing.T) { |
| 106 | fx, token := systemArmed(t, "code-sys-queue-depth") |
| 107 | fx.srv.scanQueue.Bump(objstore.AppRef{Account: fixtureAcct, App: fixtureApp}) |
| 108 | fx.srv.scanQueue.Bump(objstore.AppRef{Account: fixtureAcct, App: "second"}) |
| 109 | |
| 110 | status, out := getJSON(t, fx.ts.URL+systemAntivirusURL, token) |
| 111 | if status != http.StatusOK { |
| 112 | t.Fatalf("status = %d %v", status, out) |
| 113 | } |
| 114 | if got := int(out["queue_apps_with_work"].(float64)); got != 2 { |
| 115 | t.Fatalf("queue_apps_with_work = %d, want 2", got) |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | // The record is the audit trail: who turned it on, and when. |
| 120 | func TestSystemAntivirusRecordsWhoAndWhen(t *testing.T) { |
| 121 | fx, token := systemArmed(t, "code-sys-audit") |
| 122 | before := time.Now().Add(-time.Second) |
| 123 | |
| 124 | if s, o := postJSON(t, fx.ts.URL+systemAntivirusURL, map[string]any{"enabled": true}, token); s != http.StatusOK { |
| 125 | t.Fatalf("enable = %d %v", s, o) |
| 126 | } |
| 127 | av, _, err := release.GetAntivirus(context.Background(), fx.rel) |
| 128 | if err != nil || av == nil { |
| 129 | t.Fatalf("record = %+v, %v", av, err) |
| 130 | } |
| 131 | if av.By != "alice@keymail.dev" { |
| 132 | t.Fatalf("by = %q, want the operator who did it", av.By) |
| 133 | } |
| 134 | if av.At.Before(before) { |
| 135 | t.Fatalf("at = %v, not stamped", av.At) |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | // A body that says neither is a 400, not a guess. There is no safe default |
| 140 | // for a switch that decides whether tenant bytes get read. |
| 141 | func TestSystemAntivirusNeedsAnExplicitState(t *testing.T) { |
| 142 | fx, token := systemArmed(t, "code-sys-explicit") |
| 143 | |
| 144 | status, out := postJSON(t, fx.ts.URL+systemAntivirusURL, map[string]any{}, token) |
| 145 | if status != http.StatusBadRequest { |
| 146 | t.Fatalf("empty body = %d %v, want 400", status, out) |
| 147 | } |
| 148 | } |
| 149 | |
| 150 | // A record this build cannot parse already stops every sweep — so `disable` |
| 151 | // refusing over it strands the operator in a state no door can leave. The one |
| 152 | // verb whose whole job is "stop" must not refuse for the reason it is needed, |
| 153 | // which is the same argument apiscan.go makes for revoke and enforce-off. |
| 154 | // |
| 155 | // `enable` still refuses: repairing by overwriting is a way OUT of a bad |
| 156 | // state, never a way into an enabled one. |
| 157 | func TestSystemAntivirusDisableRepairsAnUnreadableRecord(t *testing.T) { |
| 158 | ctx := context.Background() |
| 159 | fx, token := systemArmed(t, "code-sys-repair") |
| 160 | |
| 161 | if _, err := fx.rel.Put(ctx, release.SystemKey(release.AntivirusName), |
| 162 | []byte(`{"schema":99,"enabled":true}`), ""); err != nil { |
| 163 | t.Fatal(err) |
| 164 | } |
| 165 | |
| 166 | if s, o := postJSON(t, fx.ts.URL+systemAntivirusURL, map[string]any{"enabled": true}, token); s != http.StatusBadGateway { |
| 167 | t.Fatalf("enable over an unreadable record = %d %v, want 502 — enabling must never guess", s, o) |
| 168 | } |
| 169 | if s, o := postJSON(t, fx.ts.URL+systemAntivirusURL, map[string]any{"enabled": false}, token); s != http.StatusOK { |
| 170 | t.Fatalf("disable over an unreadable record = %d %v, want 200", s, o) |
| 171 | } |
| 172 | av, _, err := release.GetAntivirus(ctx, fx.rel) |
| 173 | if err != nil || av == nil || av.Enabled { |
| 174 | t.Fatalf("record after the repair = %+v, %v", av, err) |
| 175 | } |
| 176 | if av.By != "alice@keymail.dev" { |
| 177 | t.Fatalf("by = %q, want the operator who repaired it", av.By) |
| 178 | } |
| 179 | } |
| 180 | |