package hostbox import ( "encoding/json" "os" "path/filepath" "reflect" "testing" ) func TestPlan14GenerationPayloadInventory(t *testing.T) { for _, attack := range []string{"none", "empty", "missing-payload", "missing-revision", "missing-table", "unknown-field", "trailing-json", "wrong-key", "incomplete", "oversize", "unknown-kind", "duplicate-entry", "missing-identity", "foreign-root", "replaced-path"} { t.Run(attack, func(t *testing.T) { j, err := OpenLaunchJournal(filepath.Join(t.TempDir(), "journal.db")) if err != nil { t.Fatal(err) } defer j.Close() s, err := NewJournalService(j) if err != nil { t.Fatal(err) } in := StopInventory{Complete: true, Host: "tenant.example", Incarnation: "inc", InstallationStamp: 1, Box: "box", Enrollment: "en", DirectoryRevision: "1", Entries: []StopInventoryEntry{{Kind: "path", ArtifactID: "payload", ManagedRoot: "data", RelativePath: "instances/tenant.example/state", Ownership: "owner", Materialization: "materialized", Device: 1, Inode: 2, Type: 0100000}}} if attack != "empty" { if err := s.Inventory.WriteStopInventory(t.Context(), in); err != nil { t.Fatal(err) } } query := "" switch attack { case "missing-payload": query = "DELETE FROM stop_inventory" case "missing-revision": query = "DELETE FROM journal_inventory_versions" case "missing-table": query = "DROP TABLE stop_inventory" case "unknown-field": query = "UPDATE stop_inventory SET body=json_set(body,'$.Unknown',true)" case "trailing-json": query = "UPDATE stop_inventory SET body=CAST(body || '{}' AS BLOB)" case "wrong-key": query = "UPDATE stop_inventory SET body=json_set(body,'$.Host','other.example')" case "incomplete": query = "UPDATE stop_inventory SET body=json_set(body,'$.Complete',json('false'))" case "oversize": query = "UPDATE stop_inventory SET body=zeroblob(1048577)" case "unknown-kind": in.Entries[0].Kind = "unknown" case "duplicate-entry": in.Entries = append(in.Entries, in.Entries[0]) case "missing-identity": in.Entries[0].Inode = 0 case "foreign-root": in.Entries[0].ManagedRoot = "opt" case "replaced-path": if err := os.Rename(j.path, j.path+".old"); err != nil { t.Fatal(err) } if err := os.WriteFile(j.path, []byte("other"), 0600); err != nil { t.Fatal(err) } } if query != "" { if _, err := j.db.Exec(query); err != nil { t.Fatal(err) } } if attack == "unknown-kind" || attack == "duplicate-entry" || attack == "missing-identity" || attack == "foreign-root" { raw, err := json.Marshal(in) if err != nil { t.Fatal(err) } if _, err := j.db.Exec("UPDATE stop_inventory SET body=?", raw); err != nil { t.Fatal(err) } } // Use the production read-only connection, except for the retained-inode test. reader := j if attack != "replaced-path" { reader, err = OpenGenerationLaunchReader(j.path) if err != nil { t.Fatal(err) } defer reader.Close() } got, err := reader.GenerationPayloads(t.Context()) switch attack { case "none": if err != nil || len(got) != 1 || got[0].Revision != 1 || !reflect.DeepEqual(got[0].Inventory, in) { t.Fatalf("payloads=%+v err=%v", got, err) } case "empty": if err != nil || len(got) != 0 { t.Fatalf("empty=%+v err=%v", got, err) } default: if err == nil { t.Fatalf("unsafe %s payloads=%+v", attack, got) } } }) } } func TestPlan14GenerationPayloadReaderDoesNotInitialize(t *testing.T) { path := filepath.Join(t.TempDir(), "missing.db") if err := os.WriteFile(path, nil, 0600); err != nil { t.Fatal(err) } j, err := OpenGenerationLaunchReader(path) if err != nil { t.Fatal(err) } defer j.Close() if _, err := j.GenerationPayloads(t.Context()); err == nil { t.Fatal("missing inventory schema accepted") } if st, err := os.Stat(path); err != nil || st.Size() != 0 { t.Fatalf("reader wrote schema: %v %v", st, err) } }