refactor: move proxy block message rendering to presentation layer (#364)

* refactor: move proxy block message rendering to presentation layer

* refactor: introduce ui.ProxyPresenter with injected advisory source

* feat: friendly ecosystem labels in proxy block messages

* test: cover ecosystemLabel derivation

* fix: address review feedback on block context assertions and empty reference line
This commit is contained in:
Sahil Bansal
2026-07-10 17:42:26 +05:30
committed by GitHub
parent 2f1c348a06
commit 31e51b10dd
12 changed files with 358 additions and 97 deletions
+6
View File
@@ -795,6 +795,12 @@ func Get() *RuntimeConfig {
return globalConfig return globalConfig
} }
// AdvisoryMessage returns the org-configured advisory message appended to
// policy block output. Empty when not configured.
func AdvisoryMessage() string {
return globalConfig.Config.AdvisoryMessage
}
func ConfigureSandbox(mayDownloadPackages bool) { func ConfigureSandbox(mayDownloadPackages bool) {
if globalConfig.Config.Sandbox.Enabled { if globalConfig.Config.Sandbox.Enabled {
// Apply sandbox to all commands if EnforceAlways=true, otherwise only to // Apply sandbox to all commands if EnforceAlways=true, otherwise only to
+2
View File
@@ -348,6 +348,8 @@ func (f *proxyFlow) createAndStartProxyServer(
proxyConfig := proxy.DefaultProxyConfig() proxyConfig := proxy.DefaultProxyConfig()
proxyConfig.CertManager = certMgr proxyConfig.CertManager = certMgr
proxyConfig.Interceptors = interceptorsList proxyConfig.Interceptors = interceptorsList
presenter := ui.ProxyPresenter{Advisory: config.AdvisoryMessage}
proxyConfig.BlockMessageRenderer = presenter.BlockMessage
proxyServer, err := proxy.NewProxyServer(proxyConfig) proxyServer, err := proxy.NewProxyServer(proxyConfig)
if err != nil { if err != nil {
+3
View File
@@ -16,6 +16,7 @@ import (
"github.com/safedep/pmg/internal/audit" "github.com/safedep/pmg/internal/audit"
"github.com/safedep/pmg/internal/flows" "github.com/safedep/pmg/internal/flows"
"github.com/safedep/pmg/internal/localstore" "github.com/safedep/pmg/internal/localstore"
"github.com/safedep/pmg/internal/ui"
pmgproxy "github.com/safedep/pmg/proxy" pmgproxy "github.com/safedep/pmg/proxy"
"github.com/safedep/pmg/proxy/certmanager" "github.com/safedep/pmg/proxy/certmanager"
"github.com/safedep/pmg/proxy/interceptors" "github.com/safedep/pmg/proxy/interceptors"
@@ -115,6 +116,8 @@ func Run(ctx context.Context, cfg *config.RuntimeConfig, statePath, host string,
proxyConfig.ListenAddr = listenAddr(host, port) proxyConfig.ListenAddr = listenAddr(host, port)
proxyConfig.CertManager = certMgr proxyConfig.CertManager = certMgr
proxyConfig.Interceptors = interceptorList proxyConfig.Interceptors = interceptorList
presenter := ui.ProxyPresenter{Advisory: config.AdvisoryMessage}
proxyConfig.BlockMessageRenderer = presenter.BlockMessage
server, err := pmgproxy.NewProxyServer(proxyConfig) server, err := pmgproxy.NewProxyServer(proxyConfig)
if err != nil { if err != nil {
+71
View File
@@ -0,0 +1,71 @@
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 := "Malicious package blocked"
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
}
+130
View File
@@ -0,0 +1,130 @@
package ui
import (
"testing"
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/safedep/pmg/proxy"
"github.com/stretchr/testify/assert"
)
func TestEcosystemLabel(t *testing.T) {
tests := []struct {
ecosystem packagev1.Ecosystem
expected string
}{
{packagev1.Ecosystem_ECOSYSTEM_NPM, "npm"},
{packagev1.Ecosystem_ECOSYSTEM_PYPI, "pypi"},
{packagev1.Ecosystem_ECOSYSTEM_GO, "go"},
{packagev1.Ecosystem_ECOSYSTEM_RUBYGEMS, "rubygems"},
{packagev1.Ecosystem_ECOSYSTEM_GITHUB_ACTIONS, "github_actions"},
{packagev1.Ecosystem_ECOSYSTEM_UNSPECIFIED, "unspecified"},
}
for _, tt := range tests {
t.Run(tt.expected, func(t *testing.T) {
assert.Equal(t, tt.expected, ecosystemLabel(tt.ecosystem))
})
}
}
func TestProxyPresenterBlockMessage(t *testing.T) {
malwareCtx := &proxy.BlockContext{
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
PackageName: "evil",
PackageVersion: "1.0.0",
MalwareSummary: "Contains known malware",
MalwareReferenceURL: "https://example.com/malware-report",
}
tests := []struct {
name string
reason proxy.BlockReason
blockCtx *proxy.BlockContext
advisory string
expected string
}{
{
name: "malware",
reason: proxy.BlockReasonMalware,
blockCtx: malwareCtx,
expected: "Malicious package blocked: npm/evil@1.0.0\n\nReason: Contains known malware\n\nReference: https://example.com/malware-report",
},
{
name: "malware with advisory",
reason: proxy.BlockReasonMalware,
blockCtx: malwareCtx,
advisory: "Contact #security-help",
expected: "Malicious package blocked: npm/evil@1.0.0\n\nReason: Contains known malware\n\nReference: https://example.com/malware-report\n\nContact #security-help",
},
{
name: "user declined",
reason: proxy.BlockReasonUserDeclined,
blockCtx: malwareCtx,
advisory: "Contact #security-help",
expected: "Installation blocked by user: npm/evil@1.0.0\n\nReason: Contains known malware\n\nReference: https://example.com/malware-report\n\nContact #security-help",
},
{
name: "malware without reference URL omits reference line",
reason: proxy.BlockReasonMalware,
blockCtx: &proxy.BlockContext{
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
PackageName: "evil",
PackageVersion: "1.0.0",
MalwareSummary: "Contains known malware",
},
advisory: "Contact #security-help",
expected: "Malicious package blocked: npm/evil@1.0.0\n\nReason: Contains known malware\n\nContact #security-help",
},
{
name: "confirmation failed carries no advisory",
reason: proxy.BlockReasonConfirmationFailed,
blockCtx: malwareCtx,
advisory: "Contact #security-help",
expected: "Failed to get user confirmation for suspicious package npm/evil@1.0.0",
},
{
name: "dependency cooldown",
reason: proxy.BlockReasonDependencyCooldown,
blockCtx: &proxy.BlockContext{
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_GO,
PackageName: "example.com/fresh",
PackageVersion: "v1.1.0",
CooldownDays: 7,
CooldownDaysAgo: 2,
CooldownDaysLeft: 5,
},
advisory: "Request an exemption at go/pmg-exceptions",
expected: "Package blocked by dependency cooldown: go/example.com/fresh@v1.1.0\n\nPublished 2 day(s) ago; cooldown window is 7 day(s) (5 remaining).\n\nRequest an exemption at go/pmg-exceptions",
},
{
name: "nil context",
reason: proxy.BlockReasonMalware,
blockCtx: nil,
expected: "",
},
{
name: "no reason",
reason: proxy.BlockReasonNone,
blockCtx: malwareCtx,
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := ProxyPresenter{Advisory: func() string { return tt.advisory }}
assert.Equal(t, tt.expected, p.BlockMessage(tt.reason, tt.blockCtx))
})
}
}
func TestProxyPresenterNilAdvisory(t *testing.T) {
message := ProxyPresenter{}.BlockMessage(proxy.BlockReasonMalware, &proxy.BlockContext{
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
PackageName: "evil",
PackageVersion: "1.0.0",
MalwareSummary: "verified malware",
})
assert.Equal(t, "Malicious package blocked: npm/evil@1.0.0\n\nReason: verified malware", message)
}
+37 -1
View File
@@ -4,6 +4,8 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"time" "time"
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
) )
// ResponseAction determines how the proxy should handle a request // ResponseAction determines how the proxy should handle a request
@@ -41,12 +43,46 @@ type RequestContext struct {
Data map[string]interface{} Data map[string]interface{}
} }
// BlockReason identifies why an interceptor blocked a request
type BlockReason int
const (
BlockReasonNone BlockReason = iota
BlockReasonMalware
BlockReasonUserDeclined
BlockReasonConfirmationFailed
BlockReasonDependencyCooldown
)
// BlockContext carries the structured facts of a block decision so a
// presentation layer can render the user-facing message. Interceptors
// populate it instead of composing message text themselves.
type BlockContext struct {
Ecosystem packagev1.Ecosystem
PackageName string
PackageVersion string
// For BlockReasonMalware and BlockReasonUserDeclined
MalwareSummary string
MalwareReferenceURL string
// For BlockReasonDependencyCooldown
CooldownDays int
CooldownDaysAgo int
CooldownDaysLeft int
}
// InterceptorResponse defines how the proxy should handle the request // InterceptorResponse defines how the proxy should handle the request
type InterceptorResponse struct { type InterceptorResponse struct {
// Action to take // Action to take
Action ResponseAction Action ResponseAction
// For Action = Block: error message to return // For Action = Block: why and what was blocked. The proxy renders the
// response body from these via ProxyConfig.BlockMessageRenderer.
BlockReason BlockReason
BlockContext *BlockContext
// BlockMessage overrides the rendered message when non-empty
BlockMessage string BlockMessage string
BlockCode int BlockCode int
+22 -24
View File
@@ -101,15 +101,6 @@ func (b *baseRegistryInterceptor) fastAllow(
return nil, false return nil, false
} }
// appendAdvisoryMessage appends the org-configured advisory_message, when set,
// to a block message body.
func appendAdvisoryMessage(message, advisory string) string {
if advisory == "" {
return message
}
return message + "\n\n" + advisory
}
// analyzePackage analyzes a package using the configured analyzer with caching // analyzePackage analyzes a package using the configured analyzer with caching
// This method is ecosystem-agnostic and can be used by any registry interceptor // This method is ecosystem-agnostic and can be used by any registry interceptor
func (b *baseRegistryInterceptor) analyzePackage( func (b *baseRegistryInterceptor) analyzePackage(
@@ -203,16 +194,17 @@ func (b *baseRegistryInterceptor) handleAnalysisResult(
b.statsCollector.RecordBlocked(result) b.statsCollector.RecordBlocked(result)
} }
message := appendAdvisoryMessage(fmt.Sprintf("Malicious package blocked: %s/%s@%s\n\nReason: %s\n\nReference: %s",
ecosystem.String(),
packageName, packageVersion,
result.Summary,
result.ReferenceURL), config.Get().Config.AdvisoryMessage)
return &proxy.InterceptorResponse{ return &proxy.InterceptorResponse{
Action: proxy.ActionBlock, Action: proxy.ActionBlock,
BlockCode: http.StatusForbidden, BlockCode: http.StatusForbidden,
BlockMessage: message, BlockReason: proxy.BlockReasonMalware,
BlockContext: &proxy.BlockContext{
Ecosystem: ecosystem,
PackageName: packageName,
PackageVersion: packageVersion,
MalwareSummary: result.Summary,
MalwareReferenceURL: result.ReferenceURL,
},
}, nil }, nil
case analyzer.ActionConfirm: case analyzer.ActionConfirm:
@@ -229,7 +221,12 @@ func (b *baseRegistryInterceptor) handleAnalysisResult(
return &proxy.InterceptorResponse{ return &proxy.InterceptorResponse{
Action: proxy.ActionBlock, Action: proxy.ActionBlock,
BlockCode: http.StatusForbidden, BlockCode: http.StatusForbidden,
BlockMessage: fmt.Sprintf("Failed to get user confirmation for suspicious package %s/%s@%s", ecosystem.String(), packageName, packageVersion), BlockReason: proxy.BlockReasonConfirmationFailed,
BlockContext: &proxy.BlockContext{
Ecosystem: ecosystem,
PackageName: packageName,
PackageVersion: packageVersion,
},
}, nil }, nil
} }
@@ -242,16 +239,17 @@ func (b *baseRegistryInterceptor) handleAnalysisResult(
b.statsCollector.RecordUserCancelled(result) b.statsCollector.RecordUserCancelled(result)
} }
message := appendAdvisoryMessage(fmt.Sprintf("Installation blocked by user: %s/%s@%s\n\nReason: %s\n\nReference: %s",
ecosystem.String(),
packageName, packageVersion,
result.Summary,
result.ReferenceURL), config.Get().Config.AdvisoryMessage)
return &proxy.InterceptorResponse{ return &proxy.InterceptorResponse{
Action: proxy.ActionBlock, Action: proxy.ActionBlock,
BlockCode: http.StatusForbidden, BlockCode: http.StatusForbidden,
BlockMessage: message, BlockReason: proxy.BlockReasonUserDeclined,
BlockContext: &proxy.BlockContext{
Ecosystem: ecosystem,
PackageName: packageName,
PackageVersion: packageVersion,
MalwareSummary: result.Summary,
MalwareReferenceURL: result.ReferenceURL,
},
}, nil }, nil
} }
+26 -36
View File
@@ -70,7 +70,7 @@ func TestBaseRegistryInterceptor_HandleAnalysisResult(t *testing.T) {
userConfirms bool userConfirms bool
expectedAction proxy.ResponseAction expectedAction proxy.ResponseAction
expectedBlockCode int expectedBlockCode int
expectBlockMessage bool expectedBlockReason proxy.BlockReason
}{ }{
{ {
name: "ActionBlock - malicious package", name: "ActionBlock - malicious package",
@@ -84,7 +84,7 @@ func TestBaseRegistryInterceptor_HandleAnalysisResult(t *testing.T) {
}, },
expectedAction: proxy.ActionBlock, expectedAction: proxy.ActionBlock,
expectedBlockCode: http.StatusForbidden, expectedBlockCode: http.StatusForbidden,
expectBlockMessage: true, expectedBlockReason: proxy.BlockReasonMalware,
}, },
{ {
name: "ActionConfirm - user confirms installation", name: "ActionConfirm - user confirms installation",
@@ -99,7 +99,7 @@ func TestBaseRegistryInterceptor_HandleAnalysisResult(t *testing.T) {
userConfirms: true, userConfirms: true,
expectedAction: proxy.ActionAllow, expectedAction: proxy.ActionAllow,
expectedBlockCode: 0, expectedBlockCode: 0,
expectBlockMessage: false, expectedBlockReason: proxy.BlockReasonNone,
}, },
{ {
name: "ActionConfirm - user declines installation", name: "ActionConfirm - user declines installation",
@@ -114,7 +114,7 @@ func TestBaseRegistryInterceptor_HandleAnalysisResult(t *testing.T) {
userConfirms: false, userConfirms: false,
expectedAction: proxy.ActionBlock, expectedAction: proxy.ActionBlock,
expectedBlockCode: http.StatusForbidden, expectedBlockCode: http.StatusForbidden,
expectBlockMessage: true, expectedBlockReason: proxy.BlockReasonUserDeclined,
}, },
// Note: Timeout test case is skipped as it would require waiting 5 minutes // Note: Timeout test case is skipped as it would require waiting 5 minutes
// The timeout behavior is covered by the implementation but not tested here // The timeout behavior is covered by the implementation but not tested here
@@ -131,7 +131,7 @@ func TestBaseRegistryInterceptor_HandleAnalysisResult(t *testing.T) {
}, },
expectedAction: proxy.ActionAllow, expectedAction: proxy.ActionAllow,
expectedBlockCode: 0, expectedBlockCode: 0,
expectBlockMessage: false, expectedBlockReason: proxy.BlockReasonNone,
}, },
{ {
name: "ActionUnknown - default to allow", name: "ActionUnknown - default to allow",
@@ -145,7 +145,7 @@ func TestBaseRegistryInterceptor_HandleAnalysisResult(t *testing.T) {
}, },
expectedAction: proxy.ActionAllow, expectedAction: proxy.ActionAllow,
expectedBlockCode: 0, expectedBlockCode: 0,
expectBlockMessage: false, expectedBlockReason: proxy.BlockReasonNone,
}, },
{ {
name: "ActionBlock - pypi ecosystem", name: "ActionBlock - pypi ecosystem",
@@ -159,7 +159,7 @@ func TestBaseRegistryInterceptor_HandleAnalysisResult(t *testing.T) {
}, },
expectedAction: proxy.ActionBlock, expectedAction: proxy.ActionBlock,
expectedBlockCode: http.StatusForbidden, expectedBlockCode: http.StatusForbidden,
expectBlockMessage: true, expectedBlockReason: proxy.BlockReasonMalware,
}, },
} }
@@ -200,35 +200,25 @@ func TestBaseRegistryInterceptor_HandleAnalysisResult(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, tt.expectedAction, response.Action) assert.Equal(t, tt.expectedAction, response.Action)
assert.Equal(t, tt.expectedBlockCode, response.BlockCode) assert.Equal(t, tt.expectedBlockCode, response.BlockCode)
assert.Equal(t, tt.expectBlockMessage, response.BlockMessage != "") assert.Equal(t, tt.expectedBlockReason, response.BlockReason)
assert.Empty(t, response.BlockMessage)
switch tt.expectedBlockReason {
case proxy.BlockReasonNone:
assert.Nil(t, response.BlockContext)
case proxy.BlockReasonMalware, proxy.BlockReasonUserDeclined:
require.NotNil(t, response.BlockContext)
assert.Equal(t, tt.ecosystem, response.BlockContext.Ecosystem)
assert.Equal(t, tt.packageName, response.BlockContext.PackageName)
assert.Equal(t, tt.packageVersion, response.BlockContext.PackageVersion)
assert.Equal(t, tt.analysisResult.Summary, response.BlockContext.MalwareSummary)
assert.Equal(t, tt.analysisResult.ReferenceURL, response.BlockContext.MalwareReferenceURL)
default:
require.NotNil(t, response.BlockContext)
assert.Equal(t, tt.ecosystem, response.BlockContext.Ecosystem)
assert.Equal(t, tt.packageName, response.BlockContext.PackageName)
assert.Equal(t, tt.packageVersion, response.BlockContext.PackageVersion)
}
}) })
} }
} }
func TestAppendAdvisoryMessage(t *testing.T) {
assert.Equal(t, "base", appendAdvisoryMessage("base", ""))
assert.Equal(t, "base\n\ncustom", appendAdvisoryMessage("base", "custom"))
}
func TestHandleAnalysisResultBlockCarriesAdvisoryMessage(t *testing.T) {
origMsg := pmgconfig.Get().Config.AdvisoryMessage
pmgconfig.Get().Config.AdvisoryMessage = "Contact #security-help"
t.Cleanup(func() { pmgconfig.Get().Config.AdvisoryMessage = origMsg })
b := &baseRegistryInterceptor{}
ctx := makeTestRequestContext("https://registry.npmjs.org/evil/-/evil-1.0.0.tgz")
result := &analyzer.PackageVersionAnalysisResult{
PackageVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{Name: "evil", Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM},
Version: "1.0.0",
},
Action: analyzer.ActionBlock,
Summary: "verified malware",
}
resp, err := b.handleAnalysisResult(ctx, packagev1.Ecosystem_ECOSYSTEM_NPM, "evil", "1.0.0", result)
require.NoError(t, err)
assert.Equal(t, proxy.ActionBlock, resp.Action)
assert.Contains(t, resp.BlockMessage, "Contact #security-help")
}
+9 -6
View File
@@ -126,15 +126,18 @@ func (h *goCooldownHandler) CheckZipDownload(ctx *proxy.RequestContext, baseURL,
pv.SetVersion(version) pv.SetVersion(version)
audit.LogDependencyCooldown(pv, publishTime, cooldownDays, daysAgo, daysLeft) audit.LogDependencyCooldown(pv, publishTime, cooldownDays, daysAgo, daysLeft)
message := appendAdvisoryMessage(
fmt.Sprintf("Package blocked by dependency cooldown: GO/%s@%s\n\nPublished %d day(s) ago; cooldown window is %d day(s) (%d remaining).",
module, version, daysAgo, cooldownDays, daysLeft),
pmgconfig.Get().Config.AdvisoryMessage)
return &proxy.InterceptorResponse{ return &proxy.InterceptorResponse{
Action: proxy.ActionBlock, Action: proxy.ActionBlock,
BlockCode: http.StatusForbidden, BlockCode: http.StatusForbidden,
BlockMessage: message, BlockReason: proxy.BlockReasonDependencyCooldown,
BlockContext: &proxy.BlockContext{
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_GO,
PackageName: module,
PackageVersion: version,
CooldownDays: cooldownDays,
CooldownDaysAgo: daysAgo,
CooldownDaysLeft: daysLeft,
},
}, true }, true
} }
+10
View File
@@ -7,6 +7,7 @@ import (
"testing" "testing"
"time" "time"
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/safedep/pmg/proxy" "github.com/safedep/pmg/proxy"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -39,6 +40,15 @@ func TestGoCooldownCheckZipDownloadSideFetch(t *testing.T) {
require.True(t, handled) require.True(t, handled)
assert.Equal(t, proxy.ActionBlock, resp.Action) assert.Equal(t, proxy.ActionBlock, resp.Action)
assert.Equal(t, http.StatusForbidden, resp.BlockCode) assert.Equal(t, http.StatusForbidden, resp.BlockCode)
assert.Equal(t, proxy.BlockReasonDependencyCooldown, resp.BlockReason)
require.NotNil(t, resp.BlockContext)
assert.Equal(t, packagev1.Ecosystem_ECOSYSTEM_GO, resp.BlockContext.Ecosystem)
assert.Equal(t, "example.com/fresh", resp.BlockContext.PackageName)
assert.Equal(t, "v1.1.0", resp.BlockContext.PackageVersion)
assert.Equal(t, 7, resp.BlockContext.CooldownDays)
assert.Equal(t, 1, resp.BlockContext.CooldownDaysAgo)
assert.Equal(t, 6, resp.BlockContext.CooldownDaysLeft)
}) })
t.Run("fails open when the out-of-band fetch fails", func(t *testing.T) { t.Run("fails open when the out-of-band fetch fails", func(t *testing.T) {
+8
View File
@@ -62,6 +62,11 @@ type ProxyConfig struct {
// Interceptors // Interceptors
Interceptors []Interceptor Interceptors []Interceptor
// BlockMessageRenderer composes the response body for blocked requests
// from the interceptor's structured block decision. nil falls back to
// the generic block message.
BlockMessageRenderer func(BlockReason, *BlockContext) string
// Other configuration // Other configuration
EnableMITM bool EnableMITM bool
RequestTimeout time.Duration RequestTimeout time.Duration
@@ -561,6 +566,9 @@ func (ps *proxyServer) registerHandlers() {
} }
message := resp.BlockMessage message := resp.BlockMessage
if message == "" && ps.config.BlockMessageRenderer != nil {
message = ps.config.BlockMessageRenderer(resp.BlockReason, resp.BlockContext)
}
if message == "" { if message == "" {
message = "Blocked by proxy interceptor" message = "Blocked by proxy interceptor"
} }
+4
View File
@@ -14,7 +14,9 @@ import (
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1" packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/safedep/pmg/analyzer" "github.com/safedep/pmg/analyzer"
"github.com/safedep/pmg/config"
"github.com/safedep/pmg/internal/models" "github.com/safedep/pmg/internal/models"
"github.com/safedep/pmg/internal/ui"
"github.com/safedep/pmg/proxy" "github.com/safedep/pmg/proxy"
"github.com/safedep/pmg/proxy/certmanager" "github.com/safedep/pmg/proxy/certmanager"
"github.com/safedep/pmg/proxy/interceptors" "github.com/safedep/pmg/proxy/interceptors"
@@ -151,6 +153,8 @@ func buildProxy(t *testing.T, certMgr certmanager.CertificateManager, upstreamAd
cfg := proxy.DefaultProxyConfig() cfg := proxy.DefaultProxyConfig()
cfg.CertManager = certMgr cfg.CertManager = certMgr
cfg.Interceptors = interceptorList cfg.Interceptors = interceptorList
presenter := ui.ProxyPresenter{Advisory: config.AdvisoryMessage}
cfg.BlockMessageRenderer = presenter.BlockMessage
// All upstream connections — MITM'd round-trips and CONNECT tunnels for // All upstream connections — MITM'd round-trips and CONNECT tunnels for
// non-MITM hosts alike — terminate at the mock registry, so no test reaches // non-MITM hosts alike — terminate at the mock registry, so no test reaches