mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
* feat: add sandbox DX commands * fix: Linter errors * fix: Sandbox deny log parsing * fix: Sandbox docs * refactor: Maintain SSOT across pkg dependencies * fix: Linter errors
288 lines
7.2 KiB
Go
288 lines
7.2 KiB
Go
package sandbox
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/safedep/pmg/internal/ui"
|
|
pmgsandbox "github.com/safedep/pmg/sandbox"
|
|
"github.com/safedep/pmg/sandbox/platform"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
type doctorOptions struct {
|
|
jsonOut bool
|
|
driver string
|
|
}
|
|
|
|
func NewDoctorCommand() *cobra.Command {
|
|
return newDoctorCommand(platform.DefaultProbes)
|
|
}
|
|
|
|
func newDoctorCommand(probes func() []pmgsandbox.Probe) *cobra.Command {
|
|
opts := &doctorOptions{}
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "doctor",
|
|
Short: "Run sandbox probes and report on host readiness",
|
|
Example: " pmg sandbox doctor\n pmg sandbox doctor --driver landlock",
|
|
Args: cobra.NoArgs,
|
|
SilenceErrors: false,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
err := runDoctor(cmd.Context(), cmd.OutOrStdout(), opts, probes)
|
|
if err != nil {
|
|
if _, isFail := err.(*doctorFailError); isFail {
|
|
cmd.SilenceErrors = true
|
|
cmd.SilenceUsage = true
|
|
return err
|
|
}
|
|
return sandboxErrorExit(cmd, err)
|
|
}
|
|
return err
|
|
},
|
|
}
|
|
|
|
cmd.Flags().BoolVar(&opts.jsonOut, "json", false, "Emit probe results as JSON")
|
|
cmd.Flags().StringVar(&opts.driver, "driver", "", "Filter probes to a single driver: seatbelt|bubblewrap|landlock")
|
|
return cmd
|
|
}
|
|
|
|
func runDoctor(ctx context.Context, out io.Writer, opts *doctorOptions, probes func() []pmgsandbox.Probe) error {
|
|
if err := validateDriver(opts.driver); err != nil {
|
|
return err
|
|
}
|
|
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
|
|
all := probes()
|
|
filtered := filterByDriver(all, opts.driver)
|
|
results := pmgsandbox.RunProbes(ctx, filtered)
|
|
|
|
if opts.jsonOut {
|
|
if err := writeJSON(out, results); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if err := renderHuman(out, results); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if exitCodeForResults(results) != 0 {
|
|
return &doctorFailError{}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type doctorFailError struct{}
|
|
|
|
func (e *doctorFailError) Error() string { return "" }
|
|
func (e *doctorFailError) ExitCode() int { return ExitCodeProbeFailure }
|
|
|
|
func exitCodeForResults(results []pmgsandbox.ProbeResult) int {
|
|
for _, r := range results {
|
|
if r.Status == pmgsandbox.ProbeStatusFail {
|
|
return ExitCodeProbeFailure
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// driverProbeNames lists which probes belong to each driver. The apparmor
|
|
// probe is shared by bubblewrap and landlock (both rely on unprivileged user
|
|
// namespaces on Linux) and excluded from the darwin-only seatbelt filter.
|
|
var driverProbeNames = map[pmgsandbox.DriverName]map[string]struct{}{
|
|
pmgsandbox.DriverSeatbelt: {
|
|
pmgsandbox.ProbeSeatbeltDriver: {},
|
|
pmgsandbox.ProbeSeatbeltCanary: {},
|
|
},
|
|
pmgsandbox.DriverBubblewrap: {
|
|
pmgsandbox.ProbeBwrapDriver: {},
|
|
pmgsandbox.ProbeBwrapCanary: {},
|
|
pmgsandbox.ProbeAppArmorUserns: {},
|
|
},
|
|
pmgsandbox.DriverLandlock: {
|
|
pmgsandbox.ProbeLandlockDriver: {},
|
|
pmgsandbox.ProbeLandlockCanary: {},
|
|
pmgsandbox.ProbeAppArmorUserns: {},
|
|
},
|
|
}
|
|
|
|
func filterByDriver(probes []pmgsandbox.Probe, driver string) []pmgsandbox.Probe {
|
|
if driver == "" {
|
|
return probes
|
|
}
|
|
want := driverProbeNames[pmgsandbox.DriverName(driver)]
|
|
out := make([]pmgsandbox.Probe, 0, len(probes))
|
|
for _, p := range probes {
|
|
if _, ok := want[p.Name()]; ok {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
type jsonFix struct {
|
|
Description string `json:"description"`
|
|
Command string `json:"command,omitempty"`
|
|
Docs string `json:"docs,omitempty"`
|
|
}
|
|
|
|
type jsonProbeResult struct {
|
|
Name string `json:"name"`
|
|
Status string `json:"status"`
|
|
Summary string `json:"summary,omitempty"`
|
|
Detail string `json:"detail,omitempty"`
|
|
Fixes []jsonFix `json:"fixes,omitempty"`
|
|
}
|
|
|
|
type jsonReport struct {
|
|
Results []jsonProbeResult `json:"results"`
|
|
}
|
|
|
|
func writeJSON(out io.Writer, results []pmgsandbox.ProbeResult) error {
|
|
report := jsonReport{Results: make([]jsonProbeResult, 0, len(results))}
|
|
for _, r := range results {
|
|
fixes := make([]jsonFix, 0, len(r.Fixes))
|
|
for _, f := range r.Fixes {
|
|
fixes = append(fixes, jsonFix{Description: f.Description, Command: f.Command, Docs: f.Docs})
|
|
}
|
|
report.Results = append(report.Results, jsonProbeResult{
|
|
Name: r.Name,
|
|
Status: string(r.Status),
|
|
Summary: r.Summary,
|
|
Detail: r.Detail,
|
|
Fixes: fixes,
|
|
})
|
|
}
|
|
|
|
return writeJSONIndent(out, report)
|
|
}
|
|
|
|
func renderHuman(out io.Writer, results []pmgsandbox.ProbeResult) error {
|
|
if len(results) == 0 {
|
|
_, err := fmt.Fprintln(out, ui.Colors.Dim("No probes to run."))
|
|
return err
|
|
}
|
|
|
|
if _, err := fmt.Fprintln(out); err != nil {
|
|
return err
|
|
}
|
|
if _, err := fmt.Fprintln(out, ui.Colors.Cyan("Sandbox Diagnostics")); err != nil {
|
|
return err
|
|
}
|
|
if _, err := fmt.Fprintln(out, ui.Colors.Normal("--------------------")); err != nil {
|
|
return err
|
|
}
|
|
|
|
rows := make([][]string, 0, len(results)+1)
|
|
rows = append(rows, []string{
|
|
ui.Colors.Bold("STATUS"),
|
|
ui.Colors.Bold("CHECK"),
|
|
ui.Colors.Bold("SUMMARY"),
|
|
ui.Colors.Bold("FIX"),
|
|
})
|
|
for _, r := range results {
|
|
rows = append(rows, []string{
|
|
statusBadge(r.Status),
|
|
displayName(r.Name),
|
|
truncate(r.Summary, 60),
|
|
fixHint(r.Fixes),
|
|
})
|
|
}
|
|
|
|
if err := renderTable(out, rows, nil); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, r := range results {
|
|
if r.Status == pmgsandbox.ProbeStatusOK {
|
|
continue
|
|
}
|
|
if err := renderDetail(out, r); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func renderDetail(out io.Writer, r pmgsandbox.ProbeResult) error {
|
|
if _, err := fmt.Fprintln(out); err != nil {
|
|
return err
|
|
}
|
|
if _, err := fmt.Fprintf(out, "%s %s — %s\n", statusBadge(r.Status), ui.Colors.Bold(displayName(r.Name)), r.Summary); err != nil {
|
|
return err
|
|
}
|
|
if r.Detail != "" {
|
|
if _, err := fmt.Fprintf(out, " %s\n", ui.Colors.Dim(r.Detail)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for i, f := range r.Fixes {
|
|
if _, err := fmt.Fprintf(out, " %s %s\n", ui.Colors.Cyan(fmt.Sprintf("Fix %d:", i+1)), f.Description); err != nil {
|
|
return err
|
|
}
|
|
if f.Command != "" {
|
|
if _, err := fmt.Fprintf(out, " %s %s\n", ui.Colors.Dim("$"), f.Command); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if f.Docs != "" {
|
|
if _, err := fmt.Fprintf(out, " %s %s\n", ui.Colors.Dim("docs:"), f.Docs); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func statusBadge(s pmgsandbox.ProbeStatus) string {
|
|
switch s {
|
|
case pmgsandbox.ProbeStatusOK:
|
|
return ui.Colors.Green("OK")
|
|
case pmgsandbox.ProbeStatusWarn:
|
|
return ui.Colors.Yellow("WARN")
|
|
case pmgsandbox.ProbeStatusFail:
|
|
return ui.Colors.Red("FAIL")
|
|
case pmgsandbox.ProbeStatusSkipped:
|
|
return ui.Colors.Dim("SKIPPED")
|
|
default:
|
|
return string(s)
|
|
}
|
|
}
|
|
|
|
func fixHint(fixes []pmgsandbox.ProbeFix) string {
|
|
if len(fixes) == 0 {
|
|
return ui.Colors.Dim("—")
|
|
}
|
|
first := truncate(fixes[0].Description, 50)
|
|
if len(fixes) > 1 {
|
|
return fmt.Sprintf("%s %s", first, ui.Colors.Dim(fmt.Sprintf("(+%d more)", len(fixes)-1)))
|
|
}
|
|
return first
|
|
}
|
|
|
|
func displayName(probeName string) string {
|
|
switch probeName {
|
|
case pmgsandbox.ProbeSeatbeltDriver:
|
|
return "Seatbelt driver"
|
|
case pmgsandbox.ProbeBwrapDriver:
|
|
return "Bubblewrap driver"
|
|
case pmgsandbox.ProbeLandlockDriver:
|
|
return "Landlock ABI"
|
|
case pmgsandbox.ProbeAppArmorUserns:
|
|
return "AppArmor user namespaces"
|
|
case pmgsandbox.ProbeSeatbeltCanary:
|
|
return "Seatbelt canary"
|
|
case pmgsandbox.ProbeBwrapCanary:
|
|
return "Bubblewrap canary"
|
|
case pmgsandbox.ProbeLandlockCanary:
|
|
return "Landlock canary"
|
|
}
|
|
return probeName
|
|
}
|