Skip to content

Commit 3f0c983

Browse files
committed
feat: basic implementation for filter expressions
1 parent 2ad1104 commit 3f0c983

File tree

7 files changed

+221
-21
lines changed

7 files changed

+221
-21
lines changed

cmd/root.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,9 @@ var config = formatter.Config{
4141
SqliteOutputOptions: formatter.SqliteOutputOptions{},
4242
ExcelOptions: formatter.ExcelOutputOptions{},
4343
},
44-
ShowVersion: false,
45-
CurrentVersion: VERSION,
44+
ShowVersion: false,
45+
CurrentVersion: VERSION,
46+
FilterExpressions: []string{},
4647
}
4748

4849
// VERSION is describing current version of the nmap-formatter
@@ -125,6 +126,9 @@ func init() {
125126
// Configs related to D2 language
126127
rootCmd.Flags().BoolVar(&config.OutputOptions.D2LangOptions.SkipDownHosts, "d2-skip-down-hosts", true, "--d2-skip-down-hosts=false, would print all hosts that are offline in D2 language output")
127128

129+
// Multiple filter expressions supported
130+
rootCmd.Flags().StringArrayVar(&config.FilterExpressions, "filter", []string{}, "--filter '.Status.State == \"up\" && any(.Port, { .PortID in [80,443] })'")
131+
128132
workflow = &formatter.MainWorkflow{}
129133
}
130134

formatter/config.go

+10-9
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,16 @@ import (
1010
// where output will be delivered, desired output format, input file path, output file path
1111
// and different output options
1212
type Config struct {
13-
Writer io.WriteCloser
14-
OutputFormat OutputFormat
15-
InputFileConfig InputFileConfig
16-
OutputFile OutputFile
17-
OutputOptions OutputOptions
18-
ShowVersion bool
19-
TemplatePath string
20-
CustomOptions []string
21-
CurrentVersion string
13+
Writer io.WriteCloser
14+
OutputFormat OutputFormat
15+
InputFileConfig InputFileConfig
16+
OutputFile OutputFile
17+
OutputOptions OutputOptions
18+
ShowVersion bool
19+
TemplatePath string
20+
CustomOptions []string
21+
CurrentVersion string
22+
FilterExpressions []string
2223
}
2324

2425
// CustomOptionsMap returns custom options provided in the CLI

formatter/expr.go

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package formatter
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/expr-lang/expr"
7+
)
8+
9+
// filterExpr filters NMAPRun.Hosts by given expression
10+
func filterExpr(r NMAPRun, code string) (NMAPRun, error) {
11+
program, err := expr.Compile(
12+
fmt.Sprintf("filter(Host, { %s })", code),
13+
expr.Env(r),
14+
)
15+
16+
if err != nil {
17+
return r, err
18+
}
19+
20+
output, err := expr.Run(program, r)
21+
if err != nil {
22+
return r, err
23+
}
24+
25+
hosts, err := convertToHosts(output)
26+
27+
if err != nil {
28+
return r, err
29+
}
30+
31+
r.Host = hosts
32+
return r, nil
33+
}
34+
35+
// convertToHosts converts output from expression engine to []Host
36+
func convertToHosts(output interface{}) ([]Host, error) {
37+
outputInterfaces, ok := output.([]interface{})
38+
if !ok {
39+
return nil, fmt.Errorf("output is not []interface{}")
40+
}
41+
42+
hosts := make([]Host, len(outputInterfaces))
43+
for i, v := range outputInterfaces {
44+
host, ok := v.(Host)
45+
if !ok {
46+
return nil, fmt.Errorf("element is not Host")
47+
}
48+
hosts[i] = host
49+
}
50+
51+
return hosts, nil
52+
}

formatter/expr_test.go

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package formatter
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
)
7+
8+
func Test_filterExpr(t *testing.T) {
9+
type args struct {
10+
nmapRUN NMAPRun
11+
code string
12+
}
13+
tests := []struct {
14+
name string
15+
args args
16+
want NMAPRun
17+
wantErr bool
18+
}{
19+
{
20+
name: "Basic test",
21+
args: args{
22+
nmapRUN: NMAPRun{
23+
Scanner: "",
24+
Args: "",
25+
Start: 0,
26+
StartStr: "",
27+
Version: "",
28+
ScanInfo: ScanInfo{},
29+
Host: []Host{
30+
{
31+
StartTime: 0,
32+
EndTime: 0,
33+
Port: []Port{
34+
{
35+
Protocol: "http",
36+
PortID: 80,
37+
State: PortState{},
38+
Service: PortService{},
39+
Script: []Script{},
40+
},
41+
},
42+
HostAddress: []HostAddress{
43+
{
44+
Address: "10.10.10.1",
45+
AddressType: "ipv4",
46+
Vendor: "",
47+
},
48+
},
49+
HostNames: HostNames{},
50+
Status: HostStatus{
51+
State: "up",
52+
Reason: "",
53+
},
54+
OS: OS{},
55+
Trace: Trace{},
56+
Uptime: Uptime{},
57+
Distance: Distance{},
58+
TCPSequence: TCPSequence{},
59+
IPIDSequence: IPIDSequence{},
60+
TCPTSSequence: TCPTSSequence{},
61+
},
62+
{
63+
StartTime: 0,
64+
EndTime: 0,
65+
Port: []Port{
66+
{
67+
Protocol: "",
68+
PortID: 22,
69+
State: PortState{},
70+
Service: PortService{},
71+
Script: []Script{},
72+
},
73+
},
74+
HostAddress: []HostAddress{
75+
{
76+
Address: "10.10.10.20",
77+
AddressType: "ipv4",
78+
Vendor: "",
79+
},
80+
},
81+
HostNames: HostNames{},
82+
Status: HostStatus{},
83+
OS: OS{},
84+
Trace: Trace{},
85+
Uptime: Uptime{},
86+
Distance: Distance{},
87+
TCPSequence: TCPSequence{},
88+
IPIDSequence: IPIDSequence{},
89+
TCPTSSequence: TCPTSSequence{},
90+
},
91+
},
92+
Verbose: Verbose{},
93+
Debugging: Debugging{},
94+
RunStats: RunStats{},
95+
},
96+
code: `.Status.State == "up" && any(.Port, { .PortID in [80] })`,
97+
},
98+
want: NMAPRun{
99+
Scanner: "",
100+
Args: "",
101+
Start: 0,
102+
StartStr: "",
103+
Version: "",
104+
ScanInfo: ScanInfo{},
105+
Host: []Host{{StartTime: 0, EndTime: 0, Port: []Port{
106+
{
107+
Protocol: "http",
108+
PortID: 80,
109+
State: PortState{},
110+
Service: PortService{},
111+
Script: []Script{},
112+
},
113+
}, HostAddress: []HostAddress{{Address: "10.10.10.1", AddressType: "ipv4", Vendor: ""}}, HostNames: HostNames{}, Status: HostStatus{State: "up", Reason: ""}, OS: OS{}, Trace: Trace{}, Uptime: Uptime{}, Distance: Distance{}, TCPSequence: TCPSequence{}, IPIDSequence: IPIDSequence{}, TCPTSSequence: TCPTSSequence{}}},
114+
Verbose: Verbose{},
115+
Debugging: Debugging{},
116+
RunStats: RunStats{},
117+
},
118+
wantErr: false,
119+
},
120+
}
121+
for _, tt := range tests {
122+
t.Run(tt.name, func(t *testing.T) {
123+
got, err := filterExpr(tt.args.nmapRUN, tt.args.code)
124+
if (err != nil) != tt.wantErr {
125+
t.Errorf("filterExpr() error = %v, wantErr %v", err, tt.wantErr)
126+
return
127+
}
128+
if !reflect.DeepEqual(got, tt.want) {
129+
t.Errorf("filterExpr() = %+v, want %+v", got, tt.want)
130+
}
131+
})
132+
}
133+
}

formatter/workflow.go

+11-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package formatter
33
import (
44
"encoding/xml"
55
"fmt"
6+
"log"
67
"os"
78
)
89

@@ -57,9 +58,18 @@ func (w *MainWorkflow) Execute() (err error) {
5758
return
5859
}
5960

61+
filteredRun := NMAPRun
62+
for _, expr := range w.Config.FilterExpressions {
63+
log.Printf("filtering with expression: %s", expr)
64+
filteredRun, err = filterExpr(filteredRun, expr)
65+
if err != nil {
66+
return fmt.Errorf("error filtering: %v", err)
67+
}
68+
}
69+
6070
// Build template data with NMAPRun entry & various output options
6171
templateData := TemplateData{
62-
NMAPRun: NMAPRun,
72+
NMAPRun: filteredRun,
6373
OutputOptions: w.Config.OutputOptions,
6474
}
6575

go.mod

+5-7
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ module github.com/vdjagilev/nmap-formatter/v2
33
go 1.22.2
44

55
require (
6+
github.com/expr-lang/expr v1.16.5
67
github.com/google/uuid v1.6.0
78
github.com/mattn/go-sqlite3 v1.14.22
89
github.com/spf13/cobra v1.8.0
10+
github.com/xuri/excelize/v2 v2.8.1
911
golang.org/x/net v0.24.0
1012
oss.terrastruct.com/d2 v0.6.5
1113
)
@@ -23,6 +25,7 @@ require (
2325
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
2426
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
2527
github.com/google/pprof v0.0.0-20231205033806-a5a03c77bf08 // indirect
28+
github.com/inconshreveable/mousetrap v1.1.0 // indirect
2629
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
2730
github.com/mattn/go-colorable v0.1.9 // indirect
2831
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -31,8 +34,9 @@ require (
3134
github.com/richardlehane/mscfb v1.0.4 // indirect
3235
github.com/richardlehane/msoleps v1.0.3 // indirect
3336
github.com/rivo/uniseg v0.4.4 // indirect
37+
github.com/spf13/pflag v1.0.5 // indirect
3438
github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 // indirect
35-
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect
39+
github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 // indirect
3640
github.com/yuin/goldmark v1.6.0 // indirect
3741
go.opencensus.io v0.24.0 // indirect
3842
golang.org/x/crypto v0.22.0 // indirect
@@ -45,9 +49,3 @@ require (
4549
gonum.org/v1/plot v0.14.0 // indirect
4650
oss.terrastruct.com/util-go v0.0.0-20231101220827-55b3812542c2 // indirect
4751
)
48-
49-
require (
50-
github.com/inconshreveable/mousetrap v1.1.0 // indirect
51-
github.com/spf13/pflag v1.0.5 // indirect
52-
github.com/xuri/excelize/v2 v2.8.1
53-
)

go.sum

+4-2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
4747
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
4848
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
4949
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
50+
github.com/expr-lang/expr v1.16.5 h1:m2hvtguFeVaVNTHj8L7BoAyt7O0PAIBaSVbjdHgRXMs=
51+
github.com/expr-lang/expr v1.16.5/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ=
5052
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
5153
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
5254
github.com/go-fonts/liberation v0.3.1 h1:9RPT2NhUpxQ7ukUvz3jeUckmN42T9D9TpjtQcqK/ceM=
@@ -141,8 +143,8 @@ github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 h1:Chd9DkqERQQuHpXjR/HSV1
141143
github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
142144
github.com/xuri/excelize/v2 v2.8.1 h1:pZLMEwK8ep+CLIUWpWmvW8IWE/yxqG0I1xcN6cVMGuQ=
143145
github.com/xuri/excelize/v2 v2.8.1/go.mod h1:oli1E4C3Pa5RXg1TBXn4ENCXDV5JUMlBluUhG7c+CEE=
144-
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A=
145-
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
146+
github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 h1:qhbILQo1K3mphbwKh1vNm4oGezE1eF9fQWmNiIpSfI4=
147+
github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
146148
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
147149
github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
148150
github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=

0 commit comments

Comments
 (0)