refactor: extract common filter logic into shared runtime
This commit is contained in:
23
Makefile
Normal file
23
Makefile
Normal 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
|
||||
|
||||
@@ -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
137
cmd/ls.go
@@ -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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
201
cmd/runtime.go
Normal 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")
|
||||
}
|
||||
|
||||
36
cmd/stats.go
36
cmd/stats.go
@@ -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)
|
||||
}
|
||||
|
||||
24
cmd/top.go
24
cmd/top.go
@@ -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)
|
||||
}
|
||||
11
cmd/trace.go
11
cmd/trace.go
@@ -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)
|
||||
}
|
||||
|
||||
21
cmd/watch.go
21
cmd/watch.go
@@ -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)
|
||||
}
|
||||
|
||||
BIN
demo/demo.gif
BIN
demo/demo.gif
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 1.6 MiB |
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user