mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
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:
+8
-9
@@ -10,7 +10,6 @@ import (
|
|||||||
|
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
|
||||||
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
|
||||||
"github.com/safedep/dry/log"
|
"github.com/safedep/dry/log"
|
||||||
"github.com/safedep/dry/usefulerror"
|
"github.com/safedep/dry/usefulerror"
|
||||||
"github.com/safedep/dry/utils"
|
"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 allows for trusting a suspicious package and ignoring the suspicious behaviour for the package in future installations
|
||||||
TrustedPackages []TrustedPackage `mapstructure:"trusted_packages"`
|
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 allows for skipping event logging.
|
||||||
SkipEventLogging bool `mapstructure:"skip_event_logging"`
|
SkipEventLogging bool `mapstructure:"skip_event_logging"`
|
||||||
|
|
||||||
@@ -289,12 +292,7 @@ type TrustedPackage struct {
|
|||||||
Purl string `mapstructure:"purl"`
|
Purl string `mapstructure:"purl"`
|
||||||
Reason string `mapstructure:"reason"`
|
Reason string `mapstructure:"reason"`
|
||||||
|
|
||||||
// Pre-parsed PURL components (not serialized, computed at load time)
|
purlRef
|
||||||
// These fields avoid repeated PURL parsing on every IsTrustedPackage() call
|
|
||||||
parsed bool
|
|
||||||
ecosystem packagev1.Ecosystem
|
|
||||||
name string
|
|
||||||
version string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RuntimeConfig is the configuration that is used at runtime. It contains static configuration
|
// RuntimeConfig is the configuration that is used at runtime. It contains static configuration
|
||||||
@@ -468,6 +466,7 @@ func DefaultConfig() RuntimeConfig {
|
|||||||
EventLogRetentionDays: 7,
|
EventLogRetentionDays: 7,
|
||||||
SkipEventLogging: false,
|
SkipEventLogging: false,
|
||||||
TrustedPackages: []TrustedPackage{},
|
TrustedPackages: []TrustedPackage{},
|
||||||
|
AdvisoryMessage: "",
|
||||||
ProxyMode: true,
|
ProxyMode: true,
|
||||||
Verbosity: VerbosityNormal,
|
Verbosity: VerbosityNormal,
|
||||||
Sandbox: SandboxConfig{
|
Sandbox: SandboxConfig{
|
||||||
@@ -596,8 +595,8 @@ func initConfig() {
|
|||||||
|
|
||||||
loadConfig()
|
loadConfig()
|
||||||
|
|
||||||
if err := preprocessTrustedPackages(&globalConfig.Config); err != nil {
|
if err := preprocessPackageRefs(&globalConfig.Config); err != nil {
|
||||||
log.Warnf("Failed to preprocess trusted packages: %v", err)
|
log.Warnf("Failed to preprocess package refs: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,13 @@ skip_event_logging: false
|
|||||||
# This is the number of days to retain event logs.
|
# This is the number of days to retain event logs.
|
||||||
event_log_retention_days: 7
|
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.
|
# Proxy configuration.
|
||||||
# When enabled, PMG uses a proxy-based interception approach instead of the
|
# 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
|
# default guard-based analysis. The proxy intercepts package manager requests in real-time
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ func TestTemplateParsesAsYAML(t *testing.T) {
|
|||||||
assert.False(t, false, cfg.SkipEventLogging, "expected SkipEventLogging false")
|
assert.False(t, false, cfg.SkipEventLogging, "expected SkipEventLogging false")
|
||||||
assert.Equal(t, 7, cfg.EventLogRetentionDays, "expected EventLogRetentionDays 7")
|
assert.Equal(t, 7, cfg.EventLogRetentionDays, "expected EventLogRetentionDays 7")
|
||||||
assert.Len(t, cfg.TrustedPackages, 1)
|
assert.Len(t, cfg.TrustedPackages, 1)
|
||||||
|
assert.Empty(t, cfg.AdvisoryMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTemplateMatchesDefaults(t *testing.T) {
|
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.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.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")
|
assert.Equal(t, def.Cloud.Enabled, parsed.Cloud.Enabled, "cloud.enabled mismatch")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func TestIsTrustedPackageRef(t *testing.T) {
|
|||||||
{Purl: "pkg:npm/all-versions"},
|
{Purl: "pkg:npm/all-versions"},
|
||||||
{Purl: "pkg:npm/pinned@1.0.0"},
|
{Purl: "pkg:npm/pinned@1.0.0"},
|
||||||
}}
|
}}
|
||||||
_ = preprocessTrustedPackages(cfg)
|
_ = preprocessPackageRefs(cfg)
|
||||||
setGlobalForTest(t, cfg)
|
setGlobalForTest(t, cfg)
|
||||||
|
|
||||||
assert.True(t, IsTrustedPackageRef(packagev1.Ecosystem_ECOSYSTEM_NPM, "all-versions", "9.9.9"))
|
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/all-versions"},
|
||||||
{Purl: "pkg:npm/pinned@1.0.0"},
|
{Purl: "pkg:npm/pinned@1.0.0"},
|
||||||
}}
|
}}
|
||||||
_ = preprocessTrustedPackages(cfg)
|
_ = preprocessPackageRefs(cfg)
|
||||||
setGlobalForTest(t, cfg)
|
setGlobalForTest(t, cfg)
|
||||||
|
|
||||||
assert.True(t, IsTrustedPackageAllVersions(packagev1.Ecosystem_ECOSYSTEM_NPM, "all-versions"))
|
assert.True(t, IsTrustedPackageAllVersions(packagev1.Ecosystem_ECOSYSTEM_NPM, "all-versions"))
|
||||||
@@ -130,7 +130,7 @@ func TestCooldownSkip(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
cfg := &Config{DependencyCooldown: DependencyCooldownConfig{Skip: tt.skip}}
|
cfg := &Config{DependencyCooldown: DependencyCooldownConfig{Skip: tt.skip}}
|
||||||
_ = preprocessTrustedPackages(cfg)
|
_ = preprocessPackageRefs(cfg)
|
||||||
|
|
||||||
got := cooldownSkip(cfg.DependencyCooldown.Skip, tt.ecosystem, tt.pkgName)
|
got := cooldownSkip(cfg.DependencyCooldown.Skip, tt.ecosystem, tt.pkgName)
|
||||||
assert.Equal(t, tt.wantSkipAll, got.SkipAll)
|
assert.Equal(t, tt.wantSkipAll, got.SkipAll)
|
||||||
@@ -156,7 +156,7 @@ func TestCooldownSkipIsSkipListOnly(t *testing.T) {
|
|||||||
TrustedPackages: []TrustedPackage{{Purl: "pkg:npm/trusted-only"}},
|
TrustedPackages: []TrustedPackage{{Purl: "pkg:npm/trusted-only"}},
|
||||||
DependencyCooldown: DependencyCooldownConfig{Skip: []TrustedPackage{{Purl: "pkg:npm/cooldown-only"}}},
|
DependencyCooldown: DependencyCooldownConfig{Skip: []TrustedPackage{{Purl: "pkg:npm/cooldown-only"}}},
|
||||||
}
|
}
|
||||||
_ = preprocessTrustedPackages(cfg)
|
_ = preprocessPackageRefs(cfg)
|
||||||
setGlobalForTest(t, cfg)
|
setGlobalForTest(t, cfg)
|
||||||
|
|
||||||
assert.False(t, CooldownSkip(packagev1.Ecosystem_ECOSYSTEM_NPM, "trusted-only").SkipAll, "trusted_packages must not leak into CooldownSkip")
|
assert.False(t, CooldownSkip(packagev1.Ecosystem_ECOSYSTEM_NPM, "trusted-only").SkipAll, "trusted_packages must not leak into CooldownSkip")
|
||||||
|
|||||||
@@ -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
@@ -2,8 +2,6 @@ package config
|
|||||||
|
|
||||||
import (
|
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/dry/api/pb"
|
|
||||||
"github.com/safedep/dry/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// IsTrustedPackage checks if a package version is trusted based on global configuration.
|
// 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
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
// PreprocessTrustedPackages pre-parses all PURL strings in the trusted package
|
// PreprocessPackageRefs pre-parses all PURL strings in the trusted and
|
||||||
// lists. Exported for use in cross-package tests that install synthetic configs
|
// cooldown skip package lists. Exported for use in cross-package tests that
|
||||||
// without going through Load.
|
// install synthetic configs without going through Load.
|
||||||
func PreprocessTrustedPackages(cfg *Config) error {
|
func PreprocessPackageRefs(cfg *Config) error {
|
||||||
return preprocessTrustedPackages(cfg)
|
return preprocessPackageRefs(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// preprocessTrustedPackages pre-parses all PURL strings in the trusted package
|
// preprocessPackageRefs parses the PURL of each list entry in place, populating
|
||||||
// lists (both the top-level guardrail list and the cooldown-exemption list).
|
// the pre-parsed purlRef. This is called once during config load to avoid
|
||||||
// This is called once during config load to avoid repeated parsing during
|
// repeated parsing at match time. Invalid PURLs are logged but not fatal.
|
||||||
// trusted package checks. Invalid PURLs are logged but not fatal.
|
func preprocessPackageRefs(cfg *Config) error {
|
||||||
func preprocessTrustedPackages(cfg *Config) error {
|
for i := range cfg.TrustedPackages {
|
||||||
preprocessTrustedPackageList(cfg.TrustedPackages)
|
cfg.TrustedPackages[i].parseFrom(cfg.TrustedPackages[i].Purl)
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
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.
|
// 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.
|
// 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.
|
// Returns false if pkgVersion is nil or if trustedPackages is empty.
|
||||||
func isTrustedPackageVersion(trustedPackages []TrustedPackage, pkgVersion *packagev1.PackageVersion) bool {
|
func isTrustedPackageVersion(trustedPackages []TrustedPackage, pkgVersion *packagev1.PackageVersion) bool {
|
||||||
if pkgVersion == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(trustedPackages) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, v := range trustedPackages {
|
for _, v := range trustedPackages {
|
||||||
if !v.parsed {
|
if v.matches(pkgVersion) {
|
||||||
continue
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ func TestIsTrustedPackageVersion(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// Pre-process trusted packages to populate pre-parsed fields
|
// Pre-process trusted packages to populate pre-parsed fields
|
||||||
cfg := &Config{TrustedPackages: tt.trustedPackages}
|
cfg := &Config{TrustedPackages: tt.trustedPackages}
|
||||||
_ = preprocessTrustedPackages(cfg)
|
_ = preprocessPackageRefs(cfg)
|
||||||
|
|
||||||
got := isTrustedPackageVersion(cfg.TrustedPackages, tt.pkgVersion)
|
got := isTrustedPackageVersion(cfg.TrustedPackages, tt.pkgVersion)
|
||||||
assert.Equal(t, tt.want, got)
|
assert.Equal(t, tt.want, got)
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ dependency_cooldown:
|
|||||||
days: 5
|
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
|
## Exempting Specific Packages
|
||||||
|
|
||||||
Some packages — typically first-party or internal — need to be installed as soon
|
Some packages — typically first-party or internal — need to be installed as soon
|
||||||
|
|||||||
@@ -291,6 +291,7 @@ func (f *proxyFlow) Run(ctx context.Context, args []string, parsedCmd *packagema
|
|||||||
reportData.BlockedPackages = statsCollector.GetBlockedPackages()
|
reportData.BlockedPackages = statsCollector.GetBlockedPackages()
|
||||||
reportData.ConfirmedPackages = statsCollector.GetConfirmedPackages()
|
reportData.ConfirmedPackages = statsCollector.GetConfirmedPackages()
|
||||||
reportData.CooldownBlockedPackages = statsCollector.GetCooldownBlocks()
|
reportData.CooldownBlockedPackages = statsCollector.GetCooldownBlocks()
|
||||||
|
reportData.AdvisoryMessage = cfg.Config.AdvisoryMessage
|
||||||
|
|
||||||
// Set outcome based on execution result using shared inference logic
|
// Set outcome based on execution result using shared inference logic
|
||||||
reportData.Outcome = inferOutcome(cfg.InsecureInstallation, cfg.DryRun, reportData.BlockedCount, stats.UserCancelledCount, executionError)
|
reportData.Outcome = inferOutcome(cfg.InsecureInstallation, cfg.DryRun, reportData.BlockedCount, stats.UserCancelledCount, executionError)
|
||||||
|
|||||||
+35
-9
@@ -82,6 +82,10 @@ type ReportData struct {
|
|||||||
// Packages blocked by the dependency cooldown policy (proxy mode only)
|
// Packages blocked by the dependency cooldown policy (proxy mode only)
|
||||||
CooldownBlockedPackages []models.CooldownBlock
|
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
|
// Configuration context
|
||||||
FlowType FlowType
|
FlowType FlowType
|
||||||
DryRun bool
|
DryRun bool
|
||||||
@@ -135,10 +139,27 @@ func Report(data *ReportData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// reportSilent only shows output on errors or blocks
|
func printMalwareBlockSection(data *ReportData) {
|
||||||
// Normal successful execution produces no output
|
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) {
|
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
|
// reportNormal shows minimal, assuring output
|
||||||
@@ -176,12 +197,7 @@ func reportNormal(data *ReportData) {
|
|||||||
|
|
||||||
switch data.Outcome {
|
switch data.Outcome {
|
||||||
case OutcomeBlocked:
|
case OutcomeBlocked:
|
||||||
if len(data.BlockedPackages) > 0 {
|
printMalwareBlockSection(data)
|
||||||
fmt.Println()
|
|
||||||
fmt.Printf("%s %s\n", Colors.Red("✗"), Colors.Red("Malicious package blocked"))
|
|
||||||
printMaliciousPackagesList(data.BlockedPackages)
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(data.CooldownBlockedPackages) > 0 {
|
if len(data.CooldownBlockedPackages) > 0 {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
@@ -193,6 +209,11 @@ func reportNormal(data *ReportData) {
|
|||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if data.AdvisoryMessage != "" {
|
||||||
|
printAdvisoryMessage(data.AdvisoryMessage)
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
onlyCooldown := len(data.BlockedPackages) == 0 && len(data.CooldownBlockedPackages) > 0
|
onlyCooldown := len(data.BlockedPackages) == 0 && len(data.CooldownBlockedPackages) > 0
|
||||||
if onlyCooldown {
|
if onlyCooldown {
|
||||||
icon = Colors.Yellow("⊘")
|
icon = Colors.Yellow("⊘")
|
||||||
@@ -307,6 +328,11 @@ func reportVerbose(data *ReportData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if data.Outcome == OutcomeBlocked && data.AdvisoryMessage != "" {
|
||||||
|
fmt.Println()
|
||||||
|
printAdvisoryMessage(data.AdvisoryMessage)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -196,7 +196,7 @@ func printMaliciousPackagesList(malwarePackages []*analyzer.PackageVersionAnalys
|
|||||||
mp.PackageVersion.GetVersion())))
|
mp.PackageVersion.GetVersion())))
|
||||||
|
|
||||||
if verbosityLevel == VerbosityLevelVerbose {
|
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 != "" {
|
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 {
|
func pluralizeDays(n int) string {
|
||||||
if n == 1 {
|
if n == 1 {
|
||||||
return "1 day"
|
return "1 day"
|
||||||
@@ -242,6 +252,12 @@ func pluralizePackages(n int) string {
|
|||||||
return fmt.Sprintf("%d packages", n)
|
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.
|
// Format the string to be maximum maxWidth. Use newlines to wrap the text.
|
||||||
func termWidthFormatText(text string, maxWidth int) string {
|
func termWidthFormatText(text string, maxWidth int) string {
|
||||||
// Replace all newlines with spaces so that we can split the text into words
|
// Replace all newlines with spaces so that we can split the text into words
|
||||||
|
|||||||
@@ -101,6 +101,15 @@ 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(
|
||||||
@@ -194,11 +203,11 @@ func (b *baseRegistryInterceptor) handleAnalysisResult(
|
|||||||
b.statsCollector.RecordBlocked(result)
|
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(),
|
ecosystem.String(),
|
||||||
packageName, packageVersion,
|
packageName, packageVersion,
|
||||||
result.Summary,
|
result.Summary,
|
||||||
result.ReferenceURL)
|
result.ReferenceURL), config.Get().Config.AdvisoryMessage)
|
||||||
|
|
||||||
return &proxy.InterceptorResponse{
|
return &proxy.InterceptorResponse{
|
||||||
Action: proxy.ActionBlock,
|
Action: proxy.ActionBlock,
|
||||||
@@ -233,11 +242,11 @@ func (b *baseRegistryInterceptor) handleAnalysisResult(
|
|||||||
b.statsCollector.RecordUserCancelled(result)
|
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(),
|
ecosystem.String(),
|
||||||
packageName, packageVersion,
|
packageName, packageVersion,
|
||||||
result.Summary,
|
result.Summary,
|
||||||
result.ReferenceURL)
|
result.ReferenceURL), config.Get().Config.AdvisoryMessage)
|
||||||
|
|
||||||
return &proxy.InterceptorResponse{
|
return &proxy.InterceptorResponse{
|
||||||
Action: proxy.ActionBlock,
|
Action: proxy.ActionBlock,
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ func setTrustedPackagesForTest(t *testing.T, pkgs []pmgconfig.TrustedPackage) {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
orig := pmgconfig.Get().Config.TrustedPackages
|
orig := pmgconfig.Get().Config.TrustedPackages
|
||||||
pmgconfig.Get().Config.TrustedPackages = pkgs
|
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() {
|
t.Cleanup(func() {
|
||||||
pmgconfig.Get().Config.TrustedPackages = orig
|
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")
|
||||||
|
}
|
||||||
|
|||||||
@@ -126,8 +126,10 @@ 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 := fmt.Sprintf("Package blocked by dependency cooldown: GO/%s@%s\n\nPublished %d day(s) ago; cooldown window is %d day(s) (%d remaining).",
|
message := appendAdvisoryMessage(
|
||||||
module, version, daysAgo, cooldownDays, daysLeft)
|
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,
|
||||||
|
|||||||
@@ -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")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -59,12 +59,13 @@ func applyConfig(t *testing.T, override func(rc *config.RuntimeConfig)) {
|
|||||||
rc.Config.Paranoid = false
|
rc.Config.Paranoid = false
|
||||||
rc.Config.TrustedPackages = nil
|
rc.Config.TrustedPackages = nil
|
||||||
rc.Config.DependencyCooldown = config.DependencyCooldownConfig{}
|
rc.Config.DependencyCooldown = config.DependencyCooldownConfig{}
|
||||||
|
rc.Config.AdvisoryMessage = ""
|
||||||
|
|
||||||
if override != nil {
|
if override != nil {
|
||||||
override(rc)
|
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)
|
t.Fatalf("failed to preprocess trusted packages: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user