feat: advisory message appended to block output (#362)

* docs(specs): add custom block messages and package blocklist spec

* docs(specs): add custom block messages and package blocklist implementation plan

* feat(config): add blocked_packages list and custom block messages

* feat(audit): add package_blocklist_blocked event and blocklist model

* feat(proxy): block blocklisted packages in the policy gate before analysis

* feat(guard): block blocklisted packages before trust skip and analysis

* feat(ui): render blocklist blocks and custom messages, fix silent-mode block output

* feat(proxy): append custom messages to malware and go-cooldown block bodies

* test(proxye2e): cover blocklist enforcement and custom block messages

* docs(specs): remove spec and plan documents

* refactor: drop guard-flow blocklist enforcement and trim docs

Guard mode is being deprecated; the blocklist is enforced in proxy mode
only. Remove the trusted_packages mirroring references outside the docs.

* refactor(config): consolidate blocklist and block message under top-level block section

Replace dependency_cooldown.message, malware.message and blocked_packages
with a single block section: block.message is appended to every block
output regardless of which control blocked, and block.packages is the
package blocklist.

* fix(ui): render block.message as info note with clean spacing

* fix(ui): indent wrapped continuation lines in block reasons and messages

* update config template

* refactor(config): replace block section with top-level advisory_message

Remove the package blocklist (will be implemented as part of policies in
the future) and replace block.message with an optional top-level
advisory_message appended to every block output.

* chore(config): move advisory_message near top-level scalar configs in template
This commit is contained in:
Sahil Bansal
2026-07-09 17:49:00 +05:30
committed by GitHub
parent c601e17cdc
commit 2d938ea381
17 changed files with 378 additions and 93 deletions
+8 -9
View File
@@ -10,7 +10,6 @@ import (
_ "embed"
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/safedep/dry/log"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/dry/utils"
@@ -82,6 +81,10 @@ type Config struct {
// TrustedPackages allows for trusting a suspicious package and ignoring the suspicious behaviour for the package in future installations
TrustedPackages []TrustedPackage `mapstructure:"trusted_packages"`
// AdvisoryMessage is an optional org-specific message appended to every
// block output, regardless of which control blocked the installation.
AdvisoryMessage string `mapstructure:"advisory_message"`
// SkipEventLogging allows for skipping event logging.
SkipEventLogging bool `mapstructure:"skip_event_logging"`
@@ -289,12 +292,7 @@ type TrustedPackage struct {
Purl string `mapstructure:"purl"`
Reason string `mapstructure:"reason"`
// Pre-parsed PURL components (not serialized, computed at load time)
// These fields avoid repeated PURL parsing on every IsTrustedPackage() call
parsed bool
ecosystem packagev1.Ecosystem
name string
version string
purlRef
}
// RuntimeConfig is the configuration that is used at runtime. It contains static configuration
@@ -468,6 +466,7 @@ func DefaultConfig() RuntimeConfig {
EventLogRetentionDays: 7,
SkipEventLogging: false,
TrustedPackages: []TrustedPackage{},
AdvisoryMessage: "",
ProxyMode: true,
Verbosity: VerbosityNormal,
Sandbox: SandboxConfig{
@@ -596,8 +595,8 @@ func initConfig() {
loadConfig()
if err := preprocessTrustedPackages(&globalConfig.Config); err != nil {
log.Warnf("Failed to preprocess trusted packages: %v", err)
if err := preprocessPackageRefs(&globalConfig.Config); err != nil {
log.Warnf("Failed to preprocess package refs: %v", err)
}
}
+7
View File
@@ -32,6 +32,13 @@ skip_event_logging: false
# This is the number of days to retain event logs.
event_log_retention_days: 7
# Optional message appended to every block output, regardless of which control
# blocked the installation (malware analysis, dependency cooldown, etc.).
# Useful for org deployments to point developers at internal policy docs or a
# security contact. Example:
# advisory_message: "Blocked by ACME security policy. Questions? #security-help"
advisory_message: ""
# Proxy configuration.
# When enabled, PMG uses a proxy-based interception approach instead of the
# default guard-based analysis. The proxy intercepts package manager requests in real-time
+2
View File
@@ -34,6 +34,7 @@ func TestTemplateParsesAsYAML(t *testing.T) {
assert.False(t, false, cfg.SkipEventLogging, "expected SkipEventLogging false")
assert.Equal(t, 7, cfg.EventLogRetentionDays, "expected EventLogRetentionDays 7")
assert.Len(t, cfg.TrustedPackages, 1)
assert.Empty(t, cfg.AdvisoryMessage)
}
func TestTemplateMatchesDefaults(t *testing.T) {
@@ -66,6 +67,7 @@ func TestTemplateMatchesDefaults(t *testing.T) {
assert.Equal(t, def.DependencyCooldown.Enabled, parsed.DependencyCooldown.Enabled, "dependency_cooldown.enabled mismatch")
assert.Equal(t, def.DependencyCooldown.Days, parsed.DependencyCooldown.Days, "dependency_cooldown.days mismatch")
assert.Equal(t, def.AdvisoryMessage, parsed.AdvisoryMessage, "advisory_message mismatch")
assert.Equal(t, def.Cloud.Enabled, parsed.Cloud.Enabled, "cloud.enabled mismatch")
}
+4 -4
View File
@@ -19,7 +19,7 @@ func TestIsTrustedPackageRef(t *testing.T) {
{Purl: "pkg:npm/all-versions"},
{Purl: "pkg:npm/pinned@1.0.0"},
}}
_ = preprocessTrustedPackages(cfg)
_ = preprocessPackageRefs(cfg)
setGlobalForTest(t, cfg)
assert.True(t, IsTrustedPackageRef(packagev1.Ecosystem_ECOSYSTEM_NPM, "all-versions", "9.9.9"))
@@ -33,7 +33,7 @@ func TestIsTrustedPackageAllVersions(t *testing.T) {
{Purl: "pkg:npm/all-versions"},
{Purl: "pkg:npm/pinned@1.0.0"},
}}
_ = preprocessTrustedPackages(cfg)
_ = preprocessPackageRefs(cfg)
setGlobalForTest(t, cfg)
assert.True(t, IsTrustedPackageAllVersions(packagev1.Ecosystem_ECOSYSTEM_NPM, "all-versions"))
@@ -130,7 +130,7 @@ func TestCooldownSkip(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &Config{DependencyCooldown: DependencyCooldownConfig{Skip: tt.skip}}
_ = preprocessTrustedPackages(cfg)
_ = preprocessPackageRefs(cfg)
got := cooldownSkip(cfg.DependencyCooldown.Skip, tt.ecosystem, tt.pkgName)
assert.Equal(t, tt.wantSkipAll, got.SkipAll)
@@ -156,7 +156,7 @@ func TestCooldownSkipIsSkipListOnly(t *testing.T) {
TrustedPackages: []TrustedPackage{{Purl: "pkg:npm/trusted-only"}},
DependencyCooldown: DependencyCooldownConfig{Skip: []TrustedPackage{{Purl: "pkg:npm/cooldown-only"}}},
}
_ = preprocessTrustedPackages(cfg)
_ = preprocessPackageRefs(cfg)
setGlobalForTest(t, cfg)
assert.False(t, CooldownSkip(packagev1.Ecosystem_ECOSYSTEM_NPM, "trusted-only").SkipAll, "trusted_packages must not leak into CooldownSkip")
+49
View File
@@ -0,0 +1,49 @@
package config
import (
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/safedep/dry/api/pb"
"github.com/safedep/dry/log"
)
// purlRef is the pre-parsed form of a PURL list entry (trusted_packages,
// dependency_cooldown.skip). Parsing happens once at config load; entries
// with an invalid PURL are marked unparsed and never match.
type purlRef struct {
parsed bool
ecosystem packagev1.Ecosystem
name string
version string
}
func (r *purlRef) parseFrom(purl string) {
parsedPurl, err := pb.NewPurlPackageVersion(purl)
if err != nil {
log.Warnf("Failed to parse package PURL: %s: %v", purl, err)
r.parsed = false
return
}
r.parsed = true
r.ecosystem = parsedPurl.Ecosystem()
r.name = parsedPurl.Name()
r.version = parsedPurl.Version()
}
// matches reports whether the ref matches a package version. A version-less
// ref matches every version of the package.
func (r purlRef) matches(pv *packagev1.PackageVersion) bool {
if !r.parsed || pv == nil {
return false
}
if r.ecosystem != pv.GetPackage().GetEcosystem() {
return false
}
if r.name != pv.GetPackage().GetName() {
return false
}
if r.version != "" && r.version != pv.GetVersion() {
return false
}
return true
}
+17 -60
View File
@@ -2,8 +2,6 @@ package config
import (
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/safedep/dry/api/pb"
"github.com/safedep/dry/log"
)
// IsTrustedPackage checks if a package version is trusted based on global configuration.
@@ -84,42 +82,24 @@ func cooldownSkip(skip []TrustedPackage, ecosystem packagev1.Ecosystem, name str
return info
}
// PreprocessTrustedPackages pre-parses all PURL strings in the trusted package
// lists. Exported for use in cross-package tests that install synthetic configs
// without going through Load.
func PreprocessTrustedPackages(cfg *Config) error {
return preprocessTrustedPackages(cfg)
// PreprocessPackageRefs pre-parses all PURL strings in the trusted and
// cooldown skip package lists. Exported for use in cross-package tests that
// install synthetic configs without going through Load.
func PreprocessPackageRefs(cfg *Config) error {
return preprocessPackageRefs(cfg)
}
// preprocessTrustedPackages pre-parses all PURL strings in the trusted package
// lists (both the top-level guardrail list and the cooldown-exemption list).
// This is called once during config load to avoid repeated parsing during
// trusted package checks. Invalid PURLs are logged but not fatal.
func preprocessTrustedPackages(cfg *Config) error {
preprocessTrustedPackageList(cfg.TrustedPackages)
preprocessTrustedPackageList(cfg.DependencyCooldown.Skip)
return nil
}
// preprocessTrustedPackageList parses the PURL of each entry in place, populating
// the pre-parsed ecosystem/name/version fields. Entries with an invalid PURL are
// marked unparsed (and skipped at match time) rather than failing the load.
func preprocessTrustedPackageList(packages []TrustedPackage) {
for i := range packages {
tp := &packages[i]
parsedPurl, err := pb.NewPurlPackageVersion(tp.Purl)
if err != nil {
log.Warnf("Failed to parse trusted package PURL: %s: %v", tp.Purl, err)
tp.parsed = false
continue
}
tp.parsed = true
tp.ecosystem = parsedPurl.Ecosystem()
tp.name = parsedPurl.Name()
tp.version = parsedPurl.Version()
// preprocessPackageRefs parses the PURL of each list entry in place, populating
// the pre-parsed purlRef. This is called once during config load to avoid
// repeated parsing at match time. Invalid PURLs are logged but not fatal.
func preprocessPackageRefs(cfg *Config) error {
for i := range cfg.TrustedPackages {
cfg.TrustedPackages[i].parseFrom(cfg.TrustedPackages[i].Purl)
}
for i := range cfg.DependencyCooldown.Skip {
cfg.DependencyCooldown.Skip[i].parseFrom(cfg.DependencyCooldown.Skip[i].Purl)
}
return nil
}
// isTrustedPackageVersion checks if a package version is in the trusted packages list.
@@ -128,33 +108,10 @@ func preprocessTrustedPackageList(packages []TrustedPackage) {
// If the trusted package PURL doesn't specify a version, all versions of that package are trusted.
// Returns false if pkgVersion is nil or if trustedPackages is empty.
func isTrustedPackageVersion(trustedPackages []TrustedPackage, pkgVersion *packagev1.PackageVersion) bool {
if pkgVersion == nil {
return false
}
if len(trustedPackages) == 0 {
return false
}
for _, v := range trustedPackages {
if !v.parsed {
continue
if v.matches(pkgVersion) {
return true
}
if v.ecosystem != pkgVersion.GetPackage().GetEcosystem() {
continue
}
if v.name != pkgVersion.GetPackage().GetName() {
continue
}
if v.version != "" && v.version != pkgVersion.GetVersion() {
continue
}
return true
}
return false
}
+1 -1
View File
@@ -258,7 +258,7 @@ func TestIsTrustedPackageVersion(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
// Pre-process trusted packages to populate pre-parsed fields
cfg := &Config{TrustedPackages: tt.trustedPackages}
_ = preprocessTrustedPackages(cfg)
_ = preprocessPackageRefs(cfg)
got := isTrustedPackageVersion(cfg.TrustedPackages, tt.pkgVersion)
assert.Equal(t, tt.want, got)
+4
View File
@@ -18,6 +18,10 @@ dependency_cooldown:
days: 5
```
To show an org-specific message whenever PMG blocks an installation (cooldown
or otherwise), see the top-level `advisory_message` in the
[config template](../config/config.template.yml).
## Exempting Specific Packages
Some packages — typically first-party or internal — need to be installed as soon
+1
View File
@@ -291,6 +291,7 @@ func (f *proxyFlow) Run(ctx context.Context, args []string, parsedCmd *packagema
reportData.BlockedPackages = statsCollector.GetBlockedPackages()
reportData.ConfirmedPackages = statsCollector.GetConfirmedPackages()
reportData.CooldownBlockedPackages = statsCollector.GetCooldownBlocks()
reportData.AdvisoryMessage = cfg.Config.AdvisoryMessage
// Set outcome based on execution result using shared inference logic
reportData.Outcome = inferOutcome(cfg.InsecureInstallation, cfg.DryRun, reportData.BlockedCount, stats.UserCancelledCount, executionError)
+35 -9
View File
@@ -82,6 +82,10 @@ type ReportData struct {
// Packages blocked by the dependency cooldown policy (proxy mode only)
CooldownBlockedPackages []models.CooldownBlock
// AdvisoryMessage is the optional org-configured message appended to block
// output regardless of which control blocked. Set from advisory_message.
AdvisoryMessage string
// Configuration context
FlowType FlowType
DryRun bool
@@ -135,10 +139,27 @@ func Report(data *ReportData) {
}
}
// reportSilent only shows output on errors or blocks
// Normal successful execution produces no output
func printMalwareBlockSection(data *ReportData) {
if len(data.BlockedPackages) == 0 {
return
}
fmt.Println()
fmt.Printf("%s %s\n", Colors.Red("✗"), Colors.Red("Malicious package blocked"))
printMaliciousPackagesList(data.BlockedPackages)
fmt.Println()
}
// reportSilent shows output only when the install was blocked: silent mode
// hides PMG except for errors and malicious package detection. Cooldown-only
// blocks stay hidden, matching the documented silent contract.
func reportSilent(data *ReportData) {
// Silent mode: no report output
if data.Outcome != OutcomeBlocked || len(data.BlockedPackages) == 0 {
return
}
printMalwareBlockSection(data)
printAdvisoryMessage(data.AdvisoryMessage)
}
// reportNormal shows minimal, assuring output
@@ -176,12 +197,7 @@ func reportNormal(data *ReportData) {
switch data.Outcome {
case OutcomeBlocked:
if len(data.BlockedPackages) > 0 {
fmt.Println()
fmt.Printf("%s %s\n", Colors.Red("✗"), Colors.Red("Malicious package blocked"))
printMaliciousPackagesList(data.BlockedPackages)
fmt.Println()
}
printMalwareBlockSection(data)
if len(data.CooldownBlockedPackages) > 0 {
fmt.Println()
@@ -193,6 +209,11 @@ func reportNormal(data *ReportData) {
fmt.Println()
}
if data.AdvisoryMessage != "" {
printAdvisoryMessage(data.AdvisoryMessage)
fmt.Println()
}
onlyCooldown := len(data.BlockedPackages) == 0 && len(data.CooldownBlockedPackages) > 0
if onlyCooldown {
icon = Colors.Yellow("⊘")
@@ -307,6 +328,11 @@ func reportVerbose(data *ReportData) {
}
}
if data.Outcome == OutcomeBlocked && data.AdvisoryMessage != "" {
fmt.Println()
printAdvisoryMessage(data.AdvisoryMessage)
}
fmt.Println()
}
+138
View File
@@ -0,0 +1,138 @@
package ui
import (
"io"
"os"
"strings"
"testing"
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/safedep/pmg/analyzer"
"github.com/safedep/pmg/internal/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func captureStdout(t *testing.T, fn func()) string {
t.Helper()
old := os.Stdout
r, w, err := os.Pipe()
require.NoError(t, err)
os.Stdout = w
defer func() { os.Stdout = old }()
fn()
require.NoError(t, w.Close())
out, err := io.ReadAll(r)
require.NoError(t, err)
return string(out)
}
func malwareBlockedData() *ReportData {
data := NewReportData()
data.TotalAnalyzed = 1
data.BlockedCount = 1
data.Outcome = OutcomeBlocked
data.BlockedPackages = []*analyzer.PackageVersionAnalysisResult{
{
PackageVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{Name: "evil", Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM},
Version: "1.0.0",
},
Summary: "verified malware",
},
}
return data
}
func TestReportNormalMalwareAdvisoryMessage(t *testing.T) {
withVerbosity(t, VerbosityLevelNormal)
data := malwareBlockedData()
data.AdvisoryMessage = "Contact #security-help"
out := captureStdout(t, func() { Report(data) })
assert.Contains(t, out, "Malicious package blocked")
assert.Contains(t, out, " Contact #security-help")
assert.NotContains(t, out, "\n\n\n", "no double blank lines in block output")
}
func TestReportNormalNoAdvisoryMessageWhenUnset(t *testing.T) {
withVerbosity(t, VerbosityLevelNormal)
out := captureStdout(t, func() { Report(malwareBlockedData()) })
assert.Contains(t, out, "Malicious package blocked")
assert.NotContains(t, out, "Contact #security-help")
}
func TestReportNormalCooldownAdvisoryMessage(t *testing.T) {
withVerbosity(t, VerbosityLevelNormal)
data := NewReportData()
data.TotalAnalyzed = 1
data.BlockedCount = 1
data.Outcome = OutcomeBlocked
data.CooldownBlockedPackages = []models.CooldownBlock{{Name: "fresh", Version: "2.0.0", DaysAgo: 1, DaysLeft: 4, CooldownDays: 5}}
data.AdvisoryMessage = "Request an exemption at go/pmg-exceptions"
out := captureStdout(t, func() { Report(data) })
assert.Contains(t, out, "Dependency cooldown")
assert.Contains(t, out, "Request an exemption at go/pmg-exceptions")
}
func TestReportSilentRendersBlocks(t *testing.T) {
withVerbosity(t, VerbosityLevelSilent)
data := malwareBlockedData()
data.AdvisoryMessage = "Contact #security-help"
out := captureStdout(t, func() { Report(data) })
assert.Contains(t, out, "Malicious package blocked")
assert.Contains(t, out, "Contact #security-help")
}
func TestReportSilentQuietOnSuccess(t *testing.T) {
withVerbosity(t, VerbosityLevelSilent)
data := NewReportData()
data.TotalAnalyzed = 3
out := captureStdout(t, func() { Report(data) })
assert.Empty(t, out)
}
func TestReportSilentCooldownOnlyStaysQuiet(t *testing.T) {
withVerbosity(t, VerbosityLevelSilent)
data := NewReportData()
data.TotalAnalyzed = 1
data.BlockedCount = 1
data.Outcome = OutcomeBlocked
data.CooldownBlockedPackages = []models.CooldownBlock{{Name: "fresh", Version: "2.0.0"}}
out := captureStdout(t, func() { Report(data) })
assert.Empty(t, out)
}
func TestReportVerboseAdvisoryMessage(t *testing.T) {
withVerbosity(t, VerbosityLevelVerbose)
data := malwareBlockedData()
data.AdvisoryMessage = "Contact #security-help"
out := captureStdout(t, func() { Report(data) })
assert.Contains(t, out, "Installation blocked")
assert.Contains(t, out, " Contact #security-help")
}
func TestTermWidthFormatTextIndent(t *testing.T) {
text := strings.Repeat("word ", 40)
out := termWidthFormatTextIndent(text, 20, " ")
lines := strings.Split(out, "\n")
require.Greater(t, len(lines), 1)
for _, line := range lines[1:] {
assert.True(t, strings.HasPrefix(line, " "), "wrapped line must be indented: %q", line)
}
}
+17 -1
View File
@@ -196,7 +196,7 @@ func printMaliciousPackagesList(malwarePackages []*analyzer.PackageVersionAnalys
mp.PackageVersion.GetVersion())))
if verbosityLevel == VerbosityLevelVerbose {
fmt.Printf(" %s\n", Colors.Dim(termWidthFormatText(mp.Summary, 76)))
fmt.Printf(" %s\n", Colors.Dim(termWidthFormatTextIndent(mp.Summary, 76, " ")))
}
if mp.ReferenceURL != "" {
@@ -228,6 +228,16 @@ func printCooldownPackagesList(packages []models.CooldownBlock) {
}
}
// printAdvisoryMessage renders the org-configured advisory_message as an info
// note attached to the block output. Callers are responsible for surrounding
// blank lines. No-op when the message is empty.
func printAdvisoryMessage(message string) {
if message == "" {
return
}
fmt.Printf(" %s %s\n", Colors.Cyan(""), Colors.Cyan(termWidthFormatTextIndent(message, 76, " ")))
}
func pluralizeDays(n int) string {
if n == 1 {
return "1 day"
@@ -242,6 +252,12 @@ func pluralizePackages(n int) string {
return fmt.Sprintf("%d packages", n)
}
// termWidthFormatTextIndent wraps text at maxWidth and indents continuation
// lines so wrapped output stays aligned with the first line.
func termWidthFormatTextIndent(text string, maxWidth int, indent string) string {
return strings.ReplaceAll(termWidthFormatText(text, maxWidth), "\n", "\n"+indent)
}
// Format the string to be maximum maxWidth. Use newlines to wrap the text.
func termWidthFormatText(text string, maxWidth int) string {
// Replace all newlines with spaces so that we can split the text into words
+13 -4
View File
@@ -101,6 +101,15 @@ func (b *baseRegistryInterceptor) fastAllow(
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
// This method is ecosystem-agnostic and can be used by any registry interceptor
func (b *baseRegistryInterceptor) analyzePackage(
@@ -194,11 +203,11 @@ func (b *baseRegistryInterceptor) handleAnalysisResult(
b.statsCollector.RecordBlocked(result)
}
message := fmt.Sprintf("Malicious package blocked: %s/%s@%s\n\nReason: %s\n\nReference: %s",
message := appendAdvisoryMessage(fmt.Sprintf("Malicious package blocked: %s/%s@%s\n\nReason: %s\n\nReference: %s",
ecosystem.String(),
packageName, packageVersion,
result.Summary,
result.ReferenceURL)
result.ReferenceURL), config.Get().Config.AdvisoryMessage)
return &proxy.InterceptorResponse{
Action: proxy.ActionBlock,
@@ -233,11 +242,11 @@ func (b *baseRegistryInterceptor) handleAnalysisResult(
b.statsCollector.RecordUserCancelled(result)
}
message := fmt.Sprintf("Installation blocked by user: %s/%s@%s\n\nReason: %s\n\nReference: %s",
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)
result.ReferenceURL), config.Get().Config.AdvisoryMessage)
return &proxy.InterceptorResponse{
Action: proxy.ActionBlock,
+30 -2
View File
@@ -18,10 +18,10 @@ func setTrustedPackagesForTest(t *testing.T, pkgs []pmgconfig.TrustedPackage) {
t.Helper()
orig := pmgconfig.Get().Config.TrustedPackages
pmgconfig.Get().Config.TrustedPackages = pkgs
require.NoError(t, pmgconfig.PreprocessTrustedPackages(&pmgconfig.Get().Config), "setTrustedPackagesForTest: preprocess")
require.NoError(t, pmgconfig.PreprocessPackageRefs(&pmgconfig.Get().Config), "setTrustedPackagesForTest: preprocess")
t.Cleanup(func() {
pmgconfig.Get().Config.TrustedPackages = orig
assert.NoError(t, pmgconfig.PreprocessTrustedPackages(&pmgconfig.Get().Config))
assert.NoError(t, pmgconfig.PreprocessPackageRefs(&pmgconfig.Get().Config))
})
}
@@ -204,3 +204,31 @@ func TestBaseRegistryInterceptor_HandleAnalysisResult(t *testing.T) {
})
}
}
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")
}
+4 -2
View File
@@ -126,8 +126,10 @@ func (h *goCooldownHandler) CheckZipDownload(ctx *proxy.RequestContext, baseURL,
pv.SetVersion(version)
audit.LogDependencyCooldown(pv, publishTime, cooldownDays, daysAgo, daysLeft)
message := 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)
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{
Action: proxy.ActionBlock,
+46
View File
@@ -567,3 +567,49 @@ func TestProxyFlow_Go(t *testing.T) {
},
})
}
func blockedBody(res ExecResult) string {
for _, r := range res.Requests {
if r.Blocked {
return r.Body
}
}
return ""
}
func TestProxyFlow_AdvisoryMessage(t *testing.T) {
RunCases(t, []TestCase{
{
Name: "malware block body carries advisory message",
Config: func(rc *config.RuntimeConfig) {
rc.Config.AdvisoryMessage = "Report false positives in #security-help"
},
Setup: func(h *Harness) {
h.Registry.AddNpm(NpmPackage{Name: "evil", DistTagLatest: "1.0.0",
Versions: []NpmVersion{{Version: "1.0.0", PublishedAt: old()}}})
h.Analyzer.SetNpm("evil", "1.0.0", VerifiedMalware())
},
Exec: func(h *Harness) ExecResult { return h.Npm().Install("evil", "1.0.0") },
Assert: func(t *testing.T, h *Harness, res ExecResult) {
assert.True(t, res.Blocked())
assert.Contains(t, blockedBody(res), "Report false positives in #security-help")
},
},
{
Name: "go cooldown block body carries advisory message",
Config: func(rc *config.RuntimeConfig) {
rc.Config.DependencyCooldown = config.DependencyCooldownConfig{Enabled: true, Days: 7}
rc.Config.AdvisoryMessage = "Request an exemption at go/pmg-exceptions"
},
Setup: func(h *Harness) {
h.Registry.AddGoModule(GoModule{Path: "example.com/fresh",
Versions: []GoVersion{{Version: "v1.0.0", PublishedAt: recent()}}})
},
Exec: func(h *Harness) ExecResult { return h.Go().Install("example.com/fresh", "v1.0.0") },
Assert: func(t *testing.T, h *Harness, res ExecResult) {
assert.True(t, res.Blocked())
assert.Contains(t, blockedBody(res), "Request an exemption at go/pmg-exceptions")
},
},
})
}
+2 -1
View File
@@ -59,12 +59,13 @@ func applyConfig(t *testing.T, override func(rc *config.RuntimeConfig)) {
rc.Config.Paranoid = false
rc.Config.TrustedPackages = nil
rc.Config.DependencyCooldown = config.DependencyCooldownConfig{}
rc.Config.AdvisoryMessage = ""
if override != nil {
override(rc)
}
if err := config.PreprocessTrustedPackages(&rc.Config); err != nil {
if err := config.PreprocessPackageRefs(&rc.Config); err != nil {
t.Fatalf("failed to preprocess trusted packages: %v", err)
}
}