carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "context"
5 "errors"
6 "net/http"
7 "strings"
8 "testing"
9 "time"
10
11 "github.com/carlosframework/platform/internal/objstore"
12 "github.com/carlosframework/platform/internal/release"
13)
14
15func scanURL(fx *consoleFixture, app string) string { return storeURL(fx, app) + "/scan" }
16
17// grantStore drives the real operator grant so the store is provisioned — the
18// state every scan verb but `request` needs to act on. The app's OWN store:
19// grantStoreIn (store_env_estate_test.go) is this for any store of the app,
20// and carries the implementation, so the two cannot drift.
21func grantStore(t *testing.T, fx *consoleFixture, token string) {
22 t.Helper()
23 grantStoreIn(t, fx, token, "")
24}
25
26// scanArmed is storeArmed plus a granted store and a working engine.
27func scanArmed(t *testing.T, code string) (*consoleFixture, *objstore.MemScanner, string) {
28 t.Helper()
29 fx, _, token, _ := storeArmed(t, code)
30 grantStore(t, fx, token)
31 sc := objstore.NewMemScanner()
32 fx.srv.cfg.StoreScanner = sc
33 enableDeploymentScanning(t, fx)
34 return fx, sc, token
35}
36
37// requestScanning is the member's half. An operator cannot enable an engine
38// without it — the attestation it carries is what says whether these objects
39// can be read at all.
40func requestScanning(t *testing.T, fx *consoleFixture, token string, encrypted bool) {
41 t.Helper()
42 if s, o := postJSON(t, scanURL(fx, "titogo"), map[string]any{"encrypted": encrypted}, token); s != http.StatusOK {
43 t.Fatalf("scan request = %d %v", s, o)
44 }
45}
46
47// The member gate runs FIRST: a stranger gets the same byte-identical 404 the
48// store doors give, never a 403 that would confirm the app exists.
49func TestScanDoorsGateOrder(t *testing.T) {
50 fx, _, token, _ := storeArmed(t, "code-scan-gate")
51 fx.srv.cfg.StoreOperators = []string{"eve@keymail.dev", "alice@keymail.dev"}
52 eve := cliToken(t, fx, "eve@keymail.dev", "code-scan-gate-eve")
53
54 for _, door := range []string{"", "/grant", "/revoke", "/sweep", "/enforce", "/callback-key/rotate"} {
55 t.Run("scan"+door, func(t *testing.T) {
56 // (a) an operator who is a member of nothing, against a real app.
57 statusA, bodyA := postRaw(t, scanURL(fx, "titogo")+door, map[string]any{}, eve)
58 // (b) a member, against an account that does not exist.
59 statusB, bodyB := postRaw(t, fx.ts.URL+"/api/cli/apps/zzzz/titogo/store/scan"+door, map[string]any{}, token)
60 // (c) a member of the account, against an app it never claimed.
61 statusC, bodyC := postRaw(t, scanURL(fx, "ghost-app")+door, map[string]any{}, token)
62 if statusA != http.StatusNotFound || statusB != http.StatusNotFound || statusC != http.StatusNotFound {
63 t.Fatalf("scan%s gate = %d / %d / %d, want 404 for all three", door, statusA, statusB, statusC)
64 }
65 if bodyA != bodyB || bodyB != bodyC {
66 t.Fatalf("scan%s refusals are not byte-identical:\n(a) %q\n(b) %q\n(c) %q", door, bodyA, bodyB, bodyC)
67 }
68 if bodyA != "404 page not found\n" {
69 t.Fatalf("scan%s refusal body = %q, want the stdlib http.NotFound output", door, bodyA)
70 }
71 })
72 }
73 if status, _ := postJSON(t, scanURL(fx, "titogo"), map[string]any{"encrypted": false}, ""); status != http.StatusUnauthorized {
74 t.Fatalf("scan request with no token = %d, want 401", status)
75 }
76}
77
78// grant and sweep 503 naming CARLOS_STORE_SCANNER when no engine is built.
79func TestScanGrantRequires503WithoutScanner(t *testing.T) {
80 fx, _, token, _ := storeArmed(t, "code-scan-noengine")
81 grantStore(t, fx, token)
82 fx.srv.cfg.StoreScanner = nil
83
84 for _, door := range []string{"/grant", "/sweep"} {
85 status, out := postJSON(t, scanURL(fx, "titogo")+door, map[string]any{}, token)
86 if status != http.StatusServiceUnavailable {
87 t.Fatalf("scan%s with no engine = %d %v, want 503", door, status, out)
88 }
89 if msg := str(t, out, "error"); !strings.Contains(msg, "CARLOS_CLAMD_ADDR") {
90 t.Fatalf("scan%s error = %q, want the wiring variable named", door, msg)
91 }
92 }
93 // enforce ON also needs one — the tag denied on comes from the engine.
94 status, out := postJSON(t, scanURL(fx, "titogo")+"/enforce", map[string]any{"on": true}, token)
95 if status != http.StatusServiceUnavailable {
96 t.Fatalf("enforce on with no engine = %d %v, want 503", status, out)
97 }
98}
99
100// request, revoke and enforce off work with NO engine built. Otherwise
101// unsetting the scanner on a deployment with an enforcing record would leave
102// the deny unclearable and the store unrepairable — the door that fixes it
103// refusing for the very reason it needs fixing.
104func TestScanRevokeAndEnforceOffWorkWithoutScanner(t *testing.T) {
105 fx, _, token := scanArmed(t, "code-scan-nodeadlock")
106 requestScanning(t, fx, token, false)
107
108 // Get into the bad state honestly: enable and enforce with an engine.
109 if status, out := postJSON(t, scanURL(fx, "titogo")+"/grant", map[string]any{}, token); status != http.StatusOK {
110 t.Fatalf("scan grant = %d %v", status, out)
111 }
112 if status, out := postJSON(t, scanURL(fx, "titogo")+"/enforce", map[string]any{"on": true}, token); status != http.StatusOK {
113 t.Fatalf("enforce on = %d %v", status, out)
114 }
115 if rec := storedRecord(t, fx, "titogo"); rec.Scanning == nil || rec.Scanning.Enforce == nil {
116 t.Fatal("setup: enforcement was never recorded")
117 }
118
119 // Now the engine goes away, exactly as unsetting the variable would do.
120 fx.srv.cfg.StoreScanner = nil
121
122 if status, out := postJSON(t, scanURL(fx, "titogo"), map[string]any{"encrypted": false}, token); status != http.StatusOK {
123 t.Fatalf("scan request with no engine = %d %v, want 200", status, out)
124 }
125 if status, out := postJSON(t, scanURL(fx, "titogo")+"/enforce", map[string]any{"on": false}, token); status != http.StatusOK {
126 t.Fatalf("enforce off with no engine = %d %v, want 200", status, out)
127 }
128 if rec := storedRecord(t, fx, "titogo"); rec.Scanning.Enforce != nil {
129 t.Fatal("enforce off left the deny standing on the record")
130 }
131 if status, out := postJSON(t, scanURL(fx, "titogo")+"/revoke", map[string]any{}, token); status != http.StatusOK {
132 t.Fatalf("revoke with no engine = %d %v, want 200", status, out)
133 }
134 if rec := storedRecord(t, fx, "titogo"); rec.Scanning.Enabled != nil {
135 t.Fatal("revoke left the engine recorded")
136 }
137}
138
139// Non-operators cannot grant, and the check runs after the member gate.
140func TestScanGrantRequiresOperator(t *testing.T) {
141 fx, _, token := scanArmed(t, "code-scan-operator")
142 fx.srv.cfg.StoreOperators = []string{"someone-else@keymail.dev"}
143
144 for _, door := range []string{"/grant", "/revoke", "/sweep", "/enforce"} {
145 status, out := postJSON(t, scanURL(fx, "titogo")+door, map[string]any{"on": false}, token)
146 if status != http.StatusForbidden {
147 t.Fatalf("scan%s as a non-operator = %d %v, want 403", door, status, out)
148 }
149 if msg := str(t, out, "error"); !strings.Contains(msg, "CARLOS_STORE_OPERATORS") {
150 t.Fatalf("scan%s error = %q, want the operator variable named", door, msg)
151 }
152 }
153 // The member's own verbs are unaffected: requesting and reading status are
154 // theirs, and always were.
155 if status, out := postJSON(t, scanURL(fx, "titogo"), map[string]any{"encrypted": false}, token); status != http.StatusOK {
156 t.Fatalf("scan request as a member = %d %v, want 200", status, out)
157 }
158 if status, out := getJSON(t, scanURL(fx, "titogo"), token); status != http.StatusOK {
159 t.Fatalf("scan status as a member = %d %v, want 200", status, out)
160 }
161}
162
163// A client-encrypted store refuses enforce: nothing will ever be tagged
164// infected, so it would be theatre — a record claiming a control that cannot
165// possibly act.
166func TestScanEnforceRefusedOnEncryptedStore(t *testing.T) {
167 fx, _, token := scanArmed(t, "code-scan-encrypted")
168
169 if status, out := postJSON(t, scanURL(fx, "titogo"), map[string]any{"encrypted": true}, token); status != http.StatusOK {
170 t.Fatalf("scan request = %d %v", status, out)
171 }
172 if status, out := postJSON(t, scanURL(fx, "titogo")+"/grant", map[string]any{}, token); status != http.StatusOK {
173 t.Fatalf("scan grant = %d %v", status, out)
174 }
175
176 status, out := postJSON(t, scanURL(fx, "titogo")+"/enforce", map[string]any{"on": true}, token)
177 if status != http.StatusConflict {
178 t.Fatalf("enforce on an encrypted store = %d %v, want 409", status, out)
179 }
180 if msg := str(t, out, "error"); !strings.Contains(msg, "encrypted") {
181 t.Fatalf("error = %q, want it to say why", msg)
182 }
183 if rec := storedRecord(t, fx, "titogo"); rec.Scanning.Enforce != nil {
184 t.Fatal("a refused enforce still wrote the deny")
185 }
186}
187
188// The attestation has no default. "Are these objects readable" is the question
189// the whole coverage claim rests on, and neither answer is safe to assume.
190func TestScanRequestRequiresAttestation(t *testing.T) {
191 fx, _, token := scanArmed(t, "code-scan-attest")
192
193 status, out := postJSON(t, scanURL(fx, "titogo"), map[string]any{}, token)
194 if status != http.StatusBadRequest {
195 t.Fatalf("request with no attestation = %d %v, want 400", status, out)
196 }
197 if msg := str(t, out, "error"); !strings.Contains(msg, "--client-encrypted") {
198 t.Fatalf("error = %q, want it to name the flags", msg)
199 }
200 if rec := storedRecord(t, fx, "titogo"); rec.Scanning != nil && rec.Scanning.Requested != nil {
201 t.Fatal("a refused request still recorded an attestation")
202 }
203}
204
205// Enforcement actually reaches the app's IAM policy, not just the record. A
206// record that said enforcing while the live policy allowed reads would be
207// worse than never having offered the switch.
208func TestScanEnforceWritesTheDenyToThePolicy(t *testing.T) {
209 fx, fake, token, _ := storeArmed(t, "code-scan-policy")
210 grantStore(t, fx, token)
211 fx.srv.cfg.StoreScanner = objstore.NewMemScanner()
212 enableDeploymentScanning(t, fx)
213 requestScanning(t, fx, token, false)
214
215 if status, out := postJSON(t, scanURL(fx, "titogo")+"/grant", map[string]any{}, token); status != http.StatusOK {
216 t.Fatalf("scan grant = %d %v", status, out)
217 }
218 user := objstore.UserName(fx.sqid, "titogo")
219
220 // The ENFORCEMENT deny specifically, which is the one keyed on the verdict
221 // tag. Every policy also carries the two anti-forgery Denys, unconditional
222 // and unrelated to this switch, so `"Effect":"Deny"` — what this test used
223 // to look for — no longer tells the two states apart. It would now report
224 // enforcement on from the moment the store was granted.
225 const enforcementDeny = "s3:ExistingObjectTag"
226
227 if strings.Contains(fake.PolicyFor(user), enforcementDeny) {
228 t.Fatal("enabling the engine alone wrote an enforcement deny — enforcement is a separate switch")
229 }
230
231 if status, out := postJSON(t, scanURL(fx, "titogo")+"/enforce", map[string]any{"on": true}, token); status != http.StatusOK {
232 t.Fatalf("enforce on = %d %v", status, out)
233 }
234 if !strings.Contains(fake.PolicyFor(user), enforcementDeny) {
235 t.Fatalf("enforce on never reached the policy:\n%s", fake.PolicyFor(user))
236 }
237
238 if status, out := postJSON(t, scanURL(fx, "titogo")+"/enforce", map[string]any{"on": false}, token); status != http.StatusOK {
239 t.Fatalf("enforce off = %d %v", status, out)
240 }
241 if strings.Contains(fake.PolicyFor(user), enforcementDeny) {
242 t.Fatalf("enforce off left a live enforcement deny standing:\n%s", fake.PolicyFor(user))
243 }
244 // And turning enforcement off must not have taken the anti-forgery Denys
245 // with it: a store that has stopped denying infected reads is exactly
246 // where a forged clean tag would sit unnoticed longest.
247 if !strings.Contains(fake.PolicyFor(user), "s3:RequestObjectTagKeys") {
248 t.Fatalf("enforce off dropped the anti-forgery denys:\n%s", fake.PolicyFor(user))
249 }
250}
251
252// Enforcement before an engine is enabled is refused. Otherwise a store could
253// carry a Deny keyed on a tag nothing will ever write, which denies nothing
254// and reads in the record as protection.
255func TestScanEnforceRefusedBeforeGrant(t *testing.T) {
256 fx, _, token := scanArmed(t, "code-scan-enforce-early")
257
258 status, out := postJSON(t, scanURL(fx, "titogo")+"/enforce", map[string]any{"on": true}, token)
259 if status != http.StatusConflict {
260 t.Fatalf("enforce before scan grant = %d %v, want 409", status, out)
261 }
262}
263
264// Status reports the engine and its signature database. A clean verdict from
265// three-month-old signatures is not the same claim as a fresh one.
266func TestScanStatusCarriesProvenance(t *testing.T) {
267 fx, _, token := scanArmed(t, "code-scan-status")
268
269 status, out := getJSON(t, scanURL(fx, "titogo"), token)
270 if status != http.StatusOK {
271 t.Fatalf("scan status = %d %v", status, out)
272 }
273 if got := str(t, out, "engine"); got != "clamav" {
274 t.Fatalf("engine = %q", got)
275 }
276 if str(t, out, "signature") == "" {
277 t.Fatal("status carries no signature-database version")
278 }
279}
280
281// `carlos store status` carries the scan block once it exists. Additive only:
282// release.PurgeApp parses this same object with its own struct and freezes the
283// grant and provisioned names, so a new key is safe and a renamed one is a
284// purge that stops working.
285func TestStoreStatusCarriesScanningBlock(t *testing.T) {
286 fx, _, token := scanArmed(t, "code-store-status-scan")
287
288 status, out := getJSON(t, storeURL(fx, "titogo"), token)
289 if status != http.StatusOK {
290 t.Fatalf("store status = %d %v", status, out)
291 }
292 if _, present := out["scanning"]; !present {
293 t.Fatal("store status carries no scanning key at all")
294 }
295 if out["scanning"] != nil {
296 t.Fatalf("a store nobody requested scanning for reported %v", out["scanning"])
297 }
298
299 if s, o := postJSON(t, scanURL(fx, "titogo"), map[string]any{"encrypted": false}, token); s != http.StatusOK {
300 t.Fatalf("scan request = %d %v", s, o)
301 }
302 if s, o := postJSON(t, scanURL(fx, "titogo")+"/grant", map[string]any{}, token); s != http.StatusOK {
303 t.Fatalf("scan grant = %d %v", s, o)
304 }
305
306 status, out = getJSON(t, storeURL(fx, "titogo"), token)
307 if status != http.StatusOK {
308 t.Fatalf("store status = %d %v", status, out)
309 }
310 sc, ok := out["scanning"].(map[string]any)
311 if !ok {
312 t.Fatalf("scanning = %#v, want an object", out["scanning"])
313 }
314 if _, ok := sc["enabled"]; !ok {
315 t.Fatalf("scanning block does not report the enabled engine: %v", sc)
316 }
317 // The grant and provisioned names PurgeApp parses must still be there and
318 // still be spelled the same.
319 for _, frozen := range []string{"grant", "provisioned", "declared"} {
320 if _, present := out[frozen]; !present {
321 t.Fatalf("the %q key PurgeApp parses is gone from the reply", frozen)
322 }
323 }
324}
325
326// Enabling an engine requires the MEMBER's request, and specifically their
327// attestation. Without one, Requested stays nil — and nil reads as "not
328// encrypted" everywhere downstream, so a client-encrypted store nobody
329// attested would be swept and reported entirely clean. It also skips the
330// consent point: this engine reads tenant object bytes.
331func TestScanGrantRefusedWithoutMemberRequest(t *testing.T) {
332 fx, _, token := scanArmed(t, "code-scan-no-request")
333
334 status, out := postJSON(t, scanURL(fx, "titogo")+"/grant", map[string]any{}, token)
335 if status != http.StatusConflict {
336 t.Fatalf("scan grant with no member request = %d %v, want 409", status, out)
337 }
338 if msg := str(t, out, "error"); !strings.Contains(msg, "scan request") {
339 t.Fatalf("error = %q, want it to name the verb the member runs", msg)
340 }
341 if rec := storedRecord(t, fx, "titogo"); rec.Scanning != nil && rec.Scanning.Enabled != nil {
342 t.Fatal("a refused grant still enabled the engine")
343 }
344
345 // With the attestation on record it goes through.
346 if s, o := postJSON(t, scanURL(fx, "titogo"), map[string]any{"encrypted": false}, token); s != http.StatusOK {
347 t.Fatalf("scan request = %d %v", s, o)
348 }
349 if s, o := postJSON(t, scanURL(fx, "titogo")+"/grant", map[string]any{}, token); s != http.StatusOK {
350 t.Fatalf("scan grant after a request = %d %v, want 200", s, o)
351 }
352}
353
354// Status says so when the record names an engine this deployment no longer
355// has. Silence there reads as health: the reply would carry the engine, the
356// enforcement state and the coverage, and never mention that nothing runs.
357func TestScanStatusReportsMissingEngineOnEnabledRecord(t *testing.T) {
358 fx, _, token := scanArmed(t, "code-scan-engine-gone")
359 if s, o := postJSON(t, scanURL(fx, "titogo"), map[string]any{"encrypted": false}, token); s != http.StatusOK {
360 t.Fatalf("scan request = %d %v", s, o)
361 }
362 if s, o := postJSON(t, scanURL(fx, "titogo")+"/grant", map[string]any{}, token); s != http.StatusOK {
363 t.Fatalf("scan grant = %d %v", s, o)
364 }
365 fx.srv.cfg.StoreScanner = nil
366
367 status, out := getJSON(t, scanURL(fx, "titogo"), token)
368 if status != http.StatusOK {
369 t.Fatalf("scan status = %d %v", status, out)
370 }
371 msg, _ := out["engine_error"].(string)
372 if !strings.Contains(msg, "CARLOS_CLAMD_ADDR") {
373 t.Fatalf("engine_error = %q, want it to say no engine is configured", msg)
374 }
375}
376
377// `disable` has to disable everywhere, or it disables nothing.
378//
379// The hourly pass going quiet is not the same as scanning being off: the sweep
380// verb and the grant door reach the engine by their own paths. Without these
381// checks an operator could disable scanning, watch the tick stop, and still
382// have `carlos store scan sweep` read tenant bytes.
383func TestDeploymentDisableStopsEveryPath(t *testing.T) {
384 fx, _, token := scanArmed(t, "code-scan-disabled")
385 requestScanning(t, fx, token, false)
386
387 if s, o := postJSON(t, scanURL(fx, "titogo")+"/grant", map[string]any{}, token); s != http.StatusOK {
388 t.Fatalf("setup grant = %d %v", s, o)
389 }
390
391 // Now switch the deployment off, the way `carlos system antivirus disable`
392 // will. The engine stays wired — this is policy, not wiring.
393 disableDeploymentScanning(t, fx)
394
395 for _, door := range []string{"/grant", "/sweep"} {
396 status, out := postJSON(t, scanURL(fx, "titogo")+door, map[string]any{}, token)
397 if status != http.StatusServiceUnavailable {
398 t.Fatalf("scan%s while disabled = %d %v, want 503", door, status, out)
399 }
400 msg := str(t, out, "error")
401 if !strings.Contains(msg, "carlos system antivirus enable") {
402 t.Fatalf("scan%s error = %q, want it to name the command that fixes it", door, msg)
403 }
404 // And NOT the wiring variable: an operator whose engine is wired and
405 // whose record is off must not be sent to edit a unit file.
406 if strings.Contains(msg, "CARLOS_CLAMD_ADDR") {
407 t.Fatalf("scan%s conflated a disabled record with missing wiring: %q", door, msg)
408 }
409 }
410}
411
412// The two 503s stay distinct. Wired-but-off and not-wired want different
413// actions, and a single message would send someone to the wrong one.
414func TestDisabledAndUnwiredAreDifferent503s(t *testing.T) {
415 fx, _, token := scanArmed(t, "code-scan-two-503s")
416 requestScanning(t, fx, token, false)
417
418 disableDeploymentScanning(t, fx)
419 _, offBody := postJSON(t, scanURL(fx, "titogo")+"/sweep", map[string]any{}, token)
420 offMsg := str(t, offBody, "error")
421
422 enableDeploymentScanning(t, fx)
423 fx.srv.cfg.StoreScanner = nil
424 _, unwiredBody := postJSON(t, scanURL(fx, "titogo")+"/sweep", map[string]any{}, token)
425 unwiredMsg := str(t, unwiredBody, "error")
426
427 if offMsg == unwiredMsg {
428 t.Fatalf("both states answer identically: %q", offMsg)
429 }
430 if !strings.Contains(unwiredMsg, "CARLOS_CLAMD_ADDR") {
431 t.Fatalf("the unwired message does not name the wiring: %q", unwiredMsg)
432 }
433}
434
435// RunSweep itself refuses, not merely the door in front of it. The sweep verb
436// is not the only caller, and a check that lived only in a handler would be a
437// check the next caller forgets.
438func TestRunSweepRefusesWhenDeploymentDisabled(t *testing.T) {
439 ctx := context.Background()
440 fx := newConsoleFixture(t)
441 fx.srv.cfg.StoreScanner = objstore.NewMemScanner()
442 enableStoreRecordOnly(t, fx, fixtureAcct, fixtureApp)
443 // No deployment record at all: absent means not enabled.
444
445 _, err := objstore.RunSweep(ctx, fx.rel, &countingLister{keys: []string{"a.txt"}},
446 objstore.NewMemScanner(), fixtureAcct, fixtureApp, time.Minute)
447 if !errors.Is(err, objstore.ErrScanningDisabled) {
448 t.Fatalf("RunSweep with no deployment record = %v, want ErrScanningDisabled", err)
449 }
450}
451
452// The member's own verbs keep working while scanning is disabled. Otherwise
453// disabling would strand every enforcing record, with the door that clears it
454// refusing for the very reason it needs using.
455func TestMemberVerbsSurviveDeploymentDisable(t *testing.T) {
456 fx, _, token := scanArmed(t, "code-scan-disabled-member")
457 requestScanning(t, fx, token, false)
458 if s, o := postJSON(t, scanURL(fx, "titogo")+"/grant", map[string]any{}, token); s != http.StatusOK {
459 t.Fatalf("setup grant = %d %v", s, o)
460 }
461 if s, o := postJSON(t, scanURL(fx, "titogo")+"/enforce", map[string]any{"on": true}, token); s != http.StatusOK {
462 t.Fatalf("setup enforce = %d %v", s, o)
463 }
464
465 disableDeploymentScanning(t, fx)
466
467 if s, o := postJSON(t, scanURL(fx, "titogo"), map[string]any{"encrypted": false}, token); s != http.StatusOK {
468 t.Fatalf("request while disabled = %d %v", s, o)
469 }
470 if s, o := postJSON(t, scanURL(fx, "titogo")+"/enforce", map[string]any{"on": false}, token); s != http.StatusOK {
471 t.Fatalf("enforce off while disabled = %d %v", s, o)
472 }
473 if s, o := postJSON(t, scanURL(fx, "titogo")+"/revoke", map[string]any{}, token); s != http.StatusOK {
474 t.Fatalf("revoke while disabled = %d %v", s, o)
475 }
476 if rec := storedRecord(t, fx, "titogo"); rec.Scanning.Enforce != nil {
477 t.Fatal("enforce off left a live deny while scanning was disabled")
478 }
479}
480
481// disableDeploymentScanning flips the switch off, leaving the engine wired.
482func disableDeploymentScanning(t *testing.T, fx *consoleFixture) {
483 t.Helper()
484 ctx := context.Background()
485 av, etag, err := release.GetAntivirus(ctx, fx.rel)
486 if err != nil {
487 t.Fatal(err)
488 }
489 if av == nil {
490 av = &release.AntivirusSettings{Engine: "clamav"}
491 }
492 av.Enabled = false
493 av.At = time.Now().UTC()
494 av.By = "test"
495 if _, err := release.PutAntivirus(ctx, fx.rel, av, etag); err != nil {
496 t.Fatal(err)
497 }
498}
499
500// A per-app record saying `enabled: clamav` while the deployment switch is off
501// describes a control that is not running. `carlos store scan status` prints
502// this reply, so omitting the deployment half is a new way for the status
503// output to lie — the same lie the engine_error branch already refuses to tell
504// about the wiring half.
505func TestStoreScanStatusReportsDeploymentState(t *testing.T) {
506 ctx := context.Background()
507 fx, _, token := scanArmed(t, "code-scan-depstate")
508 requestScanning(t, fx, token, false)
509 if s, o := postJSON(t, scanURL(fx, "titogo")+"/grant", map[string]any{}, token); s != http.StatusOK {
510 t.Fatalf("grant = %d %v", s, o)
511 }
512
513 status, out := getJSON(t, scanURL(fx, "titogo"), token)
514 if status != http.StatusOK {
515 t.Fatalf("status = %d %v", status, out)
516 }
517 if on, _ := out["deployment_enabled"].(bool); !on {
518 t.Fatalf("deployment_enabled = %v with the switch on", out["deployment_enabled"])
519 }
520 if _, bad := out["deployment_error"]; bad {
521 t.Fatalf("a healthy deployment reported %v", out["deployment_error"])
522 }
523
524 // Now turn the deployment off underneath an app whose own record still
525 // says enabled. Nothing sweeps it, and the status has to say so.
526 av, etag, err := release.GetAntivirus(ctx, fx.rel)
527 if err != nil || av == nil {
528 t.Fatalf("record = %+v, %v", av, err)
529 }
530 av.Enabled = false
531 if _, err := release.PutAntivirus(ctx, fx.rel, av, etag); err != nil {
532 t.Fatal(err)
533 }
534
535 _, out = getJSON(t, scanURL(fx, "titogo"), token)
536 if on, _ := out["deployment_enabled"].(bool); on {
537 t.Fatal("deployment_enabled stayed true after a disable")
538 }
539 msg := str(t, out, "deployment_error")
540 if !strings.Contains(msg, "carlos system antivirus enable") {
541 t.Fatalf("deployment_error = %q, want the command that fixes it", msg)
542 }
543 // The per-app record is UNCHANGED — the two facts are reported
544 // separately, never collapsed.
545 if out["scanning"] == nil {
546 t.Fatal("the per-app record vanished from the reply")
547 }
548}
549
550func TestStoreScanStatusReportsQueueDepth(t *testing.T) {
551 fx, _, token := scanArmed(t, "code-scan-queue-depth")
552 requestScanning(t, fx, token, false)
553 if _, err := objstore.Enqueue(context.Background(), fx.rel, fx.srv.scanQueue,
554 fixtureAcct, "titogo", &objstore.QueueItem{Key: "uploads/a.pdf", By: "test"}, time.Now()); err != nil {
555 t.Fatal(err)
556 }
557
558 status, out := getJSON(t, scanURL(fx, "titogo"), token)
559 if status != http.StatusOK {
560 t.Fatalf("status = %d %v", status, out)
561 }
562 if got := int(out["queue_depth"].(float64)); got != 1 {
563 t.Fatalf("queue_depth = %d, want 1", got)
564 }
565}
566