package console import ( "context" "errors" "net/http" "strings" "testing" "time" "github.com/carlosframework/platform/internal/objstore" "github.com/carlosframework/platform/internal/release" ) func scanURL(fx *consoleFixture, app string) string { return storeURL(fx, app) + "/scan" } // grantStore drives the real operator grant so the store is provisioned — the // state every scan verb but `request` needs to act on. The app's OWN store: // grantStoreIn (store_env_estate_test.go) is this for any store of the app, // and carries the implementation, so the two cannot drift. func grantStore(t *testing.T, fx *consoleFixture, token string) { t.Helper() grantStoreIn(t, fx, token, "") } // scanArmed is storeArmed plus a granted store and a working engine. func scanArmed(t *testing.T, code string) (*consoleFixture, *objstore.MemScanner, string) { t.Helper() fx, _, token, _ := storeArmed(t, code) grantStore(t, fx, token) sc := objstore.NewMemScanner() fx.srv.cfg.StoreScanner = sc enableDeploymentScanning(t, fx) return fx, sc, token } // requestScanning is the member's half. An operator cannot enable an engine // without it — the attestation it carries is what says whether these objects // can be read at all. func requestScanning(t *testing.T, fx *consoleFixture, token string, encrypted bool) { t.Helper() if s, o := postJSON(t, scanURL(fx, "titogo"), map[string]any{"encrypted": encrypted}, token); s != http.StatusOK { t.Fatalf("scan request = %d %v", s, o) } } // The member gate runs FIRST: a stranger gets the same byte-identical 404 the // store doors give, never a 403 that would confirm the app exists. func TestScanDoorsGateOrder(t *testing.T) { fx, _, token, _ := storeArmed(t, "code-scan-gate") fx.srv.cfg.StoreOperators = []string{"eve@keymail.dev", "alice@keymail.dev"} eve := cliToken(t, fx, "eve@keymail.dev", "code-scan-gate-eve") for _, door := range []string{"", "/grant", "/revoke", "/sweep", "/enforce", "/callback-key/rotate"} { t.Run("scan"+door, func(t *testing.T) { // (a) an operator who is a member of nothing, against a real app. statusA, bodyA := postRaw(t, scanURL(fx, "titogo")+door, map[string]any{}, eve) // (b) a member, against an account that does not exist. statusB, bodyB := postRaw(t, fx.ts.URL+"/api/cli/apps/zzzz/titogo/store/scan"+door, map[string]any{}, token) // (c) a member of the account, against an app it never claimed. statusC, bodyC := postRaw(t, scanURL(fx, "ghost-app")+door, map[string]any{}, token) if statusA != http.StatusNotFound || statusB != http.StatusNotFound || statusC != http.StatusNotFound { t.Fatalf("scan%s gate = %d / %d / %d, want 404 for all three", door, statusA, statusB, statusC) } if bodyA != bodyB || bodyB != bodyC { t.Fatalf("scan%s refusals are not byte-identical:\n(a) %q\n(b) %q\n(c) %q", door, bodyA, bodyB, bodyC) } if bodyA != "404 page not found\n" { t.Fatalf("scan%s refusal body = %q, want the stdlib http.NotFound output", door, bodyA) } }) } if status, _ := postJSON(t, scanURL(fx, "titogo"), map[string]any{"encrypted": false}, ""); status != http.StatusUnauthorized { t.Fatalf("scan request with no token = %d, want 401", status) } } // grant and sweep 503 naming CARLOS_STORE_SCANNER when no engine is built. func TestScanGrantRequires503WithoutScanner(t *testing.T) { fx, _, token, _ := storeArmed(t, "code-scan-noengine") grantStore(t, fx, token) fx.srv.cfg.StoreScanner = nil for _, door := range []string{"/grant", "/sweep"} { status, out := postJSON(t, scanURL(fx, "titogo")+door, map[string]any{}, token) if status != http.StatusServiceUnavailable { t.Fatalf("scan%s with no engine = %d %v, want 503", door, status, out) } if msg := str(t, out, "error"); !strings.Contains(msg, "CARLOS_CLAMD_ADDR") { t.Fatalf("scan%s error = %q, want the wiring variable named", door, msg) } } // enforce ON also needs one — the tag denied on comes from the engine. status, out := postJSON(t, scanURL(fx, "titogo")+"/enforce", map[string]any{"on": true}, token) if status != http.StatusServiceUnavailable { t.Fatalf("enforce on with no engine = %d %v, want 503", status, out) } } // request, revoke and enforce off work with NO engine built. Otherwise // unsetting the scanner on a deployment with an enforcing record would leave // the deny unclearable and the store unrepairable — the door that fixes it // refusing for the very reason it needs fixing. func TestScanRevokeAndEnforceOffWorkWithoutScanner(t *testing.T) { fx, _, token := scanArmed(t, "code-scan-nodeadlock") requestScanning(t, fx, token, false) // Get into the bad state honestly: enable and enforce with an engine. if status, out := postJSON(t, scanURL(fx, "titogo")+"/grant", map[string]any{}, token); status != http.StatusOK { t.Fatalf("scan grant = %d %v", status, out) } if status, out := postJSON(t, scanURL(fx, "titogo")+"/enforce", map[string]any{"on": true}, token); status != http.StatusOK { t.Fatalf("enforce on = %d %v", status, out) } if rec := storedRecord(t, fx, "titogo"); rec.Scanning == nil || rec.Scanning.Enforce == nil { t.Fatal("setup: enforcement was never recorded") } // Now the engine goes away, exactly as unsetting the variable would do. fx.srv.cfg.StoreScanner = nil if status, out := postJSON(t, scanURL(fx, "titogo"), map[string]any{"encrypted": false}, token); status != http.StatusOK { t.Fatalf("scan request with no engine = %d %v, want 200", status, out) } if status, out := postJSON(t, scanURL(fx, "titogo")+"/enforce", map[string]any{"on": false}, token); status != http.StatusOK { t.Fatalf("enforce off with no engine = %d %v, want 200", status, out) } if rec := storedRecord(t, fx, "titogo"); rec.Scanning.Enforce != nil { t.Fatal("enforce off left the deny standing on the record") } if status, out := postJSON(t, scanURL(fx, "titogo")+"/revoke", map[string]any{}, token); status != http.StatusOK { t.Fatalf("revoke with no engine = %d %v, want 200", status, out) } if rec := storedRecord(t, fx, "titogo"); rec.Scanning.Enabled != nil { t.Fatal("revoke left the engine recorded") } } // Non-operators cannot grant, and the check runs after the member gate. func TestScanGrantRequiresOperator(t *testing.T) { fx, _, token := scanArmed(t, "code-scan-operator") fx.srv.cfg.StoreOperators = []string{"someone-else@keymail.dev"} for _, door := range []string{"/grant", "/revoke", "/sweep", "/enforce"} { status, out := postJSON(t, scanURL(fx, "titogo")+door, map[string]any{"on": false}, token) if status != http.StatusForbidden { t.Fatalf("scan%s as a non-operator = %d %v, want 403", door, status, out) } if msg := str(t, out, "error"); !strings.Contains(msg, "CARLOS_STORE_OPERATORS") { t.Fatalf("scan%s error = %q, want the operator variable named", door, msg) } } // The member's own verbs are unaffected: requesting and reading status are // theirs, and always were. if status, out := postJSON(t, scanURL(fx, "titogo"), map[string]any{"encrypted": false}, token); status != http.StatusOK { t.Fatalf("scan request as a member = %d %v, want 200", status, out) } if status, out := getJSON(t, scanURL(fx, "titogo"), token); status != http.StatusOK { t.Fatalf("scan status as a member = %d %v, want 200", status, out) } } // A client-encrypted store refuses enforce: nothing will ever be tagged // infected, so it would be theatre — a record claiming a control that cannot // possibly act. func TestScanEnforceRefusedOnEncryptedStore(t *testing.T) { fx, _, token := scanArmed(t, "code-scan-encrypted") if status, out := postJSON(t, scanURL(fx, "titogo"), map[string]any{"encrypted": true}, token); status != http.StatusOK { t.Fatalf("scan request = %d %v", status, out) } if status, out := postJSON(t, scanURL(fx, "titogo")+"/grant", map[string]any{}, token); status != http.StatusOK { t.Fatalf("scan grant = %d %v", status, out) } status, out := postJSON(t, scanURL(fx, "titogo")+"/enforce", map[string]any{"on": true}, token) if status != http.StatusConflict { t.Fatalf("enforce on an encrypted store = %d %v, want 409", status, out) } if msg := str(t, out, "error"); !strings.Contains(msg, "encrypted") { t.Fatalf("error = %q, want it to say why", msg) } if rec := storedRecord(t, fx, "titogo"); rec.Scanning.Enforce != nil { t.Fatal("a refused enforce still wrote the deny") } } // The attestation has no default. "Are these objects readable" is the question // the whole coverage claim rests on, and neither answer is safe to assume. func TestScanRequestRequiresAttestation(t *testing.T) { fx, _, token := scanArmed(t, "code-scan-attest") status, out := postJSON(t, scanURL(fx, "titogo"), map[string]any{}, token) if status != http.StatusBadRequest { t.Fatalf("request with no attestation = %d %v, want 400", status, out) } if msg := str(t, out, "error"); !strings.Contains(msg, "--client-encrypted") { t.Fatalf("error = %q, want it to name the flags", msg) } if rec := storedRecord(t, fx, "titogo"); rec.Scanning != nil && rec.Scanning.Requested != nil { t.Fatal("a refused request still recorded an attestation") } } // Enforcement actually reaches the app's IAM policy, not just the record. A // record that said enforcing while the live policy allowed reads would be // worse than never having offered the switch. func TestScanEnforceWritesTheDenyToThePolicy(t *testing.T) { fx, fake, token, _ := storeArmed(t, "code-scan-policy") grantStore(t, fx, token) fx.srv.cfg.StoreScanner = objstore.NewMemScanner() enableDeploymentScanning(t, fx) requestScanning(t, fx, token, false) if status, out := postJSON(t, scanURL(fx, "titogo")+"/grant", map[string]any{}, token); status != http.StatusOK { t.Fatalf("scan grant = %d %v", status, out) } user := objstore.UserName(fx.sqid, "titogo") // The ENFORCEMENT deny specifically, which is the one keyed on the verdict // tag. Every policy also carries the two anti-forgery Denys, unconditional // and unrelated to this switch, so `"Effect":"Deny"` — what this test used // to look for — no longer tells the two states apart. It would now report // enforcement on from the moment the store was granted. const enforcementDeny = "s3:ExistingObjectTag" if strings.Contains(fake.PolicyFor(user), enforcementDeny) { t.Fatal("enabling the engine alone wrote an enforcement deny — enforcement is a separate switch") } if status, out := postJSON(t, scanURL(fx, "titogo")+"/enforce", map[string]any{"on": true}, token); status != http.StatusOK { t.Fatalf("enforce on = %d %v", status, out) } if !strings.Contains(fake.PolicyFor(user), enforcementDeny) { t.Fatalf("enforce on never reached the policy:\n%s", fake.PolicyFor(user)) } if status, out := postJSON(t, scanURL(fx, "titogo")+"/enforce", map[string]any{"on": false}, token); status != http.StatusOK { t.Fatalf("enforce off = %d %v", status, out) } if strings.Contains(fake.PolicyFor(user), enforcementDeny) { t.Fatalf("enforce off left a live enforcement deny standing:\n%s", fake.PolicyFor(user)) } // And turning enforcement off must not have taken the anti-forgery Denys // with it: a store that has stopped denying infected reads is exactly // where a forged clean tag would sit unnoticed longest. if !strings.Contains(fake.PolicyFor(user), "s3:RequestObjectTagKeys") { t.Fatalf("enforce off dropped the anti-forgery denys:\n%s", fake.PolicyFor(user)) } } // Enforcement before an engine is enabled is refused. Otherwise a store could // carry a Deny keyed on a tag nothing will ever write, which denies nothing // and reads in the record as protection. func TestScanEnforceRefusedBeforeGrant(t *testing.T) { fx, _, token := scanArmed(t, "code-scan-enforce-early") status, out := postJSON(t, scanURL(fx, "titogo")+"/enforce", map[string]any{"on": true}, token) if status != http.StatusConflict { t.Fatalf("enforce before scan grant = %d %v, want 409", status, out) } } // Status reports the engine and its signature database. A clean verdict from // three-month-old signatures is not the same claim as a fresh one. func TestScanStatusCarriesProvenance(t *testing.T) { fx, _, token := scanArmed(t, "code-scan-status") status, out := getJSON(t, scanURL(fx, "titogo"), token) if status != http.StatusOK { t.Fatalf("scan status = %d %v", status, out) } if got := str(t, out, "engine"); got != "clamav" { t.Fatalf("engine = %q", got) } if str(t, out, "signature") == "" { t.Fatal("status carries no signature-database version") } } // `carlos store status` carries the scan block once it exists. Additive only: // release.PurgeApp parses this same object with its own struct and freezes the // grant and provisioned names, so a new key is safe and a renamed one is a // purge that stops working. func TestStoreStatusCarriesScanningBlock(t *testing.T) { fx, _, token := scanArmed(t, "code-store-status-scan") status, out := getJSON(t, storeURL(fx, "titogo"), token) if status != http.StatusOK { t.Fatalf("store status = %d %v", status, out) } if _, present := out["scanning"]; !present { t.Fatal("store status carries no scanning key at all") } if out["scanning"] != nil { t.Fatalf("a store nobody requested scanning for reported %v", out["scanning"]) } if s, o := postJSON(t, scanURL(fx, "titogo"), map[string]any{"encrypted": false}, token); s != http.StatusOK { t.Fatalf("scan request = %d %v", s, o) } if s, o := postJSON(t, scanURL(fx, "titogo")+"/grant", map[string]any{}, token); s != http.StatusOK { t.Fatalf("scan grant = %d %v", s, o) } status, out = getJSON(t, storeURL(fx, "titogo"), token) if status != http.StatusOK { t.Fatalf("store status = %d %v", status, out) } sc, ok := out["scanning"].(map[string]any) if !ok { t.Fatalf("scanning = %#v, want an object", out["scanning"]) } if _, ok := sc["enabled"]; !ok { t.Fatalf("scanning block does not report the enabled engine: %v", sc) } // The grant and provisioned names PurgeApp parses must still be there and // still be spelled the same. for _, frozen := range []string{"grant", "provisioned", "declared"} { if _, present := out[frozen]; !present { t.Fatalf("the %q key PurgeApp parses is gone from the reply", frozen) } } } // Enabling an engine requires the MEMBER's request, and specifically their // attestation. Without one, Requested stays nil — and nil reads as "not // encrypted" everywhere downstream, so a client-encrypted store nobody // attested would be swept and reported entirely clean. It also skips the // consent point: this engine reads tenant object bytes. func TestScanGrantRefusedWithoutMemberRequest(t *testing.T) { fx, _, token := scanArmed(t, "code-scan-no-request") status, out := postJSON(t, scanURL(fx, "titogo")+"/grant", map[string]any{}, token) if status != http.StatusConflict { t.Fatalf("scan grant with no member request = %d %v, want 409", status, out) } if msg := str(t, out, "error"); !strings.Contains(msg, "scan request") { t.Fatalf("error = %q, want it to name the verb the member runs", msg) } if rec := storedRecord(t, fx, "titogo"); rec.Scanning != nil && rec.Scanning.Enabled != nil { t.Fatal("a refused grant still enabled the engine") } // With the attestation on record it goes through. if s, o := postJSON(t, scanURL(fx, "titogo"), map[string]any{"encrypted": false}, token); s != http.StatusOK { t.Fatalf("scan request = %d %v", s, o) } if s, o := postJSON(t, scanURL(fx, "titogo")+"/grant", map[string]any{}, token); s != http.StatusOK { t.Fatalf("scan grant after a request = %d %v, want 200", s, o) } } // Status says so when the record names an engine this deployment no longer // has. Silence there reads as health: the reply would carry the engine, the // enforcement state and the coverage, and never mention that nothing runs. func TestScanStatusReportsMissingEngineOnEnabledRecord(t *testing.T) { fx, _, token := scanArmed(t, "code-scan-engine-gone") if s, o := postJSON(t, scanURL(fx, "titogo"), map[string]any{"encrypted": false}, token); s != http.StatusOK { t.Fatalf("scan request = %d %v", s, o) } if s, o := postJSON(t, scanURL(fx, "titogo")+"/grant", map[string]any{}, token); s != http.StatusOK { t.Fatalf("scan grant = %d %v", s, o) } fx.srv.cfg.StoreScanner = nil status, out := getJSON(t, scanURL(fx, "titogo"), token) if status != http.StatusOK { t.Fatalf("scan status = %d %v", status, out) } msg, _ := out["engine_error"].(string) if !strings.Contains(msg, "CARLOS_CLAMD_ADDR") { t.Fatalf("engine_error = %q, want it to say no engine is configured", msg) } } // `disable` has to disable everywhere, or it disables nothing. // // The hourly pass going quiet is not the same as scanning being off: the sweep // verb and the grant door reach the engine by their own paths. Without these // checks an operator could disable scanning, watch the tick stop, and still // have `carlos store scan sweep` read tenant bytes. func TestDeploymentDisableStopsEveryPath(t *testing.T) { fx, _, token := scanArmed(t, "code-scan-disabled") requestScanning(t, fx, token, false) if s, o := postJSON(t, scanURL(fx, "titogo")+"/grant", map[string]any{}, token); s != http.StatusOK { t.Fatalf("setup grant = %d %v", s, o) } // Now switch the deployment off, the way `carlos system antivirus disable` // will. The engine stays wired — this is policy, not wiring. disableDeploymentScanning(t, fx) for _, door := range []string{"/grant", "/sweep"} { status, out := postJSON(t, scanURL(fx, "titogo")+door, map[string]any{}, token) if status != http.StatusServiceUnavailable { t.Fatalf("scan%s while disabled = %d %v, want 503", door, status, out) } msg := str(t, out, "error") if !strings.Contains(msg, "carlos system antivirus enable") { t.Fatalf("scan%s error = %q, want it to name the command that fixes it", door, msg) } // And NOT the wiring variable: an operator whose engine is wired and // whose record is off must not be sent to edit a unit file. if strings.Contains(msg, "CARLOS_CLAMD_ADDR") { t.Fatalf("scan%s conflated a disabled record with missing wiring: %q", door, msg) } } } // The two 503s stay distinct. Wired-but-off and not-wired want different // actions, and a single message would send someone to the wrong one. func TestDisabledAndUnwiredAreDifferent503s(t *testing.T) { fx, _, token := scanArmed(t, "code-scan-two-503s") requestScanning(t, fx, token, false) disableDeploymentScanning(t, fx) _, offBody := postJSON(t, scanURL(fx, "titogo")+"/sweep", map[string]any{}, token) offMsg := str(t, offBody, "error") enableDeploymentScanning(t, fx) fx.srv.cfg.StoreScanner = nil _, unwiredBody := postJSON(t, scanURL(fx, "titogo")+"/sweep", map[string]any{}, token) unwiredMsg := str(t, unwiredBody, "error") if offMsg == unwiredMsg { t.Fatalf("both states answer identically: %q", offMsg) } if !strings.Contains(unwiredMsg, "CARLOS_CLAMD_ADDR") { t.Fatalf("the unwired message does not name the wiring: %q", unwiredMsg) } } // RunSweep itself refuses, not merely the door in front of it. The sweep verb // is not the only caller, and a check that lived only in a handler would be a // check the next caller forgets. func TestRunSweepRefusesWhenDeploymentDisabled(t *testing.T) { ctx := context.Background() fx := newConsoleFixture(t) fx.srv.cfg.StoreScanner = objstore.NewMemScanner() enableStoreRecordOnly(t, fx, fixtureAcct, fixtureApp) // No deployment record at all: absent means not enabled. _, err := objstore.RunSweep(ctx, fx.rel, &countingLister{keys: []string{"a.txt"}}, objstore.NewMemScanner(), fixtureAcct, fixtureApp, time.Minute) if !errors.Is(err, objstore.ErrScanningDisabled) { t.Fatalf("RunSweep with no deployment record = %v, want ErrScanningDisabled", err) } } // The member's own verbs keep working while scanning is disabled. Otherwise // disabling would strand every enforcing record, with the door that clears it // refusing for the very reason it needs using. func TestMemberVerbsSurviveDeploymentDisable(t *testing.T) { fx, _, token := scanArmed(t, "code-scan-disabled-member") requestScanning(t, fx, token, false) if s, o := postJSON(t, scanURL(fx, "titogo")+"/grant", map[string]any{}, token); s != http.StatusOK { t.Fatalf("setup grant = %d %v", s, o) } if s, o := postJSON(t, scanURL(fx, "titogo")+"/enforce", map[string]any{"on": true}, token); s != http.StatusOK { t.Fatalf("setup enforce = %d %v", s, o) } disableDeploymentScanning(t, fx) if s, o := postJSON(t, scanURL(fx, "titogo"), map[string]any{"encrypted": false}, token); s != http.StatusOK { t.Fatalf("request while disabled = %d %v", s, o) } if s, o := postJSON(t, scanURL(fx, "titogo")+"/enforce", map[string]any{"on": false}, token); s != http.StatusOK { t.Fatalf("enforce off while disabled = %d %v", s, o) } if s, o := postJSON(t, scanURL(fx, "titogo")+"/revoke", map[string]any{}, token); s != http.StatusOK { t.Fatalf("revoke while disabled = %d %v", s, o) } if rec := storedRecord(t, fx, "titogo"); rec.Scanning.Enforce != nil { t.Fatal("enforce off left a live deny while scanning was disabled") } } // disableDeploymentScanning flips the switch off, leaving the engine wired. func disableDeploymentScanning(t *testing.T, fx *consoleFixture) { t.Helper() ctx := context.Background() av, etag, err := release.GetAntivirus(ctx, fx.rel) if err != nil { t.Fatal(err) } if av == nil { av = &release.AntivirusSettings{Engine: "clamav"} } av.Enabled = false av.At = time.Now().UTC() av.By = "test" if _, err := release.PutAntivirus(ctx, fx.rel, av, etag); err != nil { t.Fatal(err) } } // A per-app record saying `enabled: clamav` while the deployment switch is off // describes a control that is not running. `carlos store scan status` prints // this reply, so omitting the deployment half is a new way for the status // output to lie — the same lie the engine_error branch already refuses to tell // about the wiring half. func TestStoreScanStatusReportsDeploymentState(t *testing.T) { ctx := context.Background() fx, _, token := scanArmed(t, "code-scan-depstate") requestScanning(t, fx, token, false) if s, o := postJSON(t, scanURL(fx, "titogo")+"/grant", map[string]any{}, token); s != http.StatusOK { t.Fatalf("grant = %d %v", s, o) } status, out := getJSON(t, scanURL(fx, "titogo"), token) if status != http.StatusOK { t.Fatalf("status = %d %v", status, out) } if on, _ := out["deployment_enabled"].(bool); !on { t.Fatalf("deployment_enabled = %v with the switch on", out["deployment_enabled"]) } if _, bad := out["deployment_error"]; bad { t.Fatalf("a healthy deployment reported %v", out["deployment_error"]) } // Now turn the deployment off underneath an app whose own record still // says enabled. Nothing sweeps it, and the status has to say so. av, etag, err := release.GetAntivirus(ctx, fx.rel) if err != nil || av == nil { t.Fatalf("record = %+v, %v", av, err) } av.Enabled = false if _, err := release.PutAntivirus(ctx, fx.rel, av, etag); err != nil { t.Fatal(err) } _, out = getJSON(t, scanURL(fx, "titogo"), token) if on, _ := out["deployment_enabled"].(bool); on { t.Fatal("deployment_enabled stayed true after a disable") } msg := str(t, out, "deployment_error") if !strings.Contains(msg, "carlos system antivirus enable") { t.Fatalf("deployment_error = %q, want the command that fixes it", msg) } // The per-app record is UNCHANGED — the two facts are reported // separately, never collapsed. if out["scanning"] == nil { t.Fatal("the per-app record vanished from the reply") } } func TestStoreScanStatusReportsQueueDepth(t *testing.T) { fx, _, token := scanArmed(t, "code-scan-queue-depth") requestScanning(t, fx, token, false) if _, err := objstore.Enqueue(context.Background(), fx.rel, fx.srv.scanQueue, fixtureAcct, "titogo", &objstore.QueueItem{Key: "uploads/a.pdf", By: "test"}, time.Now()); err != nil { t.Fatal(err) } status, out := getJSON(t, scanURL(fx, "titogo"), token) if status != http.StatusOK { t.Fatalf("status = %d %v", status, out) } if got := int(out["queue_depth"].(float64)); got != 1 { t.Fatalf("queue_depth = %d, want 1", got) } }