From aaebb8f3abc10b4ef24a75a379db2e6fc91e68dc Mon Sep 17 00:00:00 2001 From: Ophestra Date: Fri, 14 Feb 2025 14:44:28 +0900 Subject: [PATCH] fortify: check print behaviour These output are supposed to be deterministic, so checking them is a good way to catch regressions. Signed-off-by: Ophestra --- main.go | 10 +- print.go | 196 ++++++++------- print_test.go | 671 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 787 insertions(+), 90 deletions(-) create mode 100644 print_test.go diff --git a/main.go b/main.go index 1ef11e1..59e5592 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,7 @@ import ( "sync" "syscall" "text/tabwriter" + "time" "git.gensokyo.uk/security/fortify/dbus" "git.gensokyo.uk/security/fortify/fst" @@ -23,6 +24,7 @@ import ( "git.gensokyo.uk/security/fortify/internal/linux" init0 "git.gensokyo.uk/security/fortify/internal/priv/init" "git.gensokyo.uk/security/fortify/internal/priv/shim" + "git.gensokyo.uk/security/fortify/internal/state" "git.gensokyo.uk/security/fortify/internal/system" ) @@ -114,7 +116,7 @@ func main() { fmt.Println(license) fmsg.Exit(0) case "template": // print full template configuration - printJSON(fst.Template()) + printJSON(os.Stdout, false, fst.Template()) fmsg.Exit(0) case "help": // print help message flag.CommandLine.Usage() @@ -127,7 +129,7 @@ func main() { // Ignore errors; set is set for ExitOnError. _ = set.Parse(args[1:]) - printPs(short) + printPs(os.Stdout, time.Now().UTC(), state.NewMulti(sys.Paths().RunDirPath), short) fmsg.Exit(0) case "show": // pretty-print app info set := flag.NewFlagSet("show", flag.ExitOnError) @@ -139,14 +141,14 @@ func main() { switch len(set.Args()) { case 0: // system - printShowSystem(short) + printShowSystem(os.Stdout, short) case 1: // instance name := set.Args()[0] config, instance := tryShort(name) if config == nil { config = tryPath(name) } - printShowInstance(instance, config, short) + printShowInstance(os.Stdout, time.Now().UTC(), instance, config, short) default: fmsg.Fatal("show requires 1 argument") } diff --git a/print.go b/print.go index 8c074a1..7660353 100644 --- a/print.go +++ b/print.go @@ -3,7 +3,7 @@ package main import ( "encoding/json" "fmt" - "os" + "io" "slices" "strconv" "strings" @@ -16,7 +16,10 @@ import ( "git.gensokyo.uk/security/fortify/internal/state" ) -func printShowSystem(short bool) { +func printShowSystem(output io.Writer, short bool) { + t := newPrinter(output) + defer t.MustFlush() + info := new(fst.Info) // get fid by querying uid of aid 0 @@ -27,58 +30,55 @@ func printShowSystem(short bool) { } if flagJSON { - printJSON(info) + printJSON(output, short, info) return } - w := tabwriter.NewWriter(os.Stdout, 0, 1, 4, ' ', 0) - - fmt.Fprintf(w, "User:\t%d\n", info.User) - - if err := w.Flush(); err != nil { - fmsg.Fatalf("cannot flush tabwriter: %v", err) - } + t.Printf("User:\t%d\n", info.User) } -func printShowInstance(instance *state.State, config *fst.Config, short bool) { +func printShowInstance( + output io.Writer, now time.Time, + instance *state.State, config *fst.Config, + short bool) { if flagJSON { if instance != nil { - printJSON(instance) + printJSON(output, short, instance) } else { - printJSON(config) + printJSON(output, short, config) } return } - now := time.Now().UTC() - w := tabwriter.NewWriter(os.Stdout, 0, 1, 4, ' ', 0) + t := newPrinter(output) + defer t.MustFlush() if config.Confinement.Sandbox == nil { - fmt.Print("Warning: this configuration uses permissive defaults!\n\n") + mustPrint(output, "Warning: this configuration uses permissive defaults!\n\n") } if instance != nil { - fmt.Fprintf(w, "State\n") - fmt.Fprintf(w, " Instance:\t%s (%d)\n", instance.ID.String(), instance.PID) - fmt.Fprintf(w, " Uptime:\t%s\n", now.Sub(instance.Time).Round(time.Second).String()) - fmt.Fprintf(w, "\n") + t.Printf("State\n") + t.Printf(" Instance:\t%s (%d)\n", instance.ID.String(), instance.PID) + t.Printf(" Uptime:\t%s\n", now.Sub(instance.Time).Round(time.Second).String()) + t.Printf("\n") } - fmt.Fprintf(w, "App\n") + t.Printf("App\n") if config.ID != "" { - fmt.Fprintf(w, " ID:\t%d (%s)\n", config.Confinement.AppID, config.ID) + t.Printf(" ID:\t%d (%s)\n", config.Confinement.AppID, config.ID) } else { - fmt.Fprintf(w, " ID:\t%d\n", config.Confinement.AppID) + t.Printf(" ID:\t%d\n", config.Confinement.AppID) } - fmt.Fprintf(w, " Enablements:\t%s\n", config.Confinement.Enablements.String()) + t.Printf(" Enablements:\t%s\n", config.Confinement.Enablements.String()) if len(config.Confinement.Groups) > 0 { - fmt.Fprintf(w, " Groups:\t%q\n", config.Confinement.Groups) + t.Printf(" Groups:\t%q\n", config.Confinement.Groups) } - fmt.Fprintf(w, " Directory:\t%s\n", config.Confinement.Outer) + t.Printf(" Directory:\t%s\n", config.Confinement.Outer) if config.Confinement.Sandbox != nil { sandbox := config.Confinement.Sandbox if sandbox.Hostname != "" { - fmt.Fprintf(w, " Hostname:\t%q\n", sandbox.Hostname) + t.Printf(" Hostname:\t%q\n", sandbox.Hostname) } flags := make([]string, 0, 7) writeFlag := func(name string, value bool) { @@ -96,27 +96,27 @@ func printShowInstance(instance *state.State, config *fst.Config, short bool) { if len(flags) == 0 { flags = append(flags, "none") } - fmt.Fprintf(w, " Flags:\t%s\n", strings.Join(flags, " ")) + t.Printf(" Flags:\t%s\n", strings.Join(flags, " ")) etc := sandbox.Etc if etc == "" { etc = "/etc" } - fmt.Fprintf(w, " Etc:\t%s\n", etc) + t.Printf(" Etc:\t%s\n", etc) if len(sandbox.Override) > 0 { - fmt.Fprintf(w, " Overrides:\t%s\n", strings.Join(sandbox.Override, " ")) + t.Printf(" Overrides:\t%s\n", strings.Join(sandbox.Override, " ")) } // Env map[string]string `json:"env"` // Link [][2]string `json:"symlink"` } - fmt.Fprintf(w, " Command:\t%s\n", strings.Join(config.Command, " ")) - fmt.Fprintf(w, "\n") + t.Printf(" Command:\t%s\n", strings.Join(config.Command, " ")) + t.Printf("\n") if !short { if config.Confinement.Sandbox != nil && len(config.Confinement.Sandbox.Filesystem) > 0 { - fmt.Fprintf(w, "Filesystem\n") + t.Printf("Filesystem\n") for _, f := range config.Confinement.Sandbox.Filesystem { if f == nil { continue @@ -141,61 +141,54 @@ func printShowInstance(instance *state.State, config *fst.Config, short bool) { if f.Dst != "" { expr.WriteString(":" + f.Dst) } - fmt.Fprintf(w, "%s\n", expr.String()) + t.Printf("%s\n", expr.String()) } - fmt.Fprintf(w, "\n") + t.Printf("\n") } if len(config.Confinement.ExtraPerms) > 0 { - fmt.Fprintf(w, "Extra ACL\n") + t.Printf("Extra ACL\n") for _, p := range config.Confinement.ExtraPerms { if p == nil { continue } - fmt.Fprintf(w, " %s\n", p.String()) + t.Printf(" %s\n", p.String()) } - fmt.Fprintf(w, "\n") + t.Printf("\n") } } printDBus := func(c *dbus.Config) { - fmt.Fprintf(w, " Filter:\t%v\n", c.Filter) + t.Printf(" Filter:\t%v\n", c.Filter) if len(c.See) > 0 { - fmt.Fprintf(w, " See:\t%q\n", c.See) + t.Printf(" See:\t%q\n", c.See) } if len(c.Talk) > 0 { - fmt.Fprintf(w, " Talk:\t%q\n", c.Talk) + t.Printf(" Talk:\t%q\n", c.Talk) } if len(c.Own) > 0 { - fmt.Fprintf(w, " Own:\t%q\n", c.Own) + t.Printf(" Own:\t%q\n", c.Own) } if len(c.Call) > 0 { - fmt.Fprintf(w, " Call:\t%q\n", c.Call) + t.Printf(" Call:\t%q\n", c.Call) } if len(c.Broadcast) > 0 { - fmt.Fprintf(w, " Broadcast:\t%q\n", c.Broadcast) + t.Printf(" Broadcast:\t%q\n", c.Broadcast) } } if config.Confinement.SessionBus != nil { - fmt.Fprintf(w, "Session bus\n") + t.Printf("Session bus\n") printDBus(config.Confinement.SessionBus) - fmt.Fprintf(w, "\n") + t.Printf("\n") } if config.Confinement.SystemBus != nil { - fmt.Fprintf(w, "System bus\n") + t.Printf("System bus\n") printDBus(config.Confinement.SystemBus) - fmt.Fprintf(w, "\n") - } - - if err := w.Flush(); err != nil { - fmsg.Fatalf("cannot flush tabwriter: %v", err) + t.Printf("\n") } } -func printPs(short bool) { - now := time.Now().UTC() - +func printPs(output io.Writer, now time.Time, s state.Store, short bool) { var entries state.Entries - s := state.NewMulti(sys.Paths().RunDirPath) if e, err := state.Join(s); err != nil { fmsg.Fatalf("cannot join store: %v", err) } else { @@ -205,12 +198,12 @@ func printPs(short bool) { fmsg.Printf("cannot close store: %v", err) } - if flagJSON { + if !short && flagJSON { es := make(map[string]*state.State, len(entries)) for id, instance := range entries { es[id.String()] = instance } - printJSON(es) + printJSON(output, short, es) return } @@ -225,7 +218,7 @@ func printPs(short bool) { // gracefully skip inconsistent states if id != instance.ID { - fmt.Printf("possible store corruption: entry %s has id %s", + fmsg.Printf("possible store corruption: entry %s has id %s", id.String(), instance.ID.String()) continue } @@ -239,25 +232,34 @@ func printPs(short bool) { for i, e := range exp { v[i] = e.s } - printJSON(v) + printJSON(output, short, v) } else { for _, e := range exp { - fmt.Println(e.s[:8]) + mustPrintln(output, e.s[:8]) } } return } - // buffer output to reduce terminal activity - w := tabwriter.NewWriter(os.Stdout, 0, 1, 4, ' ', 0) - fmt.Fprintln(w, "\tInstance\tPID\tApp\tUptime\tEnablements\tCommand") + t := newPrinter(output) + defer t.MustFlush() + + t.Println("\tInstance\tPID\tApp\tUptime\tEnablements\tCommand") for _, e := range exp { - printInstance(w, e, now) - } - fmt.Fprintln(w) - if err := w.Flush(); err != nil { - fmsg.Fatalf("cannot flush tabwriter: %v", err) + var ( + es = "(No confinement information)" + cs = "(No command information)" + as = "(No configuration information)" + ) + if e.Config != nil { + es = e.Config.Confinement.Enablements.String() + cs = fmt.Sprintf("%q", e.Config.Command) + as = strconv.Itoa(e.Config.Confinement.AppID) + } + t.Printf("\t%s\t%d\t%s\t%s\t%s\t%s\n", + e.s[:8], e.PID, as, now.Sub(e.Time).Round(time.Second).String(), strings.TrimPrefix(es, ", "), cs) } + t.Println() } type expandedStateEntry struct { @@ -265,26 +267,48 @@ type expandedStateEntry struct { *state.State } -func printInstance(w *tabwriter.Writer, e *expandedStateEntry, now time.Time) { - var ( - es = "(No confinement information)" - cs = "(No command information)" - as = "(No configuration information)" - ) - if e.Config != nil { - es = e.Config.Confinement.Enablements.String() - cs = fmt.Sprintf("%q", e.Config.Command) - as = strconv.Itoa(e.Config.Confinement.AppID) +func printJSON(output io.Writer, short bool, v any) { + encoder := json.NewEncoder(output) + if !short { + encoder.SetIndent("", " ") } - fmt.Fprintf(w, "\t%s\t%d\t%s\t%s\t%s\t%s\n", - e.s[:8], e.PID, as, now.Sub(e.Time).Round(time.Second).String(), strings.TrimPrefix(es, ", "), cs) -} - -func printJSON(v any) { - encoder := json.NewEncoder(os.Stdout) - encoder.SetIndent("", " ") if err := encoder.Encode(v); err != nil { fmsg.Fatalf("cannot serialise: %v", err) panic("unreachable") } } + +func newPrinter(output io.Writer) *tp { return &tp{tabwriter.NewWriter(output, 0, 1, 4, ' ', 0)} } + +type tp struct{ *tabwriter.Writer } + +func (p *tp) Printf(format string, a ...any) { + if _, err := fmt.Fprintf(p, format, a...); err != nil { + fmsg.Fatalf("cannot write to tabwriter: %v", err) + panic("unreachable") + } +} +func (p *tp) Println(a ...any) { + if _, err := fmt.Fprintln(p, a...); err != nil { + fmsg.Fatalf("cannot write to tabwriter: %v", err) + panic("unreachable") + } +} +func (p *tp) MustFlush() { + if err := p.Writer.Flush(); err != nil { + fmsg.Fatalf("cannot flush tabwriter: %v", err) + panic("unreachable") + } +} +func mustPrint(output io.Writer, a ...any) { + if _, err := fmt.Fprint(output, a...); err != nil { + fmsg.Fatalf("cannot print: %v", err) + panic("unreachable") + } +} +func mustPrintln(output io.Writer, a ...any) { + if _, err := fmt.Fprintln(output, a...); err != nil { + fmsg.Fatalf("cannot print: %v", err) + panic("unreachable") + } +} diff --git a/print_test.go b/print_test.go new file mode 100644 index 0000000..5d5e814 --- /dev/null +++ b/print_test.go @@ -0,0 +1,671 @@ +package main + +import ( + "strings" + "testing" + "time" + + "git.gensokyo.uk/security/fortify/dbus" + "git.gensokyo.uk/security/fortify/fst" + "git.gensokyo.uk/security/fortify/internal/state" +) + +var ( + testID = fst.ID{ + 0x8e, 0x2c, 0x76, 0xb0, + 0x66, 0xda, 0xbe, 0x57, + 0x4c, 0xf0, 0x73, 0xbd, + 0xb4, 0x6e, 0xb5, 0xc1, + } + testState = &state.State{ + ID: testID, + PID: 0xDEADBEEF, + Config: fst.Template(), + Time: testAppTime, + } + testTime = time.Unix(3752, 1).UTC() + testAppTime = time.Unix(0, 9).UTC() +) + +func Test_printShowInstance(t *testing.T) { + testCases := []struct { + name string + instance *state.State + config *fst.Config + short, json bool + want string + }{ + {"config", nil, fst.Template(), false, false, `App + ID: 9 (org.chromium.Chromium) + Enablements: Wayland, D-Bus, PulseAudio + Groups: ["video"] + Directory: /var/lib/persist/home/org.chromium.Chromium + Hostname: "localhost" + Flags: userns net dev tty mapuid autoetc + Etc: /etc + Overrides: /var/run/nscd + Command: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland + +Filesystem + +/nix/store + +/run/current-system + +/run/opengl-driver + +/var/db/nix-channels + w*/var/lib/fortify/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium + d+/dev/dri + +Extra ACL + --x+:/var/lib/fortify/u0 + rwx:/var/lib/fortify/u0/org.chromium.Chromium + +Session bus + Filter: true + Talk: ["org.freedesktop.Notifications" "org.freedesktop.FileManager1" "org.freedesktop.ScreenSaver" "org.freedesktop.secrets" "org.kde.kwalletd5" "org.kde.kwalletd6" "org.gnome.SessionManager"] + Own: ["org.chromium.Chromium.*" "org.mpris.MediaPlayer2.org.chromium.Chromium.*" "org.mpris.MediaPlayer2.chromium.*"] + Call: map["org.freedesktop.portal.*":"*"] + Broadcast: map["org.freedesktop.portal.*":"@/org/freedesktop/portal/*"] + +System bus + Filter: true + Talk: ["org.bluez" "org.freedesktop.Avahi" "org.freedesktop.UPower"] + +`}, + {"config pd", nil, new(fst.Config), false, false, `Warning: this configuration uses permissive defaults! + +App + ID: 0 + Enablements: (No enablements) + Directory: + Command: + +`}, + {"config flag none", nil, &fst.Config{Confinement: fst.ConfinementConfig{Sandbox: new(fst.SandboxConfig)}}, false, false, `App + ID: 0 + Enablements: (No enablements) + Directory: + Flags: none + Etc: /etc + Command: + +`}, + {"config nil entries", nil, &fst.Config{Confinement: fst.ConfinementConfig{Sandbox: &fst.SandboxConfig{Filesystem: make([]*fst.FilesystemConfig, 1)}, ExtraPerms: make([]*fst.ExtraPermConfig, 1)}}, false, false, `App + ID: 0 + Enablements: (No enablements) + Directory: + Flags: none + Etc: /etc + Command: + +Filesystem + +Extra ACL + +`}, + {"config pd dbus see", nil, &fst.Config{Confinement: fst.ConfinementConfig{SessionBus: &dbus.Config{See: []string{"org.example.test"}}}}, false, false, `Warning: this configuration uses permissive defaults! + +App + ID: 0 + Enablements: (No enablements) + Directory: + Command: + +Session bus + Filter: false + See: ["org.example.test"] + +`}, + + {"instance", testState, fst.Template(), false, false, `State + Instance: 8e2c76b066dabe574cf073bdb46eb5c1 (3735928559) + Uptime: 1h2m32s + +App + ID: 9 (org.chromium.Chromium) + Enablements: Wayland, D-Bus, PulseAudio + Groups: ["video"] + Directory: /var/lib/persist/home/org.chromium.Chromium + Hostname: "localhost" + Flags: userns net dev tty mapuid autoetc + Etc: /etc + Overrides: /var/run/nscd + Command: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland + +Filesystem + +/nix/store + +/run/current-system + +/run/opengl-driver + +/var/db/nix-channels + w*/var/lib/fortify/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium + d+/dev/dri + +Extra ACL + --x+:/var/lib/fortify/u0 + rwx:/var/lib/fortify/u0/org.chromium.Chromium + +Session bus + Filter: true + Talk: ["org.freedesktop.Notifications" "org.freedesktop.FileManager1" "org.freedesktop.ScreenSaver" "org.freedesktop.secrets" "org.kde.kwalletd5" "org.kde.kwalletd6" "org.gnome.SessionManager"] + Own: ["org.chromium.Chromium.*" "org.mpris.MediaPlayer2.org.chromium.Chromium.*" "org.mpris.MediaPlayer2.chromium.*"] + Call: map["org.freedesktop.portal.*":"*"] + Broadcast: map["org.freedesktop.portal.*":"@/org/freedesktop/portal/*"] + +System bus + Filter: true + Talk: ["org.bluez" "org.freedesktop.Avahi" "org.freedesktop.UPower"] + +`}, + {"instance pd", testState, new(fst.Config), false, false, `Warning: this configuration uses permissive defaults! + +State + Instance: 8e2c76b066dabe574cf073bdb46eb5c1 (3735928559) + Uptime: 1h2m32s + +App + ID: 0 + Enablements: (No enablements) + Directory: + Command: + +`}, + + {"json nil", nil, nil, false, true, `null +`}, + {"json instance", testState, nil, false, true, `{ + "instance": [ + 142, + 44, + 118, + 176, + 102, + 218, + 190, + 87, + 76, + 240, + 115, + 189, + 180, + 110, + 181, + 193 + ], + "pid": 3735928559, + "config": { + "id": "org.chromium.Chromium", + "command": [ + "chromium", + "--ignore-gpu-blocklist", + "--disable-smooth-scrolling", + "--enable-features=UseOzonePlatform", + "--ozone-platform=wayland" + ], + "confinement": { + "app_id": 9, + "groups": [ + "video" + ], + "username": "chronos", + "home_inner": "/var/lib/fortify", + "home": "/var/lib/persist/home/org.chromium.Chromium", + "sandbox": { + "hostname": "localhost", + "userns": true, + "net": true, + "dev": true, + "syscall": { + "compat": false, + "deny_devel": true, + "multiarch": true, + "linux32": false, + "can": false, + "bluetooth": false + }, + "no_new_session": true, + "map_real_uid": true, + "env": { + "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY", + "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com", + "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT" + }, + "filesystem": [ + { + "src": "/nix/store" + }, + { + "src": "/run/current-system" + }, + { + "src": "/run/opengl-driver" + }, + { + "src": "/var/db/nix-channels" + }, + { + "dst": "/data/data/org.chromium.Chromium", + "src": "/var/lib/fortify/u0/org.chromium.Chromium", + "write": true, + "require": true + }, + { + "src": "/dev/dri", + "dev": true + } + ], + "symlink": [ + [ + "/run/user/65534", + "/run/user/150" + ] + ], + "etc": "/etc", + "auto_etc": true, + "override": [ + "/var/run/nscd" + ] + }, + "extra_perms": [ + { + "ensure": true, + "path": "/var/lib/fortify/u0", + "x": true + }, + { + "path": "/var/lib/fortify/u0/org.chromium.Chromium", + "r": true, + "w": true, + "x": true + } + ], + "system_bus": { + "see": null, + "talk": [ + "org.bluez", + "org.freedesktop.Avahi", + "org.freedesktop.UPower" + ], + "own": null, + "call": null, + "broadcast": null, + "filter": true + }, + "session_bus": { + "see": null, + "talk": [ + "org.freedesktop.Notifications", + "org.freedesktop.FileManager1", + "org.freedesktop.ScreenSaver", + "org.freedesktop.secrets", + "org.kde.kwalletd5", + "org.kde.kwalletd6", + "org.gnome.SessionManager" + ], + "own": [ + "org.chromium.Chromium.*", + "org.mpris.MediaPlayer2.org.chromium.Chromium.*", + "org.mpris.MediaPlayer2.chromium.*" + ], + "call": { + "org.freedesktop.portal.*": "*" + }, + "broadcast": { + "org.freedesktop.portal.*": "@/org/freedesktop/portal/*" + }, + "filter": true + }, + "enablements": 13 + } + }, + "time": "1970-01-01T00:00:00.000000009Z" +} +`}, + {"json config", nil, fst.Template(), false, true, `{ + "id": "org.chromium.Chromium", + "command": [ + "chromium", + "--ignore-gpu-blocklist", + "--disable-smooth-scrolling", + "--enable-features=UseOzonePlatform", + "--ozone-platform=wayland" + ], + "confinement": { + "app_id": 9, + "groups": [ + "video" + ], + "username": "chronos", + "home_inner": "/var/lib/fortify", + "home": "/var/lib/persist/home/org.chromium.Chromium", + "sandbox": { + "hostname": "localhost", + "userns": true, + "net": true, + "dev": true, + "syscall": { + "compat": false, + "deny_devel": true, + "multiarch": true, + "linux32": false, + "can": false, + "bluetooth": false + }, + "no_new_session": true, + "map_real_uid": true, + "env": { + "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY", + "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com", + "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT" + }, + "filesystem": [ + { + "src": "/nix/store" + }, + { + "src": "/run/current-system" + }, + { + "src": "/run/opengl-driver" + }, + { + "src": "/var/db/nix-channels" + }, + { + "dst": "/data/data/org.chromium.Chromium", + "src": "/var/lib/fortify/u0/org.chromium.Chromium", + "write": true, + "require": true + }, + { + "src": "/dev/dri", + "dev": true + } + ], + "symlink": [ + [ + "/run/user/65534", + "/run/user/150" + ] + ], + "etc": "/etc", + "auto_etc": true, + "override": [ + "/var/run/nscd" + ] + }, + "extra_perms": [ + { + "ensure": true, + "path": "/var/lib/fortify/u0", + "x": true + }, + { + "path": "/var/lib/fortify/u0/org.chromium.Chromium", + "r": true, + "w": true, + "x": true + } + ], + "system_bus": { + "see": null, + "talk": [ + "org.bluez", + "org.freedesktop.Avahi", + "org.freedesktop.UPower" + ], + "own": null, + "call": null, + "broadcast": null, + "filter": true + }, + "session_bus": { + "see": null, + "talk": [ + "org.freedesktop.Notifications", + "org.freedesktop.FileManager1", + "org.freedesktop.ScreenSaver", + "org.freedesktop.secrets", + "org.kde.kwalletd5", + "org.kde.kwalletd6", + "org.gnome.SessionManager" + ], + "own": [ + "org.chromium.Chromium.*", + "org.mpris.MediaPlayer2.org.chromium.Chromium.*", + "org.mpris.MediaPlayer2.chromium.*" + ], + "call": { + "org.freedesktop.portal.*": "*" + }, + "broadcast": { + "org.freedesktop.portal.*": "@/org/freedesktop/portal/*" + }, + "filter": true + }, + "enablements": 13 + } +} +`}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + { + v := flagJSON + t.Cleanup(func() { flagJSON = v }) + flagJSON = tc.json + } + + output := new(strings.Builder) + printShowInstance(output, testTime, tc.instance, tc.config, tc.short) + if got := output.String(); got != tc.want { + t.Errorf("printShowInstance: got\n%s\nwant\n%s", + got, tc.want) + return + } + }) + } +} + +func Test_printPs(t *testing.T) { + testCases := []struct { + name string + entries state.Entries + short, json bool + want string + }{ + {"no entries", make(state.Entries), false, false, ` Instance PID App Uptime Enablements Command + +`}, + {"no entries short", make(state.Entries), true, false, ``}, + {"nil instance", state.Entries{testID: nil}, false, false, ` Instance PID App Uptime Enablements Command + +`}, + {"state corruption", state.Entries{fst.ID{}: testState}, false, false, ` Instance PID App Uptime Enablements Command + +`}, + + {"valid", state.Entries{testID: testState}, false, false, ` Instance PID App Uptime Enablements Command + 8e2c76b0 3735928559 9 1h2m32s Wayland, D-Bus, PulseAudio ["chromium" "--ignore-gpu-blocklist" "--disable-smooth-scrolling" "--enable-features=UseOzonePlatform" "--ozone-platform=wayland"] + +`}, + {"valid short", state.Entries{testID: testState}, true, false, `8e2c76b0 +`}, + {"valid json", state.Entries{testID: testState}, false, true, `{ + "8e2c76b066dabe574cf073bdb46eb5c1": { + "instance": [ + 142, + 44, + 118, + 176, + 102, + 218, + 190, + 87, + 76, + 240, + 115, + 189, + 180, + 110, + 181, + 193 + ], + "pid": 3735928559, + "config": { + "id": "org.chromium.Chromium", + "command": [ + "chromium", + "--ignore-gpu-blocklist", + "--disable-smooth-scrolling", + "--enable-features=UseOzonePlatform", + "--ozone-platform=wayland" + ], + "confinement": { + "app_id": 9, + "groups": [ + "video" + ], + "username": "chronos", + "home_inner": "/var/lib/fortify", + "home": "/var/lib/persist/home/org.chromium.Chromium", + "sandbox": { + "hostname": "localhost", + "userns": true, + "net": true, + "dev": true, + "syscall": { + "compat": false, + "deny_devel": true, + "multiarch": true, + "linux32": false, + "can": false, + "bluetooth": false + }, + "no_new_session": true, + "map_real_uid": true, + "env": { + "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY", + "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com", + "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT" + }, + "filesystem": [ + { + "src": "/nix/store" + }, + { + "src": "/run/current-system" + }, + { + "src": "/run/opengl-driver" + }, + { + "src": "/var/db/nix-channels" + }, + { + "dst": "/data/data/org.chromium.Chromium", + "src": "/var/lib/fortify/u0/org.chromium.Chromium", + "write": true, + "require": true + }, + { + "src": "/dev/dri", + "dev": true + } + ], + "symlink": [ + [ + "/run/user/65534", + "/run/user/150" + ] + ], + "etc": "/etc", + "auto_etc": true, + "override": [ + "/var/run/nscd" + ] + }, + "extra_perms": [ + { + "ensure": true, + "path": "/var/lib/fortify/u0", + "x": true + }, + { + "path": "/var/lib/fortify/u0/org.chromium.Chromium", + "r": true, + "w": true, + "x": true + } + ], + "system_bus": { + "see": null, + "talk": [ + "org.bluez", + "org.freedesktop.Avahi", + "org.freedesktop.UPower" + ], + "own": null, + "call": null, + "broadcast": null, + "filter": true + }, + "session_bus": { + "see": null, + "talk": [ + "org.freedesktop.Notifications", + "org.freedesktop.FileManager1", + "org.freedesktop.ScreenSaver", + "org.freedesktop.secrets", + "org.kde.kwalletd5", + "org.kde.kwalletd6", + "org.gnome.SessionManager" + ], + "own": [ + "org.chromium.Chromium.*", + "org.mpris.MediaPlayer2.org.chromium.Chromium.*", + "org.mpris.MediaPlayer2.chromium.*" + ], + "call": { + "org.freedesktop.portal.*": "*" + }, + "broadcast": { + "org.freedesktop.portal.*": "@/org/freedesktop/portal/*" + }, + "filter": true + }, + "enablements": 13 + } + }, + "time": "1970-01-01T00:00:00.000000009Z" + } +} +`}, + {"valid short json", state.Entries{testID: testState}, true, true, `["8e2c76b066dabe574cf073bdb46eb5c1"] +`}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + { + v := flagJSON + t.Cleanup(func() { flagJSON = v }) + flagJSON = tc.json + } + + output := new(strings.Builder) + printPs(output, testTime, stubStore(tc.entries), tc.short) + if got := output.String(); got != tc.want { + t.Errorf("printPs: got\n%s\nwant\n%s", + got, tc.want) + return + } + }) + } +} + +// stubStore implements [state.Store] and returns test samples via [state.Joiner]. +type stubStore state.Entries + +func (s stubStore) Join() (state.Entries, error) { return state.Entries(s), nil } +func (s stubStore) Do(int, func(c state.Cursor)) (bool, error) { panic("unreachable") } +func (s stubStore) List() ([]int, error) { panic("unreachable") } +func (s stubStore) Close() error { return nil }