Files
pmg/internal/ui/proxy_presenter.go
Sahil BansalandGitHub 46803f8e70 fix(doctor): stop npm protection passing when the real package manager is absent (#378)
`pmg setup doctor` reported npm protection as OK on a machine with no npm
installed. Two compounding defects:

1. The availability gate used a plain `exec.LookPath`, which resolves the PMG
   shim on PATH rather than the real binary, so the "skip" branch never fired.
   It now uses `shim.ResolveRealBinary` (PATH with shim dirs stripped), matching
   the runner, so a missing real binary correctly yields WARN "not available".

2. The result was inferred purely from a non-zero exit, so PackageManagerNotFound
   (exit 127) and other failures were misread as a successful block. The check
   now requires PMG's block headline in the captured output before reporting
   PASS; other non-zero exits report WARN with the error surfaced.

The block headline is extracted into `ui.MalwareBlockedHeadline` so the doctor's
marker stays in sync with what PMG prints across its block-output sites.
2026-07-15 09:11:52 +05:30

72 lines
2.4 KiB
Go

package ui
import (
"fmt"
"strings"
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/safedep/pmg/proxy"
)
// ecosystemLabel maps the ecosystem enum to the label users know the
// registry by (npm, pypi, go, ...) instead of the raw enum name.
func ecosystemLabel(ecosystem packagev1.Ecosystem) string {
return strings.ToLower(strings.TrimPrefix(ecosystem.String(), "ECOSYSTEM_"))
}
// ProxyPresenter composes all user-facing text authored by the proxy layer.
// Interceptors return structured decisions; any new proxy-emitted message
// belongs here, not in the proxy layer.
type ProxyPresenter struct {
// Advisory returns the org-configured advisory message appended to
// policy block messages. Read at render time so config changes apply
// to subsequent blocks. nil means no advisory.
Advisory func() string
}
// BlockMessage renders the response body for a blocked proxy request
// from the interceptor's structured block decision.
func (p ProxyPresenter) BlockMessage(reason proxy.BlockReason, blockCtx *proxy.BlockContext) string {
if blockCtx == nil {
return ""
}
ecosystem := ecosystemLabel(blockCtx.Ecosystem)
var message string
switch reason {
case proxy.BlockReasonMalware, proxy.BlockReasonUserDeclined:
prefix := MalwareBlockedHeadline
if reason == proxy.BlockReasonUserDeclined {
prefix = "Installation blocked by user"
}
message = fmt.Sprintf("%s: %s/%s@%s\n\nReason: %s",
prefix, ecosystem, blockCtx.PackageName, blockCtx.PackageVersion, blockCtx.MalwareSummary)
if blockCtx.MalwareReferenceURL != "" {
message += "\n\nReference: " + blockCtx.MalwareReferenceURL
}
case proxy.BlockReasonConfirmationFailed:
// Operational failure rather than a policy decision; the advisory
// message is intentionally not appended.
return fmt.Sprintf("Failed to get user confirmation for suspicious package %s/%s@%s",
ecosystem, blockCtx.PackageName, blockCtx.PackageVersion)
case proxy.BlockReasonDependencyCooldown:
message = fmt.Sprintf("Package blocked by dependency cooldown: %s/%s@%s\n\nPublished %d day(s) ago; cooldown window is %d day(s) (%d remaining).",
ecosystem, blockCtx.PackageName, blockCtx.PackageVersion,
blockCtx.CooldownDaysAgo, blockCtx.CooldownDays, blockCtx.CooldownDaysLeft)
default:
return ""
}
if p.Advisory != nil {
if advisory := p.Advisory(); advisory != "" {
message += "\n\n" + advisory
}
}
return message
}