mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
Tighten shim detection, profile repair, and install ordering while trimming over-specific doctor/info hints from the system-install path. Co-authored-by: Cursor <cursoragent@cursor.com>
276 lines
8.0 KiB
Go
276 lines
8.0 KiB
Go
package setup
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/safedep/dry/cloud"
|
|
"github.com/safedep/dry/log"
|
|
"github.com/safedep/pmg/config"
|
|
"github.com/safedep/pmg/internal/alias"
|
|
"github.com/safedep/pmg/internal/analytics"
|
|
"github.com/safedep/pmg/internal/audit"
|
|
"github.com/safedep/pmg/internal/shim"
|
|
"github.com/safedep/pmg/internal/ui"
|
|
"github.com/safedep/pmg/internal/version"
|
|
"github.com/safedep/pmg/proxy/certmanager"
|
|
"github.com/safedep/pmg/sandbox/platform"
|
|
"github.com/safedep/pmg/truststore"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func NewInfoCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "info",
|
|
Short: "Show information about PMG setup and configuration.",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
err := executeSetupInfo()
|
|
if err != nil {
|
|
ui.ErrorExit(fmt.Errorf("failed to execute setup info: %w", err))
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
return cmd
|
|
}
|
|
|
|
func executeSetupInfo() error {
|
|
fmt.Print(ui.GeneratePMGBanner(version.Version, version.Commit))
|
|
|
|
// Configuration section
|
|
cfg := config.Get()
|
|
configEntries := make(map[string]string)
|
|
configEntries["Config File"] = cfg.ConfigFilePath()
|
|
configSource := "user"
|
|
if cfg.IsManaged() {
|
|
configSource = "global"
|
|
if cfg.IsLocked() {
|
|
configSource = "global (locked)"
|
|
}
|
|
}
|
|
configEntries["Config Source"] = configSource
|
|
configEntries["Proxy Mode"] = strconv.FormatBool(cfg.IsProxyModeEnabled())
|
|
configEntries["Proxy Install Only"] = strconv.FormatBool(cfg.Config.Proxy.InstallOnly)
|
|
ui.PrintInfoSection("Configuration", configEntries)
|
|
|
|
// Shell Integration section
|
|
aliasCfg := alias.DefaultConfig()
|
|
rcFileManager, err := alias.NewDefaultRcFileManager(aliasCfg.RcFileName)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create alias manager: %w", err)
|
|
}
|
|
|
|
aliasManager := alias.New(aliasCfg, rcFileManager)
|
|
isInstalled, err := aliasManager.IsInstalled()
|
|
if err != nil {
|
|
isInstalled = false
|
|
}
|
|
|
|
shellEntries := make(map[string]string)
|
|
shell, err := alias.DetectShell()
|
|
if err != nil {
|
|
shell = "unknown"
|
|
}
|
|
|
|
shellEntries["Detected Shell"] = shell
|
|
shellEntries["Alias Installed"] = strconv.FormatBool(isInstalled)
|
|
shellEntries["User Shims"] = strconv.FormatBool(shim.UserShimsInstalled())
|
|
if shim.SystemShimsInstalled() {
|
|
shellEntries["System Shims"] = shim.SystemBinDir()
|
|
} else {
|
|
shellEntries["System Shims"] = "not installed"
|
|
}
|
|
ui.PrintInfoSection("Shell Integration", shellEntries)
|
|
|
|
// Security section
|
|
securityEntries := make(map[string]string)
|
|
trustedPackages := cfg.Config.TrustedPackages
|
|
trustedPackagesCount := len(trustedPackages)
|
|
|
|
if trustedPackagesCount > 3 {
|
|
purls := []string{}
|
|
for _, p := range trustedPackages[0:3] {
|
|
purls = append(purls, p.Purl)
|
|
}
|
|
|
|
trustedPackagesValue := fmt.Sprintf("%v ...and %d more", purls, trustedPackagesCount-3)
|
|
securityEntries["Trusted Packages"] = trustedPackagesValue
|
|
} else if trustedPackagesCount > 0 {
|
|
purls := []string{}
|
|
for _, p := range trustedPackages {
|
|
purls = append(purls, p.Purl)
|
|
}
|
|
|
|
securityEntries["Trusted Packages"] = fmt.Sprintf("%v", purls)
|
|
} else {
|
|
securityEntries["Trusted Packages"] = "None"
|
|
}
|
|
|
|
securityEntries["Dependency Cooldown"] = strconv.FormatBool(cfg.Config.DependencyCooldown.Enabled)
|
|
securityEntries["Dependency Cooldown Days"] = strconv.Itoa(cfg.Config.DependencyCooldown.Days)
|
|
securityEntries["Telemetry"] = strconv.FormatBool(!analytics.IsDisabled())
|
|
securityEntries["Event Logging"] = strconv.FormatBool(!cfg.Config.SkipEventLogging)
|
|
securityEntries["Event Log Directory"] = cfg.EventLogDir()
|
|
|
|
ui.PrintInfoSection("Security", securityEntries)
|
|
|
|
// Sandbox section
|
|
sandboxCfg := cfg.Config.Sandbox
|
|
sandboxEntries := make(map[string]string)
|
|
sandboxEntries["Enabled"] = strconv.FormatBool(sandboxCfg.Enabled)
|
|
sandboxEntries["Enforce Always"] = strconv.FormatBool(sandboxCfg.EnforceAlways)
|
|
sandboxEntries["Driver"] = resolveSandboxDriverName()
|
|
|
|
if len(sandboxCfg.Policies) > 0 {
|
|
pmNames := make([]string, 0, len(sandboxCfg.Policies))
|
|
for name := range sandboxCfg.Policies {
|
|
pmNames = append(pmNames, name)
|
|
}
|
|
|
|
sort.Strings(pmNames)
|
|
|
|
policyParts := make([]string, 0, len(pmNames))
|
|
for _, name := range pmNames {
|
|
ref, _ := sandboxCfg.PolicyFor(name)
|
|
status := "disabled"
|
|
if ref.Enabled {
|
|
status = ref.Profile
|
|
}
|
|
|
|
policyParts = append(policyParts, fmt.Sprintf("%s(%s)", name, status))
|
|
}
|
|
|
|
sandboxEntries["Policies"] = strings.Join(policyParts, ", ")
|
|
} else {
|
|
sandboxEntries["Policies"] = "None"
|
|
}
|
|
|
|
ui.PrintInfoSection("Sandbox", sandboxEntries)
|
|
|
|
// Certificate Authority section
|
|
caStatus, _ := certmanager.InspectCA(cfg.ConfigDir())
|
|
caUser, caSystem, _ := truststore.Status(certmanager.CACommonName)
|
|
caStatus.UserTrusted, caStatus.SystemTrusted = caUser, caSystem
|
|
|
|
caEntries := make(map[string]string)
|
|
caEntries["Installed"] = strconv.FormatBool(caStatus.KeyPresent && caStatus.CertPresent)
|
|
caScope := "none"
|
|
if caStatus.SystemTrusted {
|
|
caScope = "system"
|
|
} else if caStatus.UserTrusted {
|
|
caScope = "user"
|
|
}
|
|
caEntries["Trust Scope"] = caScope
|
|
if caStatus.CertPresent {
|
|
caEntries["Expires"] = caStatus.NotAfter.Format("2006-01-02")
|
|
caEntries["Fingerprint"] = caStatus.Fingerprint
|
|
}
|
|
ui.PrintInfoSection("Certificate Authority", caEntries)
|
|
|
|
if cfg.Config.Cloud.Enabled {
|
|
cloudEntries := make(map[string]string)
|
|
cloudEntries["Enabled"] = "true"
|
|
cloudEntries["Sync DB"] = cfg.CloudSyncDBPath()
|
|
if cfg.Config.Cloud.EndpointID != "" {
|
|
cloudEntries["Endpoint ID"] = cfg.Config.Cloud.EndpointID
|
|
}
|
|
cloudEntries["Credentials"] = describeCloudCredentials()
|
|
cloudEntries["Auto Sync"] = describeAutoSync(cfg.Config.Cloud.AutoSync)
|
|
cloudEntries["Last Sync"] = describeLastSync(cfg.CloudSyncLastRunPath())
|
|
ui.PrintInfoSection("Cloud Sync", cloudEntries)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func resolveSandboxDriverName() string {
|
|
sb, err := platform.NewSandbox()
|
|
if err != nil {
|
|
return "unavailable"
|
|
}
|
|
|
|
if !sb.IsAvailable() {
|
|
return "unavailable"
|
|
}
|
|
|
|
return string(sb.Name())
|
|
}
|
|
|
|
// describeCloudCredentials reports whether SafeDep Cloud credentials can be
|
|
// resolved, and from where. The resolution order matches NewSyncClientBundle:
|
|
// keychain first, then environment variables. No network calls are made.
|
|
func describeCloudCredentials() string {
|
|
if source, ok := tryResolveKeychainCredentials(); ok {
|
|
return source
|
|
}
|
|
if source, ok := tryResolveEnvCredentials(); ok {
|
|
return source
|
|
}
|
|
return "not configured (run 'pmg cloud login' or set SAFEDEP_API_KEY and SAFEDEP_TENANT_ID)"
|
|
}
|
|
|
|
func tryResolveKeychainCredentials() (string, bool) {
|
|
resolver, err := cloud.NewKeychainCredentialResolver(cloud.CredentialTypeAPIKey)
|
|
if err != nil {
|
|
log.Debugf("keychain credential resolver unavailable: %v", err)
|
|
return "", false
|
|
}
|
|
defer func() {
|
|
if err := resolver.Close(); err != nil {
|
|
log.Warnf("failed to close keychain resolver: %v", err)
|
|
}
|
|
}()
|
|
|
|
if _, err := resolver.Resolve(); err != nil {
|
|
log.Debugf("no keychain credentials: %v", err)
|
|
return "", false
|
|
}
|
|
return "keychain", true
|
|
}
|
|
|
|
func describeAutoSync(c config.CloudAutoSyncConfig) string {
|
|
if !c.Enabled {
|
|
return "disabled"
|
|
}
|
|
return fmt.Sprintf("enabled (every %s, timeout %s)", c.MinInterval, c.Timeout)
|
|
}
|
|
|
|
// describeLastSync renders a missing or unparseable timestamp as "never" so a
|
|
// fresh install does not look broken.
|
|
func describeLastSync(path string) string {
|
|
last := audit.ReadLastSyncAttempt(path)
|
|
if last.IsZero() {
|
|
return "never"
|
|
}
|
|
|
|
delta := time.Since(last)
|
|
switch {
|
|
case delta < time.Minute:
|
|
return "just now"
|
|
case delta < time.Hour:
|
|
return fmt.Sprintf("%d minutes ago", int(delta.Minutes()))
|
|
case delta < 24*time.Hour:
|
|
return fmt.Sprintf("%d hours ago", int(delta.Hours()))
|
|
default:
|
|
return fmt.Sprintf("%d days ago", int(delta.Hours()/24))
|
|
}
|
|
}
|
|
|
|
func tryResolveEnvCredentials() (string, bool) {
|
|
resolver, err := cloud.NewEnvCredentialResolver()
|
|
if err != nil {
|
|
log.Debugf("env credential resolver unavailable: %v", err)
|
|
return "", false
|
|
}
|
|
if _, err := resolver.Resolve(); err != nil {
|
|
log.Debugf("no env credentials: %v", err)
|
|
return "", false
|
|
}
|
|
return "environment (SAFEDEP_API_KEY, SAFEDEP_TENANT_ID)", true
|
|
}
|