diff --git a/pkg/scanning/poc_test.go b/pkg/scanning/poc_test.go new file mode 100644 index 00000000..3aee3378 --- /dev/null +++ b/pkg/scanning/poc_test.go @@ -0,0 +1,124 @@ +package scanning + +import ( + "io" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/hahwul/dalfox/v2/pkg/model" +) + +func TestMakePoC(t *testing.T) { + type args struct { + poc string + req *http.Request + options model.Options + } + tests := []struct { + name string + args args + want string + }{ + { + name: "HTTP RAW REQUEST", + args: args{ + poc: "http://example.com", + req: func() *http.Request { + req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil) + return req + }(), + options: model.Options{ + PoCType: "http-request", + }, + }, + want: "HTTP RAW REQUEST\nGET / HTTP/1.1\r\nHost: example.com\r\nUser-Agent: Go-http-client/1.1\r\nAccept-Encoding: gzip\r\n\r\n", + }, + { + name: "curl with body", + args: args{ + poc: "http://example.com", + req: func() *http.Request { + body := ioutil.NopCloser(strings.NewReader("test body")) + req, _ := http.NewRequest(http.MethodPost, "http://example.com", body) + req.GetBody = func() (io.ReadCloser, error) { + return ioutil.NopCloser(strings.NewReader("test body")), nil + } + return req + }(), + options: model.Options{ + PoCType: "curl", + }, + }, + want: "curl -i -k -X POST http://example.com -d \"test body\"", + }, + { + name: "httpie with body", + args: args{ + poc: "http://example.com", + req: func() *http.Request { + body := ioutil.NopCloser(strings.NewReader("test body")) + req, _ := http.NewRequest(http.MethodPost, "http://example.com", body) + req.GetBody = func() (io.ReadCloser, error) { + return ioutil.NopCloser(strings.NewReader("test body")), nil + } + return req + }(), + options: model.Options{ + PoCType: "httpie", + }, + }, + want: "http POST http://example.com \"test body\" --verify=false -f", + }, + { + name: "curl without body", + args: args{ + poc: "http://example.com", + req: func() *http.Request { + req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil) + return req + }(), + options: model.Options{ + PoCType: "curl", + }, + }, + want: "curl -i -k http://example.com", + }, + { + name: "httpie without body", + args: args{ + poc: "http://example.com", + req: func() *http.Request { + req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil) + return req + }(), + options: model.Options{ + PoCType: "httpie", + }, + }, + want: "http http://example.com --verify=false", + }, + { + name: "default without body", + args: args{ + poc: "http://example.com", + req: func() *http.Request { + req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil) + return req + }(), + options: model.Options{ + PoCType: "default", + }, + }, + want: "http://example.com", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := MakePoC(tt.args.poc, tt.args.req, tt.args.options); got != tt.want { + t.Errorf("MakePoC() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/scanning/queries_test.go b/pkg/scanning/queries_test.go new file mode 100644 index 00000000..b07d07f9 --- /dev/null +++ b/pkg/scanning/queries_test.go @@ -0,0 +1,67 @@ +package scanning + +import "testing" + +func Test_checkVStatus(t *testing.T) { + type args struct { + vStatus map[string]bool + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "All true values", + args: args{ + vStatus: map[string]bool{ + "status1": true, + "status2": true, + }, + }, + want: true, + }, + { + name: "Contains false value", + args: args{ + vStatus: map[string]bool{ + "status1": true, + "status2": false, + }, + }, + want: false, + }, + { + name: "Special key with false value", + args: args{ + vStatus: map[string]bool{ + "pleasedonthaveanamelikethis_plz_plz": false, + }, + }, + want: false, + }, + { + name: "Special key with true value", + args: args{ + vStatus: map[string]bool{ + "pleasedonthaveanamelikethis_plz_plz": true, + }, + }, + want: false, + }, + { + name: "Empty map", + args: args{ + vStatus: map[string]bool{}, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := checkVStatus(tt.args.vStatus); got != tt.want { + t.Errorf("checkVStatus() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/scanning/sendReq_test.go b/pkg/scanning/sendReq_test.go new file mode 100644 index 00000000..479aab6c --- /dev/null +++ b/pkg/scanning/sendReq_test.go @@ -0,0 +1,84 @@ +package scanning + +import ( + "net/http" + "strings" + "testing" + + "github.com/hahwul/dalfox/v2/pkg/model" +) + +func TestSendReq(t *testing.T) { + type args struct { + req *http.Request + payload string + options model.Options + } + tests := []struct { + name string + args args + want string + want1 *http.Response + want2 bool + want3 bool + wantErr bool + }{ + { + name: "Successful request", + args: args{ + req: func() *http.Request { + req, _ := http.NewRequest(http.MethodGet, "https://dalfox.hahwul.com", nil) + return req + }(), + payload: "test-payload", + options: model.Options{ + Timeout: 10, + }, + }, + want: "dalfox", + want1: &http.Response{StatusCode: http.StatusOK}, + want2: false, + want3: false, + wantErr: false, + }, + { + name: "Request with error", + args: args{ + req: func() *http.Request { + req, _ := http.NewRequest(http.MethodGet, "http://invalid-url", nil) + return req + }(), + payload: "test-payload", + options: model.Options{ + Timeout: 10, + }, + }, + want: "", + want1: nil, + want2: false, + want3: false, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1, got2, got3, err := SendReq(tt.args.req, tt.args.payload, tt.args.options) + if (err != nil) != tt.wantErr { + t.Errorf("SendReq() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !strings.Contains(got, tt.want) { + t.Errorf("SendReq() got = %v, want %v", got, tt.want) + } + if got1 != nil && tt.want1 != nil && got1.StatusCode != tt.want1.StatusCode { + t.Errorf("SendReq() got1 = %v, want %v", got1.StatusCode, tt.want1.StatusCode) + } + if got2 != tt.want2 { + t.Errorf("SendReq() got2 = %v, want %v", got2, tt.want2) + } + if got3 != tt.want3 { + t.Errorf("SendReq() got3 = %v, want %v", got3, tt.want3) + } + }) + } +} diff --git a/pkg/scanning/staticAnlaysis_test.go b/pkg/scanning/staticAnlaysis_test.go new file mode 100644 index 00000000..0df6d6cf --- /dev/null +++ b/pkg/scanning/staticAnlaysis_test.go @@ -0,0 +1,162 @@ +package scanning + +import ( + "net/http" + "testing" + "time" + + "github.com/hahwul/dalfox/v2/pkg/model" + "github.com/stretchr/testify/assert" +) + +type mockRequestSender struct{} + +func (m *mockRequestSender) SendReq(req *http.Request, payload string, options model.Options) (string, *http.Response, string, bool, error) { + return "response body with dalfoxpathtest", &http.Response{ + Header: http.Header{ + "Content-Type": []string{"text/html; charset=UTF-8"}, + }, + }, "", true, nil +} + +func Test_checkPathReflection(t *testing.T) { + type args struct { + tempURL string + id int + options model.Options + rl *rateLimiter + pathReflection map[int]string + } + tests := []struct { + name string + args args + want map[int]string + }{ + { + name: "Path reflection found", + args: args{ + tempURL: "http://example.com/dalfoxpathtest", + id: 0, + options: model.Options{}, + rl: newRateLimiter(time.Duration(0)), + pathReflection: map[int]string{ + 0: "Injected: /dalfoxpathtest(1)", + }, + }, + want: map[int]string{ + 0: "Injected: /dalfoxpathtest(1)", + }, + }, + { + name: "Path reflection not found", + args: args{ + tempURL: "http://example.com/", + id: 0, + options: model.Options{}, + rl: newRateLimiter(time.Duration(0)), + pathReflection: map[int]string{}, + }, + want: map[int]string{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + checkPathReflection(tt.args.tempURL, tt.args.id, tt.args.options, tt.args.rl, tt.args.pathReflection) + assert.Equal(t, tt.want, tt.args.pathReflection) + }) + } +} + +func Test_extractPolicyHeaders(t *testing.T) { + tests := []struct { + name string + header http.Header + want map[string]string + }{ + { + name: "All headers present", + header: http.Header{ + "Content-Type": []string{"text/html"}, + "Content-Security-Policy": []string{"default-src 'self'"}, + "X-Frame-Options": []string{"DENY"}, + "Strict-Transport-Security": []string{"max-age=31536000; includeSubDomains"}, + "Access-Control-Allow-Origin": []string{"*"}, + }, + want: map[string]string{ + "Content-Type": "text/html", + "Content-Security-Policy": "default-src 'self'", + "X-Frame-Options": "DENY", + "Strict-Transport-Security": "max-age=31536000; includeSubDomains", + "Access-Control-Allow-Origin": "*", + }, + }, + { + name: "Some headers missing", + header: http.Header{ + "Content-Type": []string{"text/html"}, + "X-Frame-Options": []string{"DENY"}, + "Strict-Transport-Security": []string{"max-age=31536000; includeSubDomains"}, + }, + want: map[string]string{ + "Content-Type": "text/html", + "X-Frame-Options": "DENY", + "Strict-Transport-Security": "max-age=31536000; includeSubDomains", + }, + }, + { + name: "No headers present", + header: http.Header{}, + want: map[string]string{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + policy := make(map[string]string) + extractPolicyHeaders(tt.header, policy) + assert.Equal(t, tt.want, policy) + }) + } +} + +func Test_StaticAnalysis(t *testing.T) { + tests := []struct { + name string + target string + options model.Options + wantPolicy map[string]string + wantReflection map[int]string + }{ + { + name: "Static analysis with path reflection", + target: "http://example.com/dalfoxpathtest", + options: model.Options{ + Timeout: 10, + Delay: 1, + }, + wantPolicy: map[string]string{ + "Content-Type": "text/html; charset=UTF-8", + }, + wantReflection: map[int]string{ + 0: "Injected: /dalfoxpathtest(1)", + }, + }, + { + name: "Static analysis without path reflection", + target: "http://example.com/", + options: model.Options{ + Timeout: 10, + Delay: 1, + }, + wantPolicy: map[string]string{ + "Content-Type": "text/html; charset=UTF-8", + }, + wantReflection: map[int]string{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rl := newRateLimiter(time.Duration(tt.options.Delay * 1000000)) + StaticAnalysis(tt.target, tt.options, rl) + }) + } +} diff --git a/pkg/scanning/transport_test.go b/pkg/scanning/transport_test.go new file mode 100644 index 00000000..253668fb --- /dev/null +++ b/pkg/scanning/transport_test.go @@ -0,0 +1,113 @@ +package scanning + +import ( + "crypto/tls" + "net" + "net/http" + "net/url" + "os" + "testing" + "time" + + "github.com/hahwul/dalfox/v2/pkg/har" + "github.com/hahwul/dalfox/v2/pkg/model" +) + +func Test_getTransport(t *testing.T) { + type args struct { + options model.Options + } + tests := []struct { + name string + args args + want func() http.RoundTripper + }{ + { + name: "Default transport", + args: args{ + options: model.Options{ + Timeout: 10, + }, + }, + want: func() http.RoundTripper { + return &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + Renegotiation: tls.RenegotiateOnceAsClient, + }, + DisableKeepAlives: true, + DialContext: (&net.Dialer{ + Timeout: 10 * time.Second, + DualStack: true, + }).DialContext, + } + }, + }, + { + name: "Transport with proxy", + args: args{ + options: model.Options{ + Timeout: 10, + ProxyAddress: "http://localhost:8080", + }, + }, + want: func() http.RoundTripper { + return &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + Renegotiation: tls.RenegotiateOnceAsClient, + }, + DisableKeepAlives: true, + DialContext: (&net.Dialer{ + Timeout: 10 * time.Second, + DualStack: true, + }).DialContext, + Proxy: http.ProxyURL(&url.URL{ + Scheme: "http", + Host: "localhost:8080", + }), + } + }, + }, + { + name: "Transport with HAR writer", + args: args{ + options: model.Options{ + Timeout: 10, + }, + }, + want: func() http.RoundTripper { + transport := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + Renegotiation: tls.RenegotiateOnceAsClient, + }, + DisableKeepAlives: true, + DialContext: (&net.Dialer{ + Timeout: 10 * time.Second, + DualStack: true, + }).DialContext, + } + file, err := os.CreateTemp("", "har_writer_test") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(file.Name()) + harWriter, err := har.NewWriter(file, &har.Creator{Name: "dalfox", Version: "v2.0.0"}) + if err != nil { + t.Fatalf("Failed to create HAR writer: %v", err) + } + return har.NewRoundTripper(transport, harWriter, rewrite) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := getTransport(tt.args.options) + want := tt.want() + if _, ok := got.(http.RoundTripper); !ok { + t.Errorf("getTransport() = %v, want %v", got, want) + } + }) + } +} diff --git a/pkg/scanning/utils_test.go b/pkg/scanning/utils_test.go new file mode 100644 index 00000000..07bd940e --- /dev/null +++ b/pkg/scanning/utils_test.go @@ -0,0 +1,147 @@ +package scanning + +import ( + "testing" + + "github.com/hahwul/dalfox/v2/pkg/model" +) + +func Test_indexOf(t *testing.T) { + type args struct { + element string + data []string + } + tests := []struct { + name string + args args + want int + }{ + { + name: "Element found", + args: args{element: "b", data: []string{"a", "b", "c"}}, + want: 1, + }, + { + name: "Element not found", + args: args{element: "d", data: []string{"a", "b", "c"}}, + want: -1, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := indexOf(tt.args.element, tt.args.data); got != tt.want { + t.Errorf("indexOf() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_duplicatedResult(t *testing.T) { + type args struct { + result []model.PoC + rst model.PoC + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "Duplicate found", + args: args{ + result: []model.PoC{{Type: "type1"}, {Type: "type2"}}, + rst: model.PoC{Type: "type1"}, + }, + want: true, + }, + { + name: "Duplicate not found", + args: args{ + result: []model.PoC{{Type: "type1"}, {Type: "type2"}}, + rst: model.PoC{Type: "type3"}, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := duplicatedResult(tt.args.result, tt.args.rst); got != tt.want { + t.Errorf("duplicatedResult() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_containsFromArray(t *testing.T) { + type args struct { + slice []string + item string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "Item found", + args: args{slice: []string{"a", "b", "c"}, item: "b"}, + want: true, + }, + { + name: "Item not found", + args: args{slice: []string{"a", "b", "c"}, item: "d"}, + want: false, + }, + { + name: "Item found with parentheses", + args: args{slice: []string{"a", "b", "c"}, item: "b(something)"}, + want: true, + }, + { + name: "Item not found with parentheses", + args: args{slice: []string{"a", "b", "c"}, item: "d(something)"}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := containsFromArray(tt.args.slice, tt.args.item); got != tt.want { + t.Errorf("containsFromArray() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_checkPType(t *testing.T) { + type args struct { + str string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "Valid type", + args: args{str: "validType"}, + want: true, + }, + { + name: "Invalid type toBlind", + args: args{str: "toBlind"}, + want: false, + }, + { + name: "Invalid type toGrepping", + args: args{str: "toGrepping"}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := checkPType(tt.args.str); got != tt.want { + t.Errorf("checkPType() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/scanning/waf_test.go b/pkg/scanning/waf_test.go new file mode 100644 index 00000000..5c5e84b2 --- /dev/null +++ b/pkg/scanning/waf_test.go @@ -0,0 +1,67 @@ +package scanning + +import ( + "net/http" + "testing" +) + +func Test_checkWAF(t *testing.T) { + type args struct { + header http.Header + body string + } + tests := []struct { + name string + args args + want bool + want1 string + }{ + { + name: "Match 360 Web Application Firewall", + args: args{ + header: http.Header{"X-Powered-By-360wzb": []string{"value"}}, + body: "some body content", + }, + want: true, + want1: "360 Web Application Firewall (360)", + }, + { + name: "Match aeSecure", + args: args{ + header: http.Header{"aeSecure-code": []string{"value"}}, + body: "aesecure_denied.png", + }, + want: true, + want1: "aeSecure", + }, + { + name: "Match CloudFlare Web Application Firewall", + args: args{ + header: http.Header{"cf-ray": []string{"value"}}, + body: "Attention Required!", + }, + want: true, + want1: "CloudFlare Web Application Firewall (CloudFlare)", + }, + { + name: "No match", + args: args{ + header: http.Header{"Some-Header": []string{"value"}}, + body: "some body content", + }, + want: false, + want1: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1 := checkWAF(tt.args.header, tt.args.body) + if got != tt.want { + t.Errorf("checkWAF() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("checkWAF() got1 = %v, want %v", got1, tt.want1) + } + }) + } +}