Skip to content

Commit 83a5729

Browse files
Group CLI flags and make --help text for modules descriptive (#571)
* grouped CLI flags * alphabetize * add ini parser and CLI flag groupings * extract cli functions into a separate file for organization * give proper short descriptions for the --help text * add short description for memcached
1 parent 00d51cb commit 83a5729

File tree

33 files changed

+168
-108
lines changed

33 files changed

+168
-108
lines changed

cli.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package zgrab2
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
"strings"
7+
8+
log "github.com/sirupsen/logrus"
9+
flags "github.com/zmap/zflags"
10+
)
11+
12+
var (
13+
parser *flags.Parser // parser for main zgrab2 command
14+
// iniParser is a parser just for ini files. It's frustrating that we need both, but the ini parser needs an option
15+
// group 'Application Options' and it seems we can't have this along with option groups in the main parser and have
16+
// options set correctly. The hidden one shadows the other and no CLI flags are set.
17+
iniParser *flags.Parser
18+
)
19+
20+
const defaultDNSPort = "53"
21+
22+
func init() {
23+
parser = flags.NewParser(nil, flags.Default)
24+
desc := []string{
25+
// Using a long single line so the terminal can handle wrapping, except for Input/Examples which should be on
26+
// separate lines
27+
"zgrab2 is fast, modular L7 application-layer scanner. It is commonly used with tools like ZMap which identify " +
28+
"\"potential services\", or services we know are active on a given IP + port, and these are fed into ZGrab2 " +
29+
"to confirm and provide details of the service. It has support for a number of protocols listed below as " +
30+
"'Available commands' including SSH and HTTP. By default, zgrab2 will accept input from stdin and output " +
31+
"results to stdout, with updates and logs to stderr. Please see 'zgrab2 <command> --help' for more details " +
32+
"on a specific command.",
33+
"Input is taken from stdin or --input-file, if specified. Input is CSV-formatted with 'IP, Domain, Tag, Port' " +
34+
"or simply 'IP' or 'Domain'. See README.md for more details.",
35+
"",
36+
"Example usages:",
37+
"echo '1.1.1.1' | zgrab2 tls # Scan 1.1.1.1 with TLS",
38+
"echo example.com | zgrab2 http # Scan example.com with HTTP",
39+
}
40+
parser.LongDescription = strings.Join(desc, "\n")
41+
_, err := parser.AddCommand("multiple", "Run multiple commands in a single run", "", &config.Multiple)
42+
if err != nil {
43+
log.Fatalf("could not add multiple command: %v", err)
44+
}
45+
_, err = parser.AddGroup("General Options", "General options for controlling the behavior of ZGrab2", &config.GeneralOptions)
46+
if err != nil {
47+
log.Fatalf("could not add general options group: %v", err)
48+
}
49+
_, err = parser.AddGroup("Input/Output Options", "Options for controlling the input/output behavior of ZGrab2", &config.InputOutputOptions)
50+
if err != nil {
51+
log.Fatalf("could not add I/O options group: %v", err)
52+
}
53+
_, err = parser.AddGroup("Network Options", "Options for controlling the network behavior of ZGrab2", &config.NetworkingOptions)
54+
if err != nil {
55+
log.Fatalf("could not add networking options group: %v", err)
56+
}
57+
iniParser = flags.NewParser(nil, flags.Default)
58+
}
59+
60+
// NewIniParser creates and returns a ini parser initialized
61+
// with the default parser
62+
func NewIniParser() *flags.IniParser {
63+
group, err := iniParser.AddGroup("Application Options", "Hidden group including all global options for ini files", &config)
64+
if err != nil {
65+
log.Fatalf("could not add Application Options group: %v", err)
66+
}
67+
group.Hidden = true
68+
69+
return flags.NewIniParser(iniParser)
70+
}
71+
72+
// AddCommand adds a module to the parser and returns a pointer to
73+
// a flags.command object or an error
74+
func AddCommand(command string, shortDescription string, longDescription string, port int, m ScanModule) (*flags.Command, error) {
75+
cmd, err := parser.AddCommand(command, shortDescription, longDescription, m)
76+
if err != nil {
77+
return nil, fmt.Errorf("could not add command to default parser: %w", err)
78+
}
79+
cmd.FindOptionByLongName("port").Default = []string{strconv.Itoa(port)}
80+
cmd.FindOptionByLongName("name").Default = []string{command}
81+
82+
// Add the same command to the ini parser
83+
cmd, err = iniParser.AddCommand(command, shortDescription, longDescription, m)
84+
if err != nil {
85+
return nil, fmt.Errorf("could not add command to ini parser: %w", err)
86+
}
87+
cmd.FindOptionByLongName("port").Default = []string{strconv.Itoa(port)}
88+
cmd.FindOptionByLongName("name").Default = []string{command}
89+
modules[command] = m
90+
return cmd, nil
91+
}
92+
93+
// ParseCommandLine parses the commands given on the command line
94+
// and validates the framework configuration (global options)
95+
// immediately after parsing
96+
func ParseCommandLine(flags []string) ([]string, string, ScanFlags, error) {
97+
posArgs, moduleType, f, err := parser.ParseCommandLine(flags)
98+
if err == nil {
99+
validateFrameworkConfiguration()
100+
}
101+
sf, _ := f.(ScanFlags)
102+
return posArgs, moduleType, sf, err
103+
}

config.go

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,32 +23,43 @@ const (
2323
IPVersionCapabilityIPv6Address = "2606:4700:4700::1111:80" // Same as above for IPv6
2424
)
2525

26-
// Config is the high level framework options that will be parsed
27-
// from the command line
28-
type Config struct {
29-
OutputFileName string `short:"o" long:"output-file" default:"-" description:"Output filename, use - for stdout"`
26+
type GeneralOptions struct {
27+
Senders int `short:"s" long:"senders" default:"1000" description:"Number of send goroutines to use"`
28+
GOMAXPROCS int `long:"gomaxprocs" default:"0" description:"Set GOMAXPROCS"`
29+
Prometheus string `long:"prometheus" description:"Address to use for Prometheus server (e.g. localhost:8080). If empty, Prometheus is disabled."`
30+
ReadLimitPerHost int `long:"read-limit-per-host" default:"96" description:"Maximum total kilobytes to read for a single host (default 96kb)"`
31+
}
32+
33+
type InputOutputOptions struct {
34+
BlocklistFileName string `short:"b" long:"blocklist-file" default:"-" description:"Blocklist filename, use - for $(HOME)/.config/zgrab2/blocklist.conf"`
3035
InputFileName string `short:"f" long:"input-file" default:"-" description:"Input filename, use - for stdin"`
36+
LogFileName string `short:"l" long:"log-file" default:"-" description:"Log filename, use - for stderr"`
3137
MetaFileName string `short:"m" long:"metadata-file" default:"-" description:"Metadata filename, use - for stderr."`
32-
BlocklistFileName string `short:"b" long:"blocklist-file" default:"-" description:"Blocklist filename, use - for $(HOME)/.config/zgrab2/blocklist.conf"`
38+
OutputFileName string `short:"o" long:"output-file" default:"-" description:"Output filename, use - for stdout"`
3339
StatusUpdatesFileName string `short:"u" long:"status-updates-file" default:"-" description:"Status updates filename, use - for stderr."`
34-
LogFileName string `short:"l" long:"log-file" default:"-" description:"Log filename, use - for stderr"`
40+
Debug bool `long:"debug" description:"Include debug fields in the output."`
41+
Flush bool `long:"flush" description:"Flush after each line of output."`
42+
}
43+
44+
type NetworkingOptions struct {
45+
ConnectionsPerHost int `long:"connections-per-host" default:"1" description:"Number of times to connect to each host (results in more output)"`
46+
DNSServerRateLimit int `long:"dns-rate-limit" default:"10000" description:"Rate limit for DNS lookups per second."`
47+
DNSResolutionTimeout time.Duration `long:"dns-resolution-timeout" default:"10s" description:"Timeout for DNS resolution of target hostnames. Default is 10 seconds."`
48+
CustomDNS string `long:"dns-resolvers" description:"Address of a custom DNS server(s) for lookups, comma-delimited. Default port is 53. Ex: 1.1.1.1:53,8.8.8.8. Uses the OS-default resolvers if not set."`
49+
LocalAddrString string `long:"local-addr" description:"Local address(es) to bind to for outgoing connections. Comma-separated list of IP addresses, ranges (inclusive), or CIDR blocks, ex: 1.1.1.1-1.1.1.3, 2.2.2.2, 3.3.3.0/24"`
50+
LocalPortString string `long:"local-port" description:"Local port(s) to bind to for outgoing connections. Comma-separated list of ports or port ranges (inclusive) ex: 1200-1300,2000"`
51+
UserIPv4Choice *bool `long:"resolve-ipv4" description:"Use IPv4 for resolving domains (accept A records). True by default, use only --resolve-ipv6 for IPv6 only resolution. If used with --resolve-ipv6, will use both IPv4 and IPv6."`
52+
UserIPv6Choice *bool `long:"resolve-ipv6" description:"Use IPv6 for resolving domains (accept AAAA records). IPv6 is disabled by default. If --resolve-ipv4 is not set and --resolve-ipv6 is, will only use IPv6. If used with --resolve-ipv4, will use both IPv4 and IPv6."`
53+
ServerRateLimit int `long:"server-rate-limit" default:"20" description:"Per-IP rate limit for connections to targets per second."`
54+
}
3555

36-
Senders int `short:"s" long:"senders" default:"1000" description:"Number of send goroutines to use"`
37-
Debug bool `long:"debug" description:"Include debug fields in the output."`
38-
Flush bool `long:"flush" description:"Flush after each line of output."`
39-
GOMAXPROCS int `long:"gomaxprocs" default:"0" description:"Set GOMAXPROCS"`
40-
ConnectionsPerHost int `long:"connections-per-host" default:"1" description:"Number of times to connect to each host (results in more output)"`
41-
ReadLimitPerHost int `long:"read-limit-per-host" default:"96" description:"Maximum total kilobytes to read for a single host (default 96kb)"`
42-
Prometheus string `long:"prometheus" description:"Address to use for Prometheus server (e.g. localhost:8080). If empty, Prometheus is disabled."`
43-
CustomDNS string `long:"dns-resolvers" description:"Address of a custom DNS server(s) for lookups, comma-delimited. Default port is 53. Ex: 1.1.1.1:53,8.8.8.8. Uses the OS-default resolvers if not set."`
56+
// Config is the high level framework options that will be parsed
57+
// from the command line
58+
type Config struct {
59+
GeneralOptions // CLI Options related to general framework configuration. Don't fit into any other category
60+
InputOutputOptions // CLI Options related to I/O. Just affects organization of --help
61+
NetworkingOptions // CLI Options related to networking. Just affects organization of --help
4462
Multiple MultipleCommand `command:"multiple" description:"Multiple module actions"`
45-
LocalAddrString string `long:"local-addr" description:"Local address(es) to bind to for outgoing connections. Comma-separated list of IP addresses, ranges (inclusive), or CIDR blocks, ex: 1.1.1.1-1.1.1.3, 2.2.2.2, 3.3.3.0/24"`
46-
LocalPortString string `long:"local-port" description:"Local port(s) to bind to for outgoing connections. Comma-separated list of ports or port ranges (inclusive) ex: 1200-1300,2000"`
47-
DNSResolutionTimeout time.Duration `long:"dns-resolution-timeout" default:"10s" description:"Timeout for DNS resolution of target hostnames. Default is 10 seconds."`
48-
DNSServerRateLimit int `long:"dns-rate-limit" default:"10000" description:"Rate limit for DNS lookups per second."`
49-
ServerRateLimit int `long:"server-rate-limit" default:"20" description:"Per-IP rate limit for connections to targets per second."`
50-
UserIPv4Choice *bool `long:"resolve-ipv4" description:"Use IPv4 for resolving domains (accept A records). True by default, use only --resolve-ipv6 for IPv6 only resolution. If used with --resolve-ipv6, will use both IPv4 and IPv6."`
51-
UserIPv6Choice *bool `long:"resolve-ipv6" description:"Use IPv6 for resolving domains (accept AAAA records). IPv6 is disabled by default. If --resolve-ipv4 is not set and --resolve-ipv6 is, will only use IPv6. If used with --resolve-ipv4, will use both IPv4 and IPv6."`
5263
inputFile *os.File
5364
outputFile *os.File
5465
metaFile *os.File

integration_tests/multiple/multiple.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
[Application Options]
2+
senders=2
3+
14
[ntp]
25
trigger="ntp"
36

modules/amqp091/scanner.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ type Result struct {
9797
// RegisterModule registers the zgrab2 module.
9898
func RegisterModule() {
9999
var module Module
100-
_, err := zgrab2.AddCommand("amqp091", "amqp091", module.Description(), 5672, &module)
100+
_, err := zgrab2.AddCommand("amqp091", "Advanced Message Queue Protocol v0.9.1 (AMQP)", module.Description(), 5672, &module)
101101
if err != nil {
102102
log.Fatal(err)
103103
}

modules/bacnet/scanner.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ type Scanner struct {
3737
// RegisterModule registers the zgrab2 module.
3838
func RegisterModule() {
3939
var module Module
40-
_, err := zgrab2.AddCommand("bacnet", "bacnet", module.Description(), 0xBAC0, &module)
40+
_, err := zgrab2.AddCommand("bacnet", "Building Automation and Control Network (BACNET)", module.Description(), 0xBAC0, &module)
4141
if err != nil {
4242
log.Fatal(err)
4343
}

modules/banner/scanner.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ var ErrNoMatch = errors.New("pattern did not match")
7171
// RegisterModule is called by modules/banner.go to register the scanner.
7272
func RegisterModule() {
7373
var m Module
74-
_, err := zgrab2.AddCommand("banner", "Banner", m.Description(), 80, &m)
74+
_, err := zgrab2.AddCommand("banner", "Grabs the server's response to an arbitrary probe with optional regex matching", m.Description(), 80, &m)
7575
if err != nil {
7676
log.Fatal(err)
7777
}
@@ -122,7 +122,7 @@ func (f *Flags) Validate(_ []string) error {
122122

123123
// Description returns an overview of this module.
124124
func (m *Module) Description() string {
125-
return "Fetch a raw banner by sending a static probe and checking the result against a regular expression"
125+
return "Fetch a raw banner by sending a static probe and checking the result against an optional regular expression"
126126
}
127127

128128
// Help returns the module's help string.

modules/dnp3/scanner.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ type Scanner struct {
3535
// RegisterModule registers the zgrab2 module.
3636
func RegisterModule() {
3737
var module Module
38-
_, err := zgrab2.AddCommand("dnp3", "dnp3", module.Description(), 20000, &module)
38+
_, err := zgrab2.AddCommand("dnp3", "Distributed Network Protocol 3 (DNP3)", module.Description(), 20000, &module)
3939
if err != nil {
4040
log.Fatal(err)
4141
}

modules/fox/scanner.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ type Scanner struct {
3838
// RegisterModule registers the zgrab2 module.
3939
func RegisterModule() {
4040
var module Module
41-
_, err := zgrab2.AddCommand("fox", "fox", module.Description(), 1911, &module)
41+
_, err := zgrab2.AddCommand("fox", "Niagara Fox IoT and Building Automation Communication Protocol (Fox)", module.Description(), 1911, &module)
4242
if err != nil {
4343
log.Fatal(err)
4444
}

modules/ftp/scanner.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ type Connection struct {
8282
// RegisterModule registers the ftp zgrab2 module.
8383
func RegisterModule() {
8484
var module Module
85-
_, err := zgrab2.AddCommand("ftp", "FTP", module.Description(), 21, &module)
85+
_, err := zgrab2.AddCommand("ftp", "File Transfer Protocol (FTP)", module.Description(), 21, &module)
8686
if err != nil {
8787
log.Fatal(err)
8888
}

modules/http/scanner.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,7 @@ func (scanner *Scanner) Scan(ctx context.Context, dialGroup *zgrab2.DialerGroup,
615615
// zgrab2 framework.
616616
func RegisterModule() {
617617
var module Module
618-
cmd, err := zgrab2.AddCommand("http", "HTTP Banner Grab", module.Description(), 0, &module)
618+
cmd, err := zgrab2.AddCommand("http", "Hypertext Transfer Protocol (HTTP)", module.Description(), 0, &module)
619619
if err != nil {
620620
log.Fatal(err)
621621
}

0 commit comments

Comments
 (0)