mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
* fix(sandbox): classify helper-tool errors with usefulerror Sandbox helper commands (profile lint/diff/show/init/list) used to bubble up plain fmt.Errorf chains from the registry layer, which the TUI then classified as Unknown and decorated with a bug-report link. Wrap each error path at the cmd/sandbox boundary so the TUI prints NotFound, InvalidArgument, or PermissionDenied with actionable hints instead. Closes #269 * refactor(sandbox): classify registry errors via sentinel wrapping Replace the fragile substring match in profileLoadError with errors.Is against new sandbox.ErrProfileNotFound / sandbox.ErrProfileInvalid sentinels. Every fmt.Errorf in registry.go that previously communicated "missing" or "malformed" by message text now wraps the corresponding sentinel, so the cmd layer can classify without inspecting strings. * fix(sandbox): detect IO error class when wrapping helper errors Replace static ErrCodeUnknown / ErrCodePermissionDenied wrappings with ioErrorCode, which inspects the error chain for fs.ErrPermission and fs.ErrNotExist before falling back. Applied to runProfileList (where an unreadable user profile directory now classifies as PermissionDenied), registryInitError, and the stat/MkdirAll/WriteFile paths in profile init. Also drop redundant doc comments on helpers whose names are self-evident. --------- Co-authored-by: Claude <noreply@anthropic.com>
217 lines
5.5 KiB
Go
217 lines
5.5 KiB
Go
package sandbox
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
|
|
"github.com/safedep/dry/log"
|
|
"github.com/safedep/pmg/internal/ui"
|
|
pmgsandbox "github.com/safedep/pmg/sandbox"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
const ExitCodeLintFail = 2
|
|
|
|
type profileLintOptions struct {
|
|
strict bool
|
|
verbose bool
|
|
jsonOut bool
|
|
}
|
|
|
|
func newProfileLintCommand(factory registryFactory) *cobra.Command {
|
|
opts := &profileLintOptions{}
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "lint <path|name>",
|
|
Short: "Lint a sandbox profile for schema issues, overly broad rules, and conflicts",
|
|
Example: " pmg sandbox profile lint npm-restrictive\n pmg sandbox profile lint ./my-profile.yml --strict",
|
|
Args: cobra.ExactArgs(1),
|
|
SilenceErrors: false,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
err := runProfileLint(cmd.OutOrStdout(), args[0], opts, factory)
|
|
if err != nil {
|
|
if _, isFail := err.(*lintFailError); isFail {
|
|
cmd.SilenceErrors = true
|
|
cmd.SilenceUsage = true
|
|
return err
|
|
}
|
|
return sandboxErrorExit(cmd, err)
|
|
}
|
|
return err
|
|
},
|
|
}
|
|
|
|
cmd.Flags().BoolVar(&opts.strict, "strict", false, "Treat warnings as errors (non-zero exit)")
|
|
cmd.Flags().BoolVar(&opts.verbose, "verbose", false, "Include info-level issues")
|
|
cmd.Flags().BoolVar(&opts.jsonOut, "json", false, "Emit lint results as JSON")
|
|
return cmd
|
|
}
|
|
|
|
type lintFailError struct{}
|
|
|
|
func (e *lintFailError) Error() string { return "" }
|
|
func (e *lintFailError) ExitCode() int { return ExitCodeLintFail }
|
|
|
|
func runProfileLint(out io.Writer, name string, opts *profileLintOptions, factory registryFactory) error {
|
|
registry, err := factory()
|
|
if err != nil {
|
|
return registryInitError(err)
|
|
}
|
|
|
|
policy, resolvedName, err := resolveProfileForLint(name, registry)
|
|
if err != nil {
|
|
return profileLoadError(err)
|
|
}
|
|
|
|
issues := pmgsandbox.LintProfile(policy)
|
|
if !opts.verbose {
|
|
issues = filterInfo(issues)
|
|
}
|
|
|
|
if opts.jsonOut {
|
|
if err := writeLintJSON(out, resolvedName, issues); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if err := renderLintHuman(out, resolvedName, issues); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if shouldFailLint(issues, opts.strict) {
|
|
return &lintFailError{}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func resolveProfileForLint(name string, registry pmgsandbox.ProfileRegistry) (*pmgsandbox.SandboxPolicy, string, error) {
|
|
if _, ok := registry.BuiltinProfileYAML(name); ok {
|
|
policy, err := registry.GetProfile(name)
|
|
if err != nil {
|
|
return nil, "", profileLoadError(err)
|
|
}
|
|
return policy, name, nil
|
|
}
|
|
|
|
summaries, err := registry.ListProfiles()
|
|
if err != nil {
|
|
log.Warnf("sandbox: failed to enumerate user profiles: %v", err)
|
|
}
|
|
for _, s := range summaries {
|
|
if s.Source == pmgsandbox.ProfileSourceUser && s.Name == name {
|
|
policy, loadErr := registry.LoadCustomProfile(s.Path)
|
|
if loadErr != nil {
|
|
return nil, "", profileLoadError(loadErr)
|
|
}
|
|
return policy, s.Path, nil
|
|
}
|
|
}
|
|
|
|
if _, statErr := os.Stat(name); statErr == nil {
|
|
policy, loadErr := registry.LoadCustomProfile(name)
|
|
if loadErr != nil {
|
|
return nil, "", profileLoadError(loadErr)
|
|
}
|
|
return policy, name, nil
|
|
}
|
|
|
|
return nil, "", notFoundError(
|
|
fmt.Sprintf("sandbox profile not found: %s", name),
|
|
"Use `pmg sandbox profile list` to see available profiles, or pass an existing profile YAML path.",
|
|
)
|
|
}
|
|
|
|
func filterInfo(issues []pmgsandbox.LintIssue) []pmgsandbox.LintIssue {
|
|
out := make([]pmgsandbox.LintIssue, 0, len(issues))
|
|
for _, i := range issues {
|
|
if i.Level == pmgsandbox.LintLevelInfo {
|
|
continue
|
|
}
|
|
out = append(out, i)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func shouldFailLint(issues []pmgsandbox.LintIssue, strict bool) bool {
|
|
for _, i := range issues {
|
|
if i.Level == pmgsandbox.LintLevelError {
|
|
return true
|
|
}
|
|
if strict && i.Level == pmgsandbox.LintLevelWarn {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
type jsonLintReport struct {
|
|
Profile string `json:"profile"`
|
|
Issues []pmgsandbox.LintIssue `json:"issues"`
|
|
}
|
|
|
|
func writeLintJSON(out io.Writer, profile string, issues []pmgsandbox.LintIssue) error {
|
|
if issues == nil {
|
|
issues = []pmgsandbox.LintIssue{}
|
|
}
|
|
return writeJSONIndent(out, jsonLintReport{Profile: profile, Issues: issues})
|
|
}
|
|
|
|
func renderLintHuman(out io.Writer, profile string, issues []pmgsandbox.LintIssue) error {
|
|
if _, err := fmt.Fprintln(out); err != nil {
|
|
return err
|
|
}
|
|
if _, err := fmt.Fprintln(out, ui.Colors.Cyan("Profile Lint: ")+ui.Colors.Bold(profile)); err != nil {
|
|
return err
|
|
}
|
|
if _, err := fmt.Fprintln(out, ui.Colors.Normal("---------------")); err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(issues) == 0 {
|
|
_, err := fmt.Fprintln(out, ui.Colors.Green("OK")+" no issues found")
|
|
return err
|
|
}
|
|
|
|
rows := make([][]string, 0, len(issues)+1)
|
|
rows = append(rows, []string{
|
|
ui.Colors.Bold("LEVEL"),
|
|
ui.Colors.Bold("CODE"),
|
|
ui.Colors.Bold("FIELD"),
|
|
ui.Colors.Bold("MESSAGE"),
|
|
})
|
|
for _, i := range issues {
|
|
rows = append(rows, []string{
|
|
lintLevelBadge(i.Level),
|
|
i.Code,
|
|
i.Field,
|
|
i.Message,
|
|
})
|
|
}
|
|
|
|
indent := firstColumnIndent(rows)
|
|
return renderTable(out, rows, func(dataIdx int) error {
|
|
if dataIdx < 0 {
|
|
return nil
|
|
}
|
|
issue := issues[dataIdx]
|
|
if issue.Rule == "" {
|
|
return nil
|
|
}
|
|
_, err := fmt.Fprintf(out, "%s%s %s\n", indent, ui.Colors.Dim("rule:"), ui.Colors.Dim(issue.Rule))
|
|
return err
|
|
})
|
|
}
|
|
|
|
func lintLevelBadge(level pmgsandbox.LintLevel) string {
|
|
switch level {
|
|
case pmgsandbox.LintLevelError:
|
|
return ui.Colors.Red("ERROR")
|
|
case pmgsandbox.LintLevelWarn:
|
|
return ui.Colors.Yellow("WARN")
|
|
case pmgsandbox.LintLevelInfo:
|
|
return ui.Colors.Dim("INFO")
|
|
}
|
|
return string(level)
|
|
}
|