mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
* feat: add doctor check runner core types and logic * feat: add doctor checks for config, binary, directory, and aliases * feat: add doctor checks for sandbox and security features * feat: add protection verification check using test malicious packages * feat: add summarized package manager availability check * feat: add pmg setup doctor command with compact output * feat: add PATH shim verification to doctor command * fix: improve doctor command UX and alias detection - Capitalize all check messages for consistent output - Dim passing checks, color warn/fail for visual clarity - Silence empty Cobra error output on doctor failure - Remove redundant pmg binary check (self-evident) - Fix alias IsInstalled to skip commented-out source lines - Improve protection failure message * refactor: remove package manager availability check from doctor * fix: handle os.RemoveAll error in doctor protection check * refactor: use table layout for setup doctor, extract shared table renderer Move renderTable, truncate, and visibleWidth helpers from cmd/sandbox to internal/ui so both sandbox and setup doctor share them. Rewrite setup doctor output to use the same table structure as sandbox doctor. Fix VisibleWidth to count runes instead of bytes for correct alignment with multi-byte UTF-8 characters. * docs: add pmg setup doctor to README, remove manual verification step * refactor: use constants for check names, rename and inline doctor helpers Address PR review comments: extract check name constants, rename CheckConfigFile to CheckFileExists and CheckDirectoryWritable to CheckDirectoryExists for reusability, inline trivial wrappers (CheckSandbox, CheckSecurityFeature, CheckProxyMode), and add fix hints for all checks with correct config keys. * refactor: inline simple doctor checks into command layer * fix: skip protection check when aliases and shims are inactive Protection checks now fail immediately when shell aliases and shims are both inactive, instead of falsely passing by running through the pmg binary directly. Also clean up summary messages to remove redundant fix hints and truncated paths.
144 lines
3.6 KiB
Go
144 lines
3.6 KiB
Go
package sandbox
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
|
|
"github.com/safedep/dry/usefulerror"
|
|
"github.com/safedep/pmg/errcodes"
|
|
"github.com/safedep/pmg/internal/ui"
|
|
pmgsandbox "github.com/safedep/pmg/sandbox"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
const ExitCodeProbeFailure = 2
|
|
|
|
var sandboxErrorExit = func(_ *cobra.Command, err error) error {
|
|
type exitCoder interface{ ExitCode() int }
|
|
if ec, ok := err.(exitCoder); ok {
|
|
ui.ErrorExitWithCode(err, ec.ExitCode())
|
|
return nil
|
|
}
|
|
|
|
ui.ErrorExit(err)
|
|
return nil
|
|
}
|
|
|
|
var validDrivers = map[pmgsandbox.DriverName]struct{}{
|
|
pmgsandbox.DriverSeatbelt: {},
|
|
pmgsandbox.DriverBubblewrap: {},
|
|
pmgsandbox.DriverLandlock: {},
|
|
}
|
|
|
|
func validateDriver(name string) error {
|
|
if name == "" {
|
|
return nil
|
|
}
|
|
if _, ok := validDrivers[pmgsandbox.DriverName(name)]; !ok {
|
|
return invalidArgumentError(
|
|
fmt.Sprintf("unknown driver %q", name),
|
|
"Use one of: seatbelt, bubblewrap, landlock",
|
|
)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func invalidArgumentError(message, help string) error {
|
|
return usefulerror.NewUsefulError().
|
|
WithCode(errcodes.InvalidArgument).
|
|
WithHumanError(message).
|
|
WithHelp(help).
|
|
Wrap(errors.New(message))
|
|
}
|
|
|
|
func notFoundError(message, help string) error {
|
|
return usefulerror.NewUsefulError().
|
|
WithCode(errcodes.NotFound).
|
|
WithHumanError(message).
|
|
WithHelp(help).
|
|
Wrap(errors.New(message))
|
|
}
|
|
|
|
// Idempotent: returns err unchanged when nil or already useful, so call
|
|
// sites can apply it without losing more precise pre-classified errors.
|
|
func wrapUseful(err error, code, help string) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if hasUsefulError(err) {
|
|
return err
|
|
}
|
|
return usefulerror.NewUsefulError().
|
|
WithCode(code).
|
|
WithHumanError(err.Error()).
|
|
WithHelp(help).
|
|
Wrap(err)
|
|
}
|
|
|
|
func profileLoadError(err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if hasUsefulError(err) {
|
|
return err
|
|
}
|
|
switch {
|
|
case errors.Is(err, pmgsandbox.ErrProfileNotFound):
|
|
return wrapUseful(err, errcodes.NotFound,
|
|
"Use `pmg sandbox profile list` to see available profiles, or pass an existing profile YAML path.")
|
|
case errors.Is(err, pmgsandbox.ErrProfileInvalid):
|
|
return wrapUseful(err, errcodes.InvalidArgument,
|
|
"Check the profile YAML for syntax/schema issues and verify any 'inherits:' parent name.")
|
|
}
|
|
return wrapUseful(err, errcodes.Unknown,
|
|
"Failed to load the sandbox profile. Run with --verbose for the underlying cause.")
|
|
}
|
|
|
|
func hasUsefulError(err error) bool {
|
|
// AsUsefulError also runs global converters for plain errors like
|
|
// fs.ErrPermission. Contextual wrappers must only skip errors that already
|
|
// carry UsefulError details, otherwise generic converters hide command help.
|
|
var usefulErr usefulerror.UsefulError
|
|
return errors.As(err, &usefulErr)
|
|
}
|
|
|
|
func registryInitError(err error) error {
|
|
return wrapUseful(err, ioErrorCode(err, errcodes.Unknown),
|
|
"Failed to initialise the sandbox profile registry. Run with --verbose for details.")
|
|
}
|
|
|
|
func ioErrorCode(err error, fallback string) string {
|
|
switch {
|
|
case errors.Is(err, fs.ErrPermission):
|
|
return errcodes.PermissionDenied
|
|
case errors.Is(err, fs.ErrNotExist):
|
|
return errcodes.NotFound
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func writeJSONIndent(out io.Writer, v any) error {
|
|
enc := json.NewEncoder(out)
|
|
enc.SetIndent("", " ")
|
|
return enc.Encode(v)
|
|
}
|
|
|
|
func renderTable(out io.Writer, rows [][]string, after func(rowIdx int) error) error {
|
|
return ui.RenderTable(out, rows, after)
|
|
}
|
|
|
|
func firstColumnIndent(rows [][]string) string {
|
|
return ui.FirstColumnIndent(rows)
|
|
}
|
|
|
|
func truncate(s string, n int) string {
|
|
return ui.Truncate(s, n)
|
|
}
|
|
|
|
func truncateLeft(s string, n int) string {
|
|
return ui.TruncateLeft(s, n)
|
|
}
|