Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tool to grep for ips that overlap #185

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6ad098e
init
talhahwahla Nov 7, 2023
1798dd0
Merge branch 'master' into talhahameed/be-2457-cli-tool-to-grep-for-i…
talhahwahla Nov 8, 2023
a69c666
improvements
talhahwahla Nov 13, 2023
689c69d
Merge branch 'master' into talhahameed/be-2457-cli-tool-to-grep-for-i…
talhahwahla Nov 15, 2023
64c9135
update matchip help
talhahwahla Nov 15, 2023
4d1e8c9
use --expresssion flag only
talhahwahla Nov 15, 2023
633a31c
rm `parseInput` from `findOverlapping`
talhahwahla Nov 15, 2023
e706ece
rm usage of `FilterFile` and `CriteriaFile` flags
talhahwahla Nov 15, 2023
53c0486
rm `scanrdr`, use `ProcessStringsFromStdin`, `ProcessStringsFromFile`
talhahwahla Nov 15, 2023
ea0aa85
separate `source_op` and `filter_op`
talhahwahla Nov 15, 2023
b8e2243
parse ip
talhahwahla Nov 15, 2023
add06f6
add `rangeToCidrs()`
talhahwahla Nov 15, 2023
4e3a3d6
parse cidr, use SubnetPair
talhahwahla Nov 15, 2023
4c9643c
fix filter_op, source_op
talhahwahla Nov 15, 2023
7413b78
export InputHelper for external use
talhahwahla Nov 15, 2023
c14f828
accept inline input
talhahwahla Nov 15, 2023
60157bf
fix code repetition
talhahwahla Nov 15, 2023
37cea24
refactor
talhahwahla Nov 15, 2023
fd344b9
print help when no arg provided
talhahwahla Nov 15, 2023
0d9102b
handle matchip completions
talhahwahla Nov 15, 2023
511d6dd
fix matchip help
talhahwahla Nov 15, 2023
2ef983c
make `matchip` available as a separate tool
talhahwahla Nov 15, 2023
a0bd1a6
make `matchip` available as a separate tool
talhahwahla Nov 15, 2023
43673fc
update help
talhahwahla Nov 15, 2023
132edf1
update README.md
talhahwahla Nov 15, 2023
9404e69
update .gitignore
talhahwahla Nov 15, 2023
2a4f616
update default help
talhahwahla Nov 15, 2023
3e8fb39
update scripts
talhahwahla Nov 15, 2023
a7b9e96
fix vsn
talhahwahla Nov 15, 2023
67f4b69
fix spacing
talhahwahla Nov 16, 2023
9744342
Merge branch 'master' into talhahameed/be-2457-cli-tool-to-grep-for-i…
talhahwahla Nov 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions ipinfo/cmd_match_ip.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package main

import (
"fmt"

"github.com/ipinfo/cli/lib"
"github.com/spf13/pflag"
)

func printHelpMatchIP() {
fmt.Printf(
`Usage: %s matchip --filter <file(s) | stdin> --criteria <file(s) | stdin>
Copy link
Contributor

@UmanShahzad UmanShahzad Nov 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A more ideal form is one that matches grep's solution to this:

# single expression + single file
matchip 127.0.0.1 file1.txt

# single expression + multiple files
matchip 127.0.0.1 file1.txt file2.txt file3.txt

# multi expression + any files
cat expression-list1.txt | matchip -e 127.0.0.1 -e 8.8.8.8 -e - -e expression-list2.txt file1.txt file2.txt file3.txt

I.e., use an 'expression' or 'input' flag to label each input, equivalent to the --filter flag you currently have. However -f wouldn't be a great option since it commonly stands for file so could cause confusion. -e / --expression is what grep uses, so in the spirit of trying to re-use people's habits / knowledge of grep like we did in grepip, maybe that's ideal.

Description:
Prints the overlapping IPs and subnets.

Examples:
# Match from a file
$ %[1]s matchip --filter /path/to/list1.txt --criteria /path/to/list2.txt

# Match from multiple files
$ %[1]s matchip --filter=/path/to/list.txt,/path/to/list1.txt --criteria=/path/to/list2.txt,/path/to/list3.txt

# Match from stdin
$ cat /path/to/list1.txt | %[1]s matchip --filter - --criteria /path/to/list2.txt

Options:
General:
--filter, -f
IPs, CIDRs, and/or Ranges to be filtered.
--criteria, -c
CIDRs and/or Ranges to check overlap with.
--help
show help.
`, progBase)
}

func cmdMatchIP() error {
f := lib.CmdMatchIPFlags{}
f.Init()
pflag.Parse()

return lib.CmdMatchIP(f, pflag.Args()[1:], printHelpMatchIP)
}
2 changes: 2 additions & 0 deletions ipinfo/main.go
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Also add completions to main tool
  • Also add its doc in main ipinfo CLI doc
  • Also make it available as a separate tool like grepip (copy/paste, change name & version)
  • Ensure anywhere else we see grepip being used in e.g. build tools / etc, this is added there

Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ func main() {
err = cmdPrips()
case cmd == "grepip":
err = cmdGrepIP()
case cmd == "matchip":
err = cmdMatchIP()
case cmd == "cidr2range":
err = cmdCIDR2Range()
case cmd == "cidr2ip":
Expand Down
259 changes: 259 additions & 0 deletions lib/cmd_match_ip.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
package lib

import (
"bufio"
"fmt"
"io"
"net"
"os"
"strings"

"github.com/spf13/pflag"
)

type CmdMatchIPFlags struct {
FilterFile []string
CriteriaFile []string
Help bool
}

func (f *CmdMatchIPFlags) Init() {
pflag.StringSliceVarP(
&f.FilterFile,
"filter", "f", nil,
"IPs, subnets to be filtered.",
)
pflag.StringSliceVarP(
&f.CriteriaFile,
"criteria", "c", nil,
"subnets to check overlap with.",
)
pflag.BoolVarP(
&f.Help,
"help", "", false,
"show help.",
)
}

func CmdMatchIP(
f CmdMatchIPFlags,
args []string,
printHelp func(),
) error {
if f.Help || f.FilterFile[0] == "" || f.CriteriaFile[0] == "" {
printHelp()
return nil
}

stat, _ := os.Stdin.Stat()
isStdin := (stat.Mode() & os.ModeCharDevice) == 0

scanrdr := func(r io.Reader) ([]string, error) {
var hitEOF bool
buf := bufio.NewReader(r)
var ips []string

for {
if hitEOF {
return ips, nil
}

d, err := buf.ReadString('\n')
d = strings.TrimRight(d, "\n")
if err == io.EOF {
if len(d) == 0 {
return ips, nil
}

hitEOF = true
} else if err != nil {
return ips, err
}

if len(d) == 0 {
continue
}

ips = append(ips, d)
}
}

var filter []string
var err error
if len(f.FilterFile) == 1 && f.FilterFile[0] == "-" && isStdin {
filter, err = scanrdr(os.Stdin)
if err != nil {
return err
}
} else {
for _, file := range f.FilterFile {
f, err := os.Open(file)
if err != nil {
return err
}

res, err := scanrdr(f)
if err != nil {
return err
}
filter = append(filter, res...)
}
}

var criteria []string
if len(f.CriteriaFile) == 1 && f.CriteriaFile[0] == "-" && isStdin {
criteria, err = scanrdr(os.Stdin)
if err != nil {
return err
}
} else {
for _, file := range f.CriteriaFile {
f, err := os.Open(file)
if err != nil {
return err
}

res, err := scanrdr(f)
if err != nil {
return err
}
criteria = append(criteria, res...)
}
}

matches := findOverlapping(filter, criteria)
for _, v := range matches {
fmt.Println(v)
}

return nil
}

type SubnetPair struct {
Raw string
Parsed []net.IPNet
}

func findOverlapping(filter, criteria []string) []string {
parseInput := func(rows []string) ([]SubnetPair, []net.IP) {
parseCIDRs := func(cidrs []string) []net.IPNet {
parsedCIDRs := make([]net.IPNet, 0)
for _, cidrStr := range cidrs {
_, ipNet, err := net.ParseCIDR(cidrStr)
if err != nil {
continue
}
parsedCIDRs = append(parsedCIDRs, *ipNet)
}

return parsedCIDRs
}

parsedCIDRs := make([]SubnetPair, 0)
parsedIPs := make([]net.IP, 0)
var separator string
for _, rowStr := range rows {
if strings.ContainsAny(rowStr, ",-") {
if delim := strings.ContainsRune(rowStr, ','); delim {
separator = ","
} else {
separator = "-"
}

ipRange := strings.Split(rowStr, separator)
if len(ipRange) != 2 {
continue
}

if strings.ContainsRune(rowStr, ':') {
cidrs, err := CIDRsFromIP6RangeStrRaw(rowStr)
if err == nil {
pair := SubnetPair{
Raw: rowStr,
Parsed: parseCIDRs(cidrs),
}
parsedCIDRs = append(parsedCIDRs, pair)
} else {
continue
}
} else {
cidrs, err := CIDRsFromIPRangeStrRaw(rowStr)
if err == nil {
pair := SubnetPair{
Raw: rowStr,
Parsed: parseCIDRs(cidrs),
}
parsedCIDRs = append(parsedCIDRs, pair)
} else {
continue
}
}
} else if strings.ContainsRune(rowStr, '/') {
pair := SubnetPair{
Raw: rowStr,
Parsed: parseCIDRs([]string{rowStr}),
}
parsedCIDRs = append(parsedCIDRs, pair)
} else {
if ip := net.ParseIP(rowStr); ip != nil {
parsedIPs = append(parsedIPs, ip)
}
}
}

return parsedCIDRs, parsedIPs
}

sourceCIDRs, sourceIPs := parseInput(filter)
filterCIDRs, filterIPs := parseInput(criteria)

var matches []string
for _, sourceCIDR := range sourceCIDRs {
foundMatch := false
for _, v := range sourceCIDR.Parsed {
for _, filterCIDR := range filterCIDRs {
for _, fv := range filterCIDR.Parsed {
if isCIDROverlapping(&v, &fv) {
if !foundMatch {
matches = append(matches, sourceCIDR.Raw)
foundMatch = true
}
break
}
}
}
}
}

for _, sourceIP := range sourceIPs {
foundMatch := false
for _, filterCIDR := range filterCIDRs {
for _, fv := range filterCIDR.Parsed {
if isIPRangeOverlapping(&sourceIP, &fv) {
if !foundMatch {
matches = append(matches, sourceIP.String())
foundMatch = true
}
break
}
}
}

for _, filterIP := range filterIPs {
if sourceIP.String() == filterIP.String() {
matches = append(matches, sourceIP.String())
break
}
}
}

return matches
}

func isCIDROverlapping(cidr1, cidr2 *net.IPNet) bool {
return cidr1.Contains(cidr2.IP) || cidr2.Contains(cidr1.IP)
}

func isIPRangeOverlapping(ip *net.IP, cidr *net.IPNet) bool {
return cidr.Contains(*ip)
}