Skip to content

Commit bcfaca7

Browse files
committed
feat(package-firewall): add go-dev subcommand and Homebrew ecosystem
Add a package-firewall go-dev command that configures pkgsite-cli to use the Vulnetix pkg.go.dev API proxy via netrc Basic auth, and add Homebrew (Pro tier) support that writes a sourceable homebrew.env setting HOMEBREW_API_DOMAIN/HOMEBREW_ARTIFACT_DOMAIN with embedded credentials. Includes detection paths, tests, and enterprise docs.
1 parent 788f506 commit bcfaca7

7 files changed

Lines changed: 266 additions & 2 deletions

File tree

cmd/package_firewall.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@ they are updated as well.`,
4646
RunE: runPackageFirewallGo,
4747
}
4848

49+
var packageFirewallGoDevCmd = &cobra.Command{
50+
Use: "go-dev",
51+
Short: "Configure pkgsite-cli to use Vulnetix Package Firewall",
52+
Long: `Configure pkgsite-cli to use the Vulnetix pkg.go.dev API proxy.
53+
54+
This writes Basic auth credentials to netrc for packages.vulnetix.com and prints
55+
the shell alias or function needed to point pkgsite-cli at the firewall.`,
56+
RunE: runPackageFirewallGoDev,
57+
}
58+
4959
type packageFirewallAction struct {
5060
Target string
5161
Result string
@@ -115,6 +125,64 @@ func runPackageFirewallGo(cmd *cobra.Command, args []string) error {
115125
return nil
116126
}
117127

128+
func runPackageFirewallGoDev(cmd *cobra.Command, args []string) error {
129+
ctx := display.FromCommand(cmd)
130+
t := ctx.Term
131+
132+
proxyURL := strings.TrimSpace(packageFirewallProxyURL)
133+
if proxyURL == "" {
134+
proxyURL = packageFirewallDefaultProxy
135+
}
136+
proxyHost, err := parseProxyHost(proxyURL)
137+
if err != nil {
138+
return err
139+
}
140+
apiURL := strings.TrimRight(proxyURL, "/") + "/go-dev/v1beta"
141+
142+
ctx.Logger.Info("Configuring Vulnetix pkg.go.dev API proxy...")
143+
orgID, apiKey, credentialSource, err := packageFirewallAPIKey(packageFirewallBaseURL)
144+
if err != nil {
145+
return err
146+
}
147+
148+
var actions []packageFirewallAction
149+
netrcPath, err := auth.NetrcPath()
150+
if err != nil {
151+
return err
152+
}
153+
result, err := upsertNetrc(netrcPath, proxyHost, orgID, apiKey, packageFirewallDryRun)
154+
if err != nil {
155+
return err
156+
}
157+
actions = append(actions, packageFirewallAction{Target: netrcPath, Result: result})
158+
159+
var b strings.Builder
160+
if packageFirewallDryRun {
161+
b.WriteString(display.Bold(t, "Vulnetix pkg.go.dev API proxy dry run") + "\n")
162+
} else {
163+
b.WriteString(display.Bold(t, "Vulnetix pkg.go.dev API proxy configured") + "\n")
164+
}
165+
b.WriteString(display.KeyValue(t, []display.KVPair{
166+
{Key: "Credential source", Value: credentialSource},
167+
{Key: "Organization", Value: orgID},
168+
{Key: "API base URL", Value: apiURL},
169+
{Key: "API key", Value: maskSecret(apiKey)},
170+
}) + "\n")
171+
b.WriteString("\n" + display.Subheader(t, "Actions") + "\n")
172+
for _, action := range actions {
173+
b.WriteString(fmt.Sprintf(" %s: %s\n", action.Target, action.Result))
174+
}
175+
b.WriteString("\n" + display.Subheader(t, "pkgsite-cli setup") + "\n")
176+
b.WriteString(" pkgsite-cli does not persist a config file, so use one of the following:\n\n")
177+
b.WriteString(" Shell alias:\n")
178+
b.WriteString(fmt.Sprintf(" alias pkgsite-cli='pkgsite-cli -api %s'\n\n", apiURL))
179+
b.WriteString(" Or set for a single invocation:\n")
180+
b.WriteString(fmt.Sprintf(" pkgsite-cli -api %s search uuid\n\n", apiURL))
181+
b.WriteString(" Your netrc credentials will be used automatically for Basic auth.\n")
182+
ctx.Logger.Result(strings.TrimRight(b.String(), "\n"))
183+
return nil
184+
}
185+
118186
func runPackageFirewallEcosystem(cmd *cobra.Command, eco pfw.Ecosystem) error {
119187
if err := pfw.RequireWriter(eco); err != nil {
120188
return err
@@ -186,6 +254,26 @@ func runPackageFirewallEcosystem(cmd *cobra.Command, eco pfw.Ecosystem) error {
186254
for _, action := range actions {
187255
b.WriteString(fmt.Sprintf(" %s: %s\n", action.Target, action.Result))
188256
}
257+
258+
if eco.ID == "homebrew" {
259+
b.WriteString("\n" + display.Subheader(t, "Homebrew setup") + "\n")
260+
b.WriteString(" Homebrew reads these settings from environment variables.\n")
261+
if packageFirewallDryRun {
262+
b.WriteString(" After running without --dry-run, source the env file:\n")
263+
} else {
264+
b.WriteString(" Source the env file to activate the firewall:\n")
265+
}
266+
envFile := ""
267+
for _, a := range actions {
268+
if strings.HasSuffix(a.Target, "homebrew.env") {
269+
envFile = a.Target
270+
break
271+
}
272+
}
273+
b.WriteString(fmt.Sprintf(" source %s\n", envFile))
274+
b.WriteString(" Add that line to your shell profile (e.g. ~/.zshrc or ~/.bashrc) to persist it.\n")
275+
}
276+
189277
ctx.Logger.Result(strings.TrimRight(b.String(), "\n"))
190278
return nil
191279
}
@@ -579,6 +667,8 @@ func maskSecret(s string) string {
579667
func init() {
580668
addPackageFirewallFlags(packageFirewallGoCmd, "Go")
581669
packageFirewallCmd.AddCommand(packageFirewallGoCmd)
670+
addPackageFirewallFlags(packageFirewallGoDevCmd, "Go pkg.go.dev API")
671+
packageFirewallCmd.AddCommand(packageFirewallGoDevCmd)
582672
for _, eco := range pfw.All() {
583673
if eco.Command == "go" {
584674
continue

pkg/packagefirewall/config.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ func ConfigFiles(eco Ecosystem, opts ConfigOptions) ([]ConfigFile, error) {
8989
return []ConfigFile{{Path: filepath.Join(home, ".Rprofile"), Content: cranConfig(eco, opts)}}, nil
9090
case "helm":
9191
return []ConfigFile{{Path: filepath.Join(home, ".config", "helm", "repositories.yaml"), Content: helmConfig(eco, opts), Structured: true}}, nil
92+
case "homebrew":
93+
// Homebrew reads the API and bottle domains from environment variables.
94+
// We write a dedicated env file and instruct the user to source it.
95+
return []ConfigFile{{Path: filepath.Join(home, ".config", "vulnetix", "package-firewall", "homebrew.env"), Content: homebrewEnvConfig(opts)}}, nil
9296
case "aur":
9397
// paru and yay (AUR helpers) get the AUR RPC + git base pointed at the
9498
// /aur prefix; merged non-destructively into their real configs. pacman's
@@ -545,6 +549,31 @@ func yamlString(s string) string {
545549
return strconv.Quote(s)
546550
}
547551

552+
// homebrewEnvConfig returns a shell snippet that sets Homebrew's API and
553+
// artifact domain variables to the firewall. Credentials are embedded in the
554+
// URL userinfo so curl (which Homebrew spawns) sends Basic auth.
555+
func homebrewEnvConfig(opts ConfigOptions) string {
556+
apiDomain := withBasicAuth(ProxyURLWithSlash(opts.ProxyURL, Ecosystem{Prefix: "homebrew"})+"api", opts.OrgID, opts.APIKey)
557+
artifactDomain := withBasicAuth(proxyURLTrim(opts.ProxyURL)+"/homebrew-bottle", opts.OrgID, opts.APIKey)
558+
return strings.Join([]string{
559+
"# Vulnetix Package Firewall — Homebrew",
560+
"# Source this file in your shell, e.g. add to ~/.zshrc:",
561+
"# source " + filepath.Join(opts.HomeDir, ".config", "vulnetix", "package-firewall", "homebrew.env"),
562+
"export HOMEBREW_API_DOMAIN=\"" + apiDomain + "\"",
563+
"export HOMEBREW_ARTIFACT_DOMAIN=\"" + artifactDomain + "\"",
564+
"export HOMEBREW_ARTIFACT_DOMAIN_NO_FALLBACK=\"1\"",
565+
"",
566+
}, "\n")
567+
}
568+
569+
func proxyURLTrim(rawURL string) string {
570+
base := strings.TrimRight(strings.TrimSpace(rawURL), "/")
571+
if base == "" {
572+
return "https://packages.vulnetix.com"
573+
}
574+
return base
575+
}
576+
548577
func withBasicAuth(rawURL, username, password string) string {
549578
u, err := url.Parse(rawURL)
550579
if err != nil {

pkg/packagefirewall/config_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,3 +381,31 @@ func TestAURConfig(t *testing.T) {
381381
}
382382
}
383383
}
384+
385+
func TestHomebrewConfig(t *testing.T) {
386+
eco, _ := ByCommand("homebrew")
387+
files, err := ConfigFiles(eco, ConfigOptions{
388+
HomeDir: "/home/test",
389+
ProxyURL: "https://packages.vulnetix.com",
390+
OrgID: "org-123",
391+
APIKey: "secret",
392+
})
393+
if err != nil {
394+
t.Fatal(err)
395+
}
396+
if len(files) != 1 {
397+
t.Fatalf("want 1 file, got %d", len(files))
398+
}
399+
if files[0].Path != "/home/test/.config/vulnetix/package-firewall/homebrew.env" {
400+
t.Errorf("path = %q", files[0].Path)
401+
}
402+
for _, want := range []string{
403+
"HOMEBREW_API_DOMAIN=\"https://org-123:secret@packages.vulnetix.com/homebrew/api\"",
404+
"HOMEBREW_ARTIFACT_DOMAIN=\"https://org-123:secret@packages.vulnetix.com/homebrew-bottle\"",
405+
"HOMEBREW_ARTIFACT_DOMAIN_NO_FALLBACK=\"1\"",
406+
} {
407+
if !strings.Contains(files[0].Content, want) {
408+
t.Errorf("homebrew.env missing %q:\n%s", want, files[0].Content)
409+
}
410+
}
411+
}

pkg/packagefirewall/detect.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ func ConfigPaths(eco Ecosystem, home string) []string {
5252
filepath.Join(home, ".config", "vulnetix", "package-firewall", "arch-mirrorlist"),
5353
filepath.Join(home, ".config", "vulnetix", "package-firewall", "pacman.conf"),
5454
}
55+
case "homebrew":
56+
return []string{
57+
filepath.Join(home, ".config", "vulnetix", "package-firewall", "homebrew.env"),
58+
}
5559
default:
5660
return nil
5761
}

pkg/packagefirewall/ecosystem.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ var ecosystems = []Ecosystem{
4747
// Arch Linux: one command configures paru/yay (AUR, /aur prefix) and stages a
4848
// pacman mirrorlist for the official repos (/arch prefix). Free (community).
4949
{ID: "aur", Command: "aur", DisplayName: "Arch Linux", Prefix: "aur", Tier: TierCommunity, LiveWriter: true},
50+
{ID: "homebrew", Command: "homebrew", DisplayName: "Homebrew", Prefix: "homebrew", Tier: TierPro, LiveWriter: true},
5051
}
5152

5253
func All() []Ecosystem {

pkg/packagefirewall/ecosystem_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import "testing"
44

55
func TestAllMatchesPlan(t *testing.T) {
66
got := All()
7-
if len(got) != 22 {
8-
t.Fatalf("All() length = %d, want 22", len(got))
7+
if len(got) != 23 {
8+
t.Fatalf("All() length = %d, want 23", len(got))
99
}
1010
cases := map[string]struct {
1111
prefix string
@@ -32,6 +32,7 @@ func TestAllMatchesPlan(t *testing.T) {
3232
"helm": {"helm", TierEnterprise},
3333
"chef": {"chef", TierEnterprise},
3434
"terraform": {"terraform", TierEnterprise},
35+
"homebrew": {"homebrew", TierPro},
3536
}
3637
for command, want := range cases {
3738
eco, ok := ByCommand(command)
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
---
2+
title: Package Firewall
3+
weight: 10
4+
---
5+
6+
# Vulnetix Package Firewall
7+
8+
The Vulnetix Package Firewall is a registry proxy that sits between your package
9+
managers and upstream registries. Every requested package/version is checked
10+
against Vulnetix intelligence (CVSS, EPSS, Coalition ESS, CISA KEV, malware,
11+
weaponized exploits, active exploitation, PoCs, and bad-actor links) before it
12+
reaches your build.
13+
14+
Run one CLI command to configure a package manager to use the firewall.
15+
16+
## Supported ecosystems
17+
18+
| Command | Registry | Plan |
19+
|---|---|---|
20+
| `vulnetix package-firewall go` | Go modules (GOPROXY) | Community |
21+
| `vulnetix package-firewall go-dev` | pkg.go.dev API (pkgsite-cli) | Community |
22+
| `vulnetix package-firewall npm` | npm | Pro |
23+
| `vulnetix package-firewall pypi` | PyPI | Pro |
24+
| `vulnetix package-firewall cargo` | Cargo | Pro |
25+
| `vulnetix package-firewall gem` | RubyGems | Pro |
26+
| `vulnetix package-firewall homebrew` | Homebrew | Pro |
27+
| `vulnetix package-firewall docker` | Docker / OCI | Enterprise |
28+
| ... | ... | ... |
29+
30+
## Go modules
31+
32+
Configure Go to download modules through the firewall:
33+
34+
```bash
35+
vulnetix package-firewall go
36+
```
37+
38+
This writes your Basic-auth credentials to `~/.netrc` and sets `GOPROXY` and
39+
`GOAUTH` in your shell profile (or project `.env` / `.envrc` / `Makefile` when
40+
present).
41+
42+
## pkg.go.dev API
43+
44+
Google's `pkgsite-cli` can query the `pkg.go.dev/v1beta` API through the
45+
firewall, so search, package metadata, versions, symbols, and vulnerability data
46+
are filtered by your org policy:
47+
48+
```bash
49+
vulnetix package-firewall go-dev
50+
```
51+
52+
Because `pkgsite-cli` does not persist a config file, the command prints the
53+
alias or flag to use. After running it, either:
54+
55+
```bash
56+
alias pkgsite-cli='pkgsite-cli -api https://packages.vulnetix.com/go-dev/v1beta'
57+
pkgsite-cli search uuid
58+
```
59+
60+
or pass the API base directly:
61+
62+
```bash
63+
pkgsite-cli -api https://packages.vulnetix.com/go-dev/v1beta package github.com/google/go-cmp/cmp
64+
```
65+
66+
Your netrc credentials are used automatically for Basic auth.
67+
68+
## Homebrew
69+
70+
Configure Homebrew's API-mode client to use the firewall:
71+
72+
```bash
73+
vulnetix package-firewall homebrew
74+
```
75+
76+
This writes a shell env file to `~/.config/vulnetix/package-firewall/homebrew.env`
77+
containing:
78+
79+
```bash
80+
export HOMEBREW_API_DOMAIN="https://<org>:<key>@packages.vulnetix.com/homebrew/api"
81+
export HOMEBREW_ARTIFACT_DOMAIN="https://<org>:<key>@packages.vulnetix.com/homebrew-bottle"
82+
export HOMEBREW_ARTIFACT_DOMAIN_NO_FALLBACK="1"
83+
```
84+
85+
Source the file to activate it for the current shell, and add the same `source`
86+
line to your shell profile (`~/.zshrc`, `~/.bashrc`, etc.) to persist it:
87+
88+
```bash
89+
source ~/.config/vulnetix/package-firewall/homebrew.env
90+
```
91+
92+
`HOMEBREW_ARTIFACT_DOMAIN_NO_FALLBACK=1` is required so that Homebrew cannot
93+
bypass the firewall's bottle gate by falling back to `ghcr.io` when a bottle is
94+
blocked.
95+
96+
## Dry run
97+
98+
Preview every change the command would make:
99+
100+
```bash
101+
vulnetix package-firewall go --dry-run
102+
vulnetix package-firewall npm --dry-run
103+
```
104+
105+
## Custom proxy URL
106+
107+
For on-prem or staging deployments:
108+
109+
```bash
110+
vulnetix package-firewall go --proxy-url https://firewall.internal
111+
```

0 commit comments

Comments
 (0)