mirror of
https://github.com/therootcompany/golib.git
synced 2026-11-04 15:05:00 +00:00
IPv4 host + CIDR membership testing backed by sorted /32 binary search and a linear scan of CIDR ranges. CSV/text loader with bounded reads (maxBytes is required so callers pick a limit appropriate to their input source — 1 KB allowlists through 100+ MB threat feeds). Returns errors for unparseable IPs so callers can decide fail-open vs fail-closed; partial cohorts are returned alongside parse errors.
186 lines
4.5 KiB
Go
186 lines
4.5 KiB
Go
// ipcohort-contains checks whether one or more IP addresses appear in a set
|
|
// of cohort files (plain text, one IP/CIDR per line).
|
|
//
|
|
// Usage:
|
|
//
|
|
// ipcohort-contains [flags] <file>... -- <ip>...
|
|
// ipcohort-contains [flags] --ip <ip> <file>...
|
|
// echo "<ip>" | ipcohort-contains <file>...
|
|
//
|
|
// Exit code: 0 if all queried IPs are found, 1 if any are not found, 2 on error.
|
|
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/therootcompany/golib/net/ipcohort"
|
|
)
|
|
|
|
// Replaced by goreleaser / ldflags at build time.
|
|
var (
|
|
name = "ipcohort-contains"
|
|
version = "0.0.0-dev"
|
|
commit = "0000000"
|
|
date = "0001-01-01"
|
|
licenseYear = "2021-present"
|
|
licenseOwner = "AJ ONeal <aj@therootcompany.com>"
|
|
licenseType = "MPL-2.0"
|
|
)
|
|
|
|
type Config struct {
|
|
IP string
|
|
Format string
|
|
}
|
|
|
|
func main() {
|
|
cfg := Config{}
|
|
fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
|
|
fs.StringVar(&cfg.IP, "ip", "", "IP address to check (alternative to -- separator)")
|
|
fs.StringVar(&cfg.Format, "format", "", "output format: pretty, tsv, csv, json (default: auto)")
|
|
fs.Usage = func() {
|
|
fmt.Fprintf(os.Stderr, "Usage: %s [flags] <file>... -- <ip>...\n", os.Args[0])
|
|
fmt.Fprintf(os.Stderr, " %s --ip <ip> <file>...\n", os.Args[0])
|
|
fmt.Fprintf(os.Stderr, " echo <ip> | %s <file>...\n", os.Args[0])
|
|
fs.PrintDefaults()
|
|
fmt.Fprintln(os.Stderr, "Exit: 0=all found, 1=not found, 2=error")
|
|
}
|
|
|
|
if len(os.Args) > 1 {
|
|
switch os.Args[1] {
|
|
case "-V", "-version", "--version", "version":
|
|
printVersion(os.Stdout)
|
|
os.Exit(0)
|
|
case "help", "-help", "--help":
|
|
printVersion(os.Stdout)
|
|
fmt.Fprintln(os.Stdout, "")
|
|
fs.SetOutput(os.Stdout)
|
|
fs.Usage()
|
|
os.Exit(0)
|
|
}
|
|
}
|
|
if err := fs.Parse(os.Args[1:]); err != nil {
|
|
if errors.Is(err, flag.ErrHelp) {
|
|
os.Exit(0)
|
|
}
|
|
os.Exit(2)
|
|
}
|
|
|
|
filePaths, ips := splitArgs(fs.Args(), cfg.IP)
|
|
if len(filePaths) == 0 {
|
|
fmt.Fprintln(os.Stderr, "error: at least one file path required")
|
|
fs.Usage()
|
|
os.Exit(2)
|
|
}
|
|
|
|
cohort := loadCohort(filePaths)
|
|
|
|
if len(ips) == 0 {
|
|
var err error
|
|
ips, err = readIPsFromStdin()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "error reading stdin: %v\n", err)
|
|
os.Exit(2)
|
|
}
|
|
}
|
|
if len(ips) == 0 {
|
|
fmt.Fprintln(os.Stderr, "error: no IP addresses to check")
|
|
fs.Usage()
|
|
os.Exit(2)
|
|
}
|
|
|
|
fmt.Fprintln(os.Stderr)
|
|
|
|
format, err := parseFormat(cfg.Format)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(2)
|
|
}
|
|
|
|
results, allFound := check(cohort, ips)
|
|
if err := writeTable(format, results); err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(2)
|
|
}
|
|
if !allFound {
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func printVersion(w io.Writer) {
|
|
_, _ = fmt.Fprintf(w, "%s v%s %s (%s)\n", name, version, commit[:7], date)
|
|
_, _ = fmt.Fprintf(w, "Copyright (C) %s %s\n", licenseYear, licenseOwner)
|
|
_, _ = fmt.Fprintf(w, "Licensed under %s\n", licenseType)
|
|
}
|
|
|
|
// splitArgs separates positional args into file paths and IPs, using either
|
|
// the --ip flag (single IP) or a `--` separator (multiple IPs). When neither
|
|
// is set, all args are file paths and IPs come from stdin.
|
|
func splitArgs(args []string, ipFlag string) (filePaths, ips []string) {
|
|
if ipFlag != "" {
|
|
return args, []string{ipFlag}
|
|
}
|
|
for i, a := range args {
|
|
if a == "--" {
|
|
return args[:i], args[i+1:]
|
|
}
|
|
}
|
|
return args, nil
|
|
}
|
|
|
|
func loadCohort(filePaths []string) *ipcohort.Cohort {
|
|
fmt.Fprint(os.Stderr, "Loading cohort... ")
|
|
t := time.Now()
|
|
cohort, err := ipcohort.LoadFiles(filePaths...)
|
|
if cohort == nil {
|
|
fmt.Fprintln(os.Stderr)
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(2)
|
|
}
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr)
|
|
fmt.Fprintf(os.Stderr, "warning: %v\n", err)
|
|
}
|
|
fmt.Fprintf(os.Stderr, "%s (entries=%s)\n",
|
|
time.Since(t).Round(time.Millisecond),
|
|
commafy(cohort.Size()),
|
|
)
|
|
return cohort
|
|
}
|
|
|
|
func readIPsFromStdin() ([]string, error) {
|
|
var ips []string
|
|
sc := bufio.NewScanner(os.Stdin)
|
|
for sc.Scan() {
|
|
line := strings.TrimSpace(sc.Text())
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
ips = append(ips, line)
|
|
}
|
|
return ips, sc.Err()
|
|
}
|
|
|
|
func check(cohort *ipcohort.Cohort, ips []string) (results []result, allFound bool) {
|
|
results = make([]result, 0, len(ips))
|
|
allFound = true
|
|
for _, ip := range ips {
|
|
found, err := cohort.Contains(ip)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(2)
|
|
}
|
|
results = append(results, result{IP: ip, Found: found})
|
|
if !found {
|
|
allFound = false
|
|
}
|
|
}
|
|
return results, allFound
|
|
}
|