refactor: extract common filter logic into shared runtime

This commit is contained in:
Karol Broda
2025-12-17 17:33:01 +01:00
parent c543a8a4e9
commit 3ce1ce8aed
13 changed files with 309 additions and 216 deletions

23
Makefile Normal file
View File

@@ -0,0 +1,23 @@
.PHONY: build test lint demo demo-build demo-run clean
build:
go build -o snitch .
test:
go test ./...
lint:
golangci-lint run
demo: demo-build demo-run
demo-build:
docker build -f demo/Dockerfile -t snitch-demo .
demo-run:
docker run --rm -v $(PWD)/demo:/output snitch-demo
clean:
rm -f snitch
rm -f demo/demo.gif

View File

@@ -361,8 +361,8 @@ func resetGlobalFlags() {
showTimestamp = false
sortBy = ""
fields = ""
ipv4 = false
ipv6 = false
filterIPv4 = false
filterIPv6 = false
colorMode = "auto"
numeric = false
}

137
cmd/ls.go
View File

@@ -22,20 +22,15 @@ import (
"golang.org/x/term"
)
// ls-specific flags
var (
outputFormat string
noHeaders bool
showTimestamp bool
sortBy string
fields string
ipv4 bool
ipv6 bool
colorMode string
numeric bool
lsTCP bool
lsUDP bool
lsListen bool
lsEstab bool
plainOutput bool
)
@@ -56,39 +51,16 @@ Available filters:
}
func runListCommand(outputFormat string, args []string) {
color.Init(colorMode)
filters, err := parseFilters(args)
if err != nil {
log.Fatalf("Error parsing filters: %v", err)
}
filters.IPv4 = ipv4
filters.IPv6 = ipv6
// apply shortcut flags
if lsTCP && !lsUDP {
filters.Proto = "tcp"
} else if lsUDP && !lsTCP {
filters.Proto = "udp"
}
if lsListen && !lsEstab {
filters.State = "LISTEN"
} else if lsEstab && !lsListen {
filters.State = "ESTABLISHED"
}
connections, err := collector.GetConnections()
rt, err := NewRuntime(args, colorMode, numeric)
if err != nil {
log.Fatal(err)
}
filteredConnections := collector.FilterConnections(connections, filters)
// apply sorting
if sortBy != "" {
collector.SortConnections(filteredConnections, collector.ParseSortOptions(sortBy))
rt.SortConnections(collector.ParseSortOptions(sortBy))
} else {
// default sort by local port
collector.SortConnections(filteredConnections, collector.SortOptions{
rt.SortConnections(collector.SortOptions{
Field: collector.SortByLport,
Direction: collector.SortAsc,
})
@@ -99,93 +71,26 @@ func runListCommand(outputFormat string, args []string) {
selectedFields = strings.Split(fields, ",")
}
switch outputFormat {
renderList(rt.Connections, outputFormat, selectedFields)
}
func renderList(connections []collector.Connection, format string, selectedFields []string) {
switch format {
case "json":
printJSON(filteredConnections)
printJSON(connections)
case "csv":
printCSV(filteredConnections, !noHeaders, showTimestamp, selectedFields)
printCSV(connections, !noHeaders, showTimestamp, selectedFields)
case "table", "wide":
if plainOutput {
printPlainTable(filteredConnections, !noHeaders, showTimestamp, selectedFields)
printPlainTable(connections, !noHeaders, showTimestamp, selectedFields)
} else {
printStyledTable(filteredConnections, !noHeaders, selectedFields)
printStyledTable(connections, !noHeaders, selectedFields)
}
default:
log.Fatalf("Invalid output format: %s. Valid formats are: table, wide, json, csv", outputFormat)
log.Fatalf("Invalid output format: %s. Valid formats are: table, wide, json, csv", format)
}
}
func parseFilters(args []string) (collector.FilterOptions, error) {
filters := collector.FilterOptions{}
for _, arg := range args {
parts := strings.SplitN(arg, "=", 2)
if len(parts) != 2 {
return filters, fmt.Errorf("invalid filter format: %s", arg)
}
key, value := parts[0], parts[1]
switch strings.ToLower(key) {
case "proto":
filters.Proto = value
case "state":
filters.State = value
case "pid":
pid, err := strconv.Atoi(value)
if err != nil {
return filters, fmt.Errorf("invalid pid value: %s", value)
}
filters.Pid = pid
case "proc":
filters.Proc = value
case "lport":
port, err := strconv.Atoi(value)
if err != nil {
return filters, fmt.Errorf("invalid lport value: %s", value)
}
filters.Lport = port
case "rport":
port, err := strconv.Atoi(value)
if err != nil {
return filters, fmt.Errorf("invalid rport value: %s", value)
}
filters.Rport = port
case "user":
uid, err := strconv.Atoi(value)
if err == nil {
filters.UID = uid
} else {
filters.User = value
}
case "laddr":
filters.Laddr = value
case "raddr":
filters.Raddr = value
case "contains":
filters.Contains = value
case "if", "interface":
filters.Interface = value
case "mark":
filters.Mark = value
case "namespace":
filters.Namespace = value
case "inode":
inode, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return filters, fmt.Errorf("invalid inode value: %s", value)
}
filters.Inode = inode
case "since":
since, sinceRel, err := collector.ParseTimeFilter(value)
if err != nil {
return filters, fmt.Errorf("invalid since value: %s", value)
}
filters.Since = since
filters.SinceRel = sinceRel
default:
return filters, fmt.Errorf("unknown filter key: %s", key)
}
}
return filters, nil
}
func getFieldMap(c collector.Connection) map[string]string {
laddr := c.Laddr
@@ -483,20 +388,16 @@ func init() {
cfg := config.Get()
// ls-specific flags
lsCmd.Flags().StringVarP(&outputFormat, "output", "o", cfg.Defaults.OutputFormat, "Output format (table, wide, json, csv)")
lsCmd.Flags().BoolVar(&noHeaders, "no-headers", cfg.Defaults.NoHeaders, "Omit headers for table/csv output")
lsCmd.Flags().BoolVar(&showTimestamp, "ts", false, "Include timestamp in output")
lsCmd.Flags().StringVarP(&sortBy, "sort", "s", cfg.Defaults.SortBy, "Sort by column (e.g., pid:desc)")
lsCmd.Flags().StringVarP(&fields, "fields", "f", strings.Join(cfg.Defaults.Fields, ","), "Comma-separated list of fields to show")
lsCmd.Flags().BoolVarP(&ipv4, "ipv4", "4", cfg.Defaults.IPv4, "Only show IPv4 connections")
lsCmd.Flags().BoolVarP(&ipv6, "ipv6", "6", cfg.Defaults.IPv6, "Only show IPv6 connections")
lsCmd.Flags().StringVar(&colorMode, "color", cfg.Defaults.Color, "Color mode (auto, always, never)")
lsCmd.Flags().BoolVarP(&numeric, "numeric", "n", cfg.Defaults.Numeric, "Don't resolve hostnames")
// shortcut filters
lsCmd.Flags().BoolVarP(&lsTCP, "tcp", "t", false, "Show only TCP connections")
lsCmd.Flags().BoolVarP(&lsUDP, "udp", "u", false, "Show only UDP connections")
lsCmd.Flags().BoolVarP(&lsListen, "listen", "l", false, "Show only listening sockets")
lsCmd.Flags().BoolVarP(&lsEstab, "established", "e", false, "Show only established connections")
lsCmd.Flags().BoolVarP(&plainOutput, "plain", "p", false, "Plain output (parsable, no styling)")
// shared filter flags
addFilterFlags(lsCmd)
}

View File

@@ -251,7 +251,7 @@ func TestParseFilters(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
filters, err := parseFilters(tt.args)
filters, err := ParseFilterArgs(tt.args)
if tt.expectError {
if err == nil {

View File

@@ -40,12 +40,11 @@ func init() {
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.config/snitch/snitch.toml)")
rootCmd.PersistentFlags().Bool("debug", false, "enable debug logs to stderr")
// add top's filter flags to root so `snitch -l` works
// add top's flags to root so `snitch -l` works (defaults to top command)
cfg := config.Get()
rootCmd.Flags().StringVar(&topTheme, "theme", cfg.Defaults.Theme, "Theme for TUI (dark, light, mono, auto)")
rootCmd.Flags().DurationVarP(&topInterval, "interval", "i", 0, "Refresh interval (default 1s)")
rootCmd.Flags().BoolVarP(&topTCP, "tcp", "t", false, "Show only TCP connections")
rootCmd.Flags().BoolVarP(&topUDP, "udp", "u", false, "Show only UDP connections")
rootCmd.Flags().BoolVarP(&topListen, "listen", "l", false, "Show only listening sockets")
rootCmd.Flags().BoolVarP(&topEstab, "established", "e", false, "Show only established connections")
// shared filter flags for root command
addFilterFlags(rootCmd)
}

201
cmd/runtime.go Normal file
View File

@@ -0,0 +1,201 @@
package cmd
import (
"fmt"
"snitch/internal/collector"
"snitch/internal/color"
"strconv"
"strings"
"github.com/spf13/cobra"
)
// Runtime holds the shared state for all commands.
// it handles common filter logic, fetching, and filtering connections.
type Runtime struct {
// filter options built from flags and args
Filters collector.FilterOptions
// filtered connections ready for rendering
Connections []collector.Connection
// common settings
ColorMode string
Numeric bool
}
// shared filter flags - used by all commands
var (
filterTCP bool
filterUDP bool
filterListen bool
filterEstab bool
filterIPv4 bool
filterIPv6 bool
)
// BuildFilters constructs FilterOptions from command args and shortcut flags.
func BuildFilters(args []string) (collector.FilterOptions, error) {
filters, err := ParseFilterArgs(args)
if err != nil {
return filters, err
}
// apply ipv4/ipv6 flags
filters.IPv4 = filterIPv4
filters.IPv6 = filterIPv6
// apply protocol shortcut flags
if filterTCP && !filterUDP {
filters.Proto = "tcp"
} else if filterUDP && !filterTCP {
filters.Proto = "udp"
}
// apply state shortcut flags
if filterListen && !filterEstab {
filters.State = "LISTEN"
} else if filterEstab && !filterListen {
filters.State = "ESTABLISHED"
}
return filters, nil
}
// FetchConnections gets connections from the collector and applies filters.
func FetchConnections(filters collector.FilterOptions) ([]collector.Connection, error) {
connections, err := collector.GetConnections()
if err != nil {
return nil, err
}
return collector.FilterConnections(connections, filters), nil
}
// NewRuntime creates a runtime with fetched and filtered connections.
func NewRuntime(args []string, colorMode string, numeric bool) (*Runtime, error) {
color.Init(colorMode)
filters, err := BuildFilters(args)
if err != nil {
return nil, fmt.Errorf("failed to parse filters: %w", err)
}
connections, err := FetchConnections(filters)
if err != nil {
return nil, fmt.Errorf("failed to fetch connections: %w", err)
}
return &Runtime{
Filters: filters,
Connections: connections,
ColorMode: colorMode,
Numeric: numeric,
}, nil
}
// SortConnections sorts the runtime's connections in place.
func (r *Runtime) SortConnections(opts collector.SortOptions) {
collector.SortConnections(r.Connections, opts)
}
// ParseFilterArgs parses key=value filter arguments.
// exported for testing.
func ParseFilterArgs(args []string) (collector.FilterOptions, error) {
filters := collector.FilterOptions{}
for _, arg := range args {
parts := strings.SplitN(arg, "=", 2)
if len(parts) != 2 {
return filters, fmt.Errorf("invalid filter format: %s (expected key=value)", arg)
}
key, value := parts[0], parts[1]
if err := applyFilter(&filters, key, value); err != nil {
return filters, err
}
}
return filters, nil
}
// applyFilter applies a single key=value filter to FilterOptions.
func applyFilter(filters *collector.FilterOptions, key, value string) error {
switch strings.ToLower(key) {
case "proto":
filters.Proto = value
case "state":
filters.State = value
case "pid":
pid, err := strconv.Atoi(value)
if err != nil {
return fmt.Errorf("invalid pid value: %s", value)
}
filters.Pid = pid
case "proc":
filters.Proc = value
case "lport":
port, err := strconv.Atoi(value)
if err != nil {
return fmt.Errorf("invalid lport value: %s", value)
}
filters.Lport = port
case "rport":
port, err := strconv.Atoi(value)
if err != nil {
return fmt.Errorf("invalid rport value: %s", value)
}
filters.Rport = port
case "user":
uid, err := strconv.Atoi(value)
if err == nil {
filters.UID = uid
} else {
filters.User = value
}
case "laddr":
filters.Laddr = value
case "raddr":
filters.Raddr = value
case "contains":
filters.Contains = value
case "if", "interface":
filters.Interface = value
case "mark":
filters.Mark = value
case "namespace":
filters.Namespace = value
case "inode":
inode, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return fmt.Errorf("invalid inode value: %s", value)
}
filters.Inode = inode
case "since":
since, sinceRel, err := collector.ParseTimeFilter(value)
if err != nil {
return fmt.Errorf("invalid since value: %s", value)
}
filters.Since = since
filters.SinceRel = sinceRel
default:
return fmt.Errorf("unknown filter key: %s", key)
}
return nil
}
// FilterFlagsHelp returns the help text for common filter flags.
const FilterFlagsHelp = `
Filters are specified in key=value format. For example:
snitch ls proto=tcp state=established
Available filters:
proto, state, pid, proc, lport, rport, user, laddr, raddr, contains, if, mark, namespace, inode, since`
// addFilterFlags adds the common filter flags to a command.
func addFilterFlags(cmd *cobra.Command) {
cmd.Flags().BoolVarP(&filterTCP, "tcp", "t", false, "Show only TCP connections")
cmd.Flags().BoolVarP(&filterUDP, "udp", "u", false, "Show only UDP connections")
cmd.Flags().BoolVarP(&filterListen, "listen", "l", false, "Show only listening sockets")
cmd.Flags().BoolVarP(&filterEstab, "established", "e", false, "Show only established connections")
cmd.Flags().BoolVarP(&filterIPv4, "ipv4", "4", false, "Only show IPv4 connections")
cmd.Flags().BoolVarP(&filterIPv6, "ipv6", "6", false, "Only show IPv6 connections")
}

View File

@@ -39,15 +39,12 @@ type InterfaceStats struct {
Count int `json:"count"`
}
// stats-specific flags
var (
statsOutputFormat string
statsInterval time.Duration
statsCount int
statsNoHeaders bool
statsTCP bool
statsUDP bool
statsListen bool
statsEstab bool
)
var statsCmd = &cobra.Command{
@@ -67,24 +64,10 @@ Available filters:
}
func runStatsCommand(args []string) {
filters, err := parseFilters(args)
filters, err := BuildFilters(args)
if err != nil {
log.Fatalf("Error parsing filters: %v", err)
}
filters.IPv4 = ipv4
filters.IPv6 = ipv6
// apply shortcut flags
if statsTCP && !statsUDP {
filters.Proto = "tcp"
} else if statsUDP && !statsTCP {
filters.Proto = "udp"
}
if statsListen && !statsEstab {
filters.State = "LISTEN"
} else if statsEstab && !statsListen {
filters.State = "ESTABLISHED"
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -137,13 +120,11 @@ func runStatsCommand(args []string) {
}
func generateStats(filters collector.FilterOptions) (*StatsData, error) {
connections, err := collector.GetConnections()
filteredConnections, err := FetchConnections(filters)
if err != nil {
return nil, err
}
filteredConnections := collector.FilterConnections(connections, filters)
stats := &StatsData{
Timestamp: time.Now(),
Total: len(filteredConnections),
@@ -307,16 +288,13 @@ func printStatsTable(stats *StatsData, headers bool) {
func init() {
rootCmd.AddCommand(statsCmd)
// stats-specific flags
statsCmd.Flags().StringVarP(&statsOutputFormat, "output", "o", "table", "Output format (table, json, csv)")
statsCmd.Flags().DurationVarP(&statsInterval, "interval", "i", 0, "Refresh interval (0 = one-shot)")
statsCmd.Flags().IntVarP(&statsCount, "count", "c", 0, "Number of iterations (0 = unlimited)")
statsCmd.Flags().BoolVar(&statsNoHeaders, "no-headers", false, "Omit headers for table/csv output")
statsCmd.Flags().BoolVarP(&ipv4, "ipv4", "4", false, "Only show IPv4 connections")
statsCmd.Flags().BoolVarP(&ipv6, "ipv6", "6", false, "Only show IPv6 connections")
// shortcut filters
statsCmd.Flags().BoolVarP(&statsTCP, "tcp", "t", false, "Show only TCP connections")
statsCmd.Flags().BoolVarP(&statsUDP, "udp", "u", false, "Show only UDP connections")
statsCmd.Flags().BoolVarP(&statsListen, "listen", "l", false, "Show only listening sockets")
statsCmd.Flags().BoolVarP(&statsEstab, "established", "e", false, "Show only established connections")
// shared filter flags
addFilterFlags(statsCmd)
}

View File

@@ -10,13 +10,10 @@ import (
"github.com/spf13/cobra"
)
// top-specific flags
var (
topTheme string
topInterval time.Duration
topTCP bool
topUDP bool
topListen bool
topEstab bool
)
var topCmd = &cobra.Command{
@@ -36,11 +33,11 @@ var topCmd = &cobra.Command{
}
// if any filter flag is set, use exclusive mode
if topTCP || topUDP || topListen || topEstab {
opts.TCP = topTCP
opts.UDP = topUDP
opts.Listening = topListen
opts.Established = topEstab
if filterTCP || filterUDP || filterListen || filterEstab {
opts.TCP = filterTCP
opts.UDP = filterUDP
opts.Listening = filterListen
opts.Established = filterEstab
opts.Other = false
opts.FilterSet = true
}
@@ -57,10 +54,11 @@ var topCmd = &cobra.Command{
func init() {
rootCmd.AddCommand(topCmd)
cfg := config.Get()
// top-specific flags
topCmd.Flags().StringVar(&topTheme, "theme", cfg.Defaults.Theme, "Theme for TUI (dark, light, mono, auto)")
topCmd.Flags().DurationVarP(&topInterval, "interval", "i", time.Second, "Refresh interval")
topCmd.Flags().BoolVarP(&topTCP, "tcp", "t", false, "Show only TCP connections")
topCmd.Flags().BoolVarP(&topUDP, "udp", "u", false, "Show only UDP connections")
topCmd.Flags().BoolVarP(&topListen, "listen", "l", false, "Show only listening sockets")
topCmd.Flags().BoolVarP(&topEstab, "established", "e", false, "Show only established connections")
// shared filter flags
addFilterFlags(topCmd)
}

View File

@@ -47,12 +47,10 @@ Available filters:
}
func runTraceCommand(args []string) {
filters, err := parseFilters(args)
filters, err := BuildFilters(args)
if err != nil {
log.Fatalf("Error parsing filters: %v", err)
}
filters.IPv4 = ipv4
filters.IPv6 = ipv6
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -222,11 +220,14 @@ func printTraceEventHuman(event TraceEvent) {
func init() {
rootCmd.AddCommand(traceCmd)
// trace-specific flags
traceCmd.Flags().DurationVarP(&traceInterval, "interval", "i", time.Second, "Polling interval (e.g., 500ms, 2s)")
traceCmd.Flags().IntVarP(&traceCount, "count", "c", 0, "Number of events to capture (0 = unlimited)")
traceCmd.Flags().StringVarP(&traceOutputFormat, "output", "o", "human", "Output format (human, json)")
traceCmd.Flags().BoolVarP(&traceNumeric, "numeric", "n", false, "Don't resolve hostnames")
traceCmd.Flags().BoolVar(&traceTimestamp, "ts", false, "Include timestamp in output")
traceCmd.Flags().BoolVarP(&ipv4, "ipv4", "4", false, "Only trace IPv4 connections")
traceCmd.Flags().BoolVarP(&ipv6, "ipv6", "6", false, "Only trace IPv6 connections")
// shared filter flags
addFilterFlags(traceCmd)
}

View File

@@ -7,7 +7,6 @@ import (
"log"
"os"
"os/signal"
"snitch/internal/collector"
"syscall"
"time"
@@ -36,17 +35,14 @@ Available filters:
}
func runWatchCommand(args []string) {
filters, err := parseFilters(args)
filters, err := BuildFilters(args)
if err != nil {
log.Fatalf("Error parsing filters: %v", err)
}
filters.IPv4 = ipv4
filters.IPv6 = ipv6
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Handle interrupts gracefully
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
go func() {
@@ -63,18 +59,16 @@ func runWatchCommand(args []string) {
case <-ctx.Done():
return
case <-ticker.C:
connections, err := collector.GetConnections()
connections, err := FetchConnections(filters)
if err != nil {
log.Printf("Error getting connections: %v", err)
continue
}
filteredConnections := collector.FilterConnections(connections, filters)
frame := map[string]interface{}{
"timestamp": time.Now().Format(time.RFC3339Nano),
"connections": filteredConnections,
"count": len(filteredConnections),
"connections": connections,
"count": len(connections),
}
jsonOutput, err := json.Marshal(frame)
@@ -95,8 +89,11 @@ func runWatchCommand(args []string) {
func init() {
rootCmd.AddCommand(watchCmd)
// watch-specific flags
watchCmd.Flags().DurationVarP(&watchInterval, "interval", "i", time.Second, "Refresh interval (e.g., 500ms, 2s)")
watchCmd.Flags().IntVarP(&watchCount, "count", "c", 0, "Number of frames to emit (0 = unlimited)")
watchCmd.Flags().BoolVarP(&ipv4, "ipv4", "4", false, "Only show IPv4 connections")
watchCmd.Flags().BoolVarP(&ipv6, "ipv6", "6", false, "Only show IPv6 connections")
// shared filter flags
addFilterFlags(watchCmd)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@@ -1,6 +1,3 @@
# VHS tape file for snitch demo
# run with: docker build -f demo/Dockerfile -t snitch-demo . && docker run -v $(pwd)/demo:/output snitch-demo
Output demo.gif
Set Shell "bash"
@@ -11,7 +8,8 @@ Set Height 700
Set Theme "Catppuccin Frappe"
Set Padding 15
Set Framerate 24
Set TypingSpeed 40ms
Set TypingSpeed 30ms
Set PlaybackSpeed 1.5
# force color output
Env TERM "xterm-256color"
@@ -23,77 +21,74 @@ Env FORCE_COLOR "1"
# launch snitch
Type "./snitch top"
Enter
Sleep 2s
Sleep 1.5s
# navigate down through connections
Down
Sleep 400ms
Sleep 200ms
Down
Sleep 400ms
Sleep 200ms
Down
Sleep 400ms
Sleep 200ms
Down
Sleep 400ms
Sleep 200ms
Down
Sleep 1s
Sleep 600ms
# open detail view for selected connection
Enter
Sleep 2s
Sleep 1.5s
# close detail view
Escape
Sleep 1s
Sleep 500ms
# search for nginx
Type "/"
Sleep 500ms
Sleep 300ms
Type "nginx"
Sleep 1s
Sleep 600ms
Enter
Sleep 2s
Sleep 1.2s
# clear search
Type "/"
Sleep 300ms
Sleep 200ms
Escape
Sleep 1s
Sleep 500ms
# filter: hide udp, show only tcp
Type "u"
Sleep 1.5s
Sleep 800ms
# show only listening connections
Type "e"
Sleep 1.5s
Sleep 800ms
Type "o"
Sleep 1.5s
Sleep 800ms
# reset to show all
Type "a"
Sleep 1.5s
Sleep 800ms
# cycle through sort options
Type "s"
Sleep 1s
Sleep 500ms
Type "s"
Sleep 1s
Type "s"
Sleep 1s
Sleep 500ms
# reverse sort order
Type "S"
Sleep 1.5s
Sleep 800ms
# show help screen
Type "?"
Sleep 3s
Sleep 2s
# close help
Escape
Sleep 1s
Sleep 500ms
# quit
Type "q"
Sleep 300ms
Sleep 200ms

View File

@@ -64,7 +64,7 @@
pname = "snitch";
version = self.shortRev or self.dirtyShortRev or "dev";
src = self;
vendorHash = "sha256-BNNbA72puV0QSLkAlgn/buJJt7mIlVkbTEBhTXOg8pY=";
vendorHash = "sha256-fX3wOqeOgjH7AuWGxPQxJ+wbhp240CW8tiF4rVUUDzk=";
env.CGO_ENABLED = 0;
ldflags = [
"-s" "-w"