Path: blob/dev/pkg/protocols/http/operators_test.go
2070 views
package http12import (3"net/http"4"testing"5"time"67"github.com/stretchr/testify/require"89"github.com/projectdiscovery/nuclei/v3/pkg/model"10"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"11"github.com/projectdiscovery/nuclei/v3/pkg/operators"12"github.com/projectdiscovery/nuclei/v3/pkg/operators/extractors"13"github.com/projectdiscovery/nuclei/v3/pkg/operators/matchers"14"github.com/projectdiscovery/nuclei/v3/pkg/output"15"github.com/projectdiscovery/nuclei/v3/pkg/testutils"16)1718func TestResponseToDSLMap(t *testing.T) {19options := testutils.DefaultOptions2021testutils.Init(options)22templateID := "testing-http"23request := &Request{24ID: templateID,25Name: "testing",26Path: []string{"{{BaseURL}}?test=1"},27Method: HTTPMethodTypeHolder{MethodType: HTTPGet},28}29executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{30ID: templateID,31Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"},32})33err := request.Compile(executerOpts)34require.Nil(t, err, "could not compile file request")3536resp := &http.Response{}37resp.Header = make(http.Header)38resp.Header.Set("Test", "Test-Response")39host := "http://example.com/test/"40matched := "http://example.com/test/?test=1"4142event := request.responseToDSLMap(resp, host, matched, exampleRawRequest, exampleRawResponse, exampleResponseBody, exampleResponseHeader, 1*time.Second, map[string]interface{}{})43require.Len(t, event, 15, "could not get correct number of items in dsl map")44require.Equal(t, exampleRawResponse, event["response"], "could not get correct resp")45require.Equal(t, "Test-Response", event["test"], "could not get correct resp for header")46}4748func TestHTTPOperatorMatch(t *testing.T) {49options := testutils.DefaultOptions5051testutils.Init(options)52templateID := "testing-http"53request := &Request{54ID: templateID,55Name: "testing",56Path: []string{"{{BaseURL}}?test=1"},57Method: HTTPMethodTypeHolder{MethodType: HTTPGet},58}59executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{60ID: templateID,61Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"},62})63err := request.Compile(executerOpts)64require.Nil(t, err, "could not compile file request")6566resp := &http.Response{}67resp.Header = make(http.Header)68resp.Header.Set("Test", "Test-Response")69host := "http://example.com/test/"70matched := "http://example.com/test/?test=1"7172event := request.responseToDSLMap(resp, host, matched, exampleRawRequest, exampleRawResponse, exampleResponseBody, exampleResponseHeader, 1*time.Second, map[string]interface{}{})73require.Len(t, event, 15, "could not get correct number of items in dsl map")74require.Equal(t, exampleRawResponse, event["response"], "could not get correct resp")75require.Equal(t, "Test-Response", event["test"], "could not get correct resp for header")7677t.Run("valid", func(t *testing.T) {78matcher := &matchers.Matcher{79Part: "body",80Type: matchers.MatcherTypeHolder{MatcherType: matchers.WordsMatcher},81Words: []string{"1.1.1.1"},82}83err = matcher.CompileMatchers()84require.Nil(t, err, "could not compile matcher")8586isMatched, matched := request.Match(event, matcher)87require.True(t, isMatched, "could not match valid response")88require.Equal(t, matcher.Words, matched)89})9091t.Run("negative", func(t *testing.T) {92matcher := &matchers.Matcher{93Part: "body",94Type: matchers.MatcherTypeHolder{MatcherType: matchers.WordsMatcher},95Negative: true,96Words: []string{"random"},97}98err := matcher.CompileMatchers()99require.Nil(t, err, "could not compile negative matcher")100101isMatched, matched := request.Match(event, matcher)102require.True(t, isMatched, "could not match valid negative response matcher")103require.Equal(t, []string{}, matched)104})105106t.Run("invalid", func(t *testing.T) {107matcher := &matchers.Matcher{108Part: "body",109Type: matchers.MatcherTypeHolder{MatcherType: matchers.WordsMatcher},110Words: []string{"random"},111}112err := matcher.CompileMatchers()113require.Nil(t, err, "could not compile matcher")114115isMatched, matched := request.Match(event, matcher)116require.False(t, isMatched, "could match invalid response matcher")117require.Equal(t, []string{}, matched)118})119120t.Run("caseInsensitive", func(t *testing.T) {121matcher := &matchers.Matcher{122Part: "body",123Type: matchers.MatcherTypeHolder{MatcherType: matchers.WordsMatcher}, // only applies to word124Words: []string{"EXAMPLE DOMAIN"},125CaseInsensitive: true,126}127err = matcher.CompileMatchers()128require.Nil(t, err, "could not compile matcher")129130isMatched, matched := request.Match(event, matcher)131require.True(t, isMatched, "could not match valid response")132require.Equal(t, []string{"example domain"}, matched)133})134}135136func TestHTTPOperatorExtract(t *testing.T) {137options := testutils.DefaultOptions138139testutils.Init(options)140templateID := "testing-http"141request := &Request{142ID: templateID,143Name: "testing",144Path: []string{"{{BaseURL}}?test=1"},145Method: HTTPMethodTypeHolder{MethodType: HTTPGet},146}147executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{148ID: templateID,149Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"},150})151err := request.Compile(executerOpts)152require.Nil(t, err, "could not compile file request")153154resp := &http.Response{}155resp.Header = make(http.Header)156resp.Header.Set("Test-Header", "Test-Response")157host := "http://example.com/test/"158matched := "http://example.com/test/?test=1"159160event := request.responseToDSLMap(resp, host, matched, exampleRawRequest, exampleRawResponse, exampleResponseBody, exampleResponseHeader, 1*time.Second, map[string]interface{}{})161require.Len(t, event, 15, "could not get correct number of items in dsl map")162require.Equal(t, exampleRawResponse, event["response"], "could not get correct resp")163require.Equal(t, "Test-Response", event["test_header"], "could not get correct resp for header")164165t.Run("extract", func(t *testing.T) {166extractor := &extractors.Extractor{167Part: "body",168Type: extractors.ExtractorTypeHolder{ExtractorType: extractors.RegexExtractor},169Regex: []string{"[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+"},170}171err = extractor.CompileExtractors()172require.Nil(t, err, "could not compile extractor")173174data := request.Extract(event, extractor)175require.Greater(t, len(data), 0, "could not extractor valid response")176require.Equal(t, map[string]struct{}{"1.1.1.1": {}}, data, "could not extract correct data")177})178179t.Run("kval", func(t *testing.T) {180extractor := &extractors.Extractor{181Type: extractors.ExtractorTypeHolder{ExtractorType: extractors.KValExtractor},182KVal: []string{"test_header"},183}184err = extractor.CompileExtractors()185require.Nil(t, err, "could not compile kval extractor")186187data := request.Extract(event, extractor)188require.Greater(t, len(data), 0, "could not extractor kval valid response")189require.Equal(t, map[string]struct{}{"Test-Response": {}}, data, "could not extract correct kval data")190})191192t.Run("json", func(t *testing.T) {193event["body"] = exampleJSONResponseBody194195t.Run("jq-simple", func(t *testing.T) {196extractor := &extractors.Extractor{197Type: extractors.ExtractorTypeHolder{ExtractorType: extractors.JSONExtractor},198JSON: []string{".batters | .batter | .[] | .id"},199}200err = extractor.CompileExtractors()201require.Nil(t, err, "could not compile json extractor")202203data := request.Extract(event, extractor)204require.Greater(t, len(data), 0, "could not extractor json valid response")205require.Equal(t, map[string]struct{}{"1001": {}, "1002": {}, "1003": {}, "1004": {}}, data, "could not extract correct json data")206})207t.Run("jq-array", func(t *testing.T) {208extractor := &extractors.Extractor{209Type: extractors.ExtractorTypeHolder{ExtractorType: extractors.JSONExtractor},210JSON: []string{".array"},211}212err = extractor.CompileExtractors()213require.Nil(t, err, "could not compile json extractor")214215data := request.Extract(event, extractor)216require.Greater(t, len(data), 0, "could not extractor json valid response")217require.Equal(t, map[string]struct{}{"[\"hello\",\"world\"]": {}}, data, "could not extract correct json data")218})219t.Run("jq-object", func(t *testing.T) {220extractor := &extractors.Extractor{221Type: extractors.ExtractorTypeHolder{ExtractorType: extractors.JSONExtractor},222JSON: []string{".batters"},223}224err = extractor.CompileExtractors()225require.Nil(t, err, "could not compile json extractor")226227data := request.Extract(event, extractor)228require.Greater(t, len(data), 0, "could not extractor json valid response")229require.Equal(t, map[string]struct{}{"{\"batter\":[{\"id\":\"1001\",\"type\":\"Regular\"},{\"id\":\"1002\",\"type\":\"Chocolate\"},{\"id\":\"1003\",\"type\":\"Blueberry\"},{\"id\":\"1004\",\"type\":\"Devil's Food\"}]}": {}}, data, "could not extract correct json data")230})231})232233t.Run("caseInsensitive", func(t *testing.T) {234event["body"] = exampleResponseBody235236extractor := &extractors.Extractor{237Type: extractors.ExtractorTypeHolder{ExtractorType: extractors.KValExtractor},238KVal: []string{"TEST_HEADER"}, // only applies to KVal239CaseInsensitive: true,240}241err = extractor.CompileExtractors()242require.Nil(t, err, "could not compile kval extractor")243244data := request.Extract(event, extractor)245require.Greater(t, len(data), 0, "could not extractor kval valid response")246require.Equal(t, map[string]struct{}{"test-response": {}}, data, "could not extract correct kval data")247})248}249250func TestHTTPMakeResult(t *testing.T) {251options := testutils.DefaultOptions252253testutils.Init(options)254templateID := "testing-http"255request := &Request{256ID: templateID,257Name: "testing",258Path: []string{"{{BaseURL}}?test=1"},259Method: HTTPMethodTypeHolder{MethodType: HTTPGet},260Operators: operators.Operators{261Matchers: []*matchers.Matcher{{262Name: "test",263Part: "body",264Type: matchers.MatcherTypeHolder{MatcherType: matchers.WordsMatcher},265Words: []string{"1.1.1.1"},266}},267Extractors: []*extractors.Extractor{{268Part: "body",269Type: extractors.ExtractorTypeHolder{ExtractorType: extractors.RegexExtractor},270Regex: []string{"[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+"},271}},272},273}274executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{275ID: templateID,276Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"},277})278err := request.Compile(executerOpts)279require.Nil(t, err, "could not compile file request")280281resp := &http.Response{}282resp.Header = make(http.Header)283resp.Header.Set("Test", "Test-Response")284host := "http://example.com/test/"285matched := "http://example.com/test/?test=1"286287event := request.responseToDSLMap(resp, host, matched, exampleRawRequest, exampleRawResponse, exampleResponseBody, exampleResponseHeader, 1*time.Second, map[string]interface{}{})288require.Len(t, event, 15, "could not get correct number of items in dsl map")289require.Equal(t, exampleRawResponse, event["response"], "could not get correct resp")290require.Equal(t, "Test-Response", event["test"], "could not get correct resp for header")291292event["ip"] = "192.169.1.1"293finalEvent := &output.InternalWrappedEvent{InternalEvent: event}294if request.CompiledOperators != nil {295result, ok := request.CompiledOperators.Execute(event, request.Match, request.Extract, false)296if ok && result != nil {297finalEvent.OperatorsResult = result298finalEvent.Results = request.MakeResultEvent(finalEvent)299}300}301require.Equal(t, 1, len(finalEvent.Results), "could not get correct number of results")302require.Equal(t, "test", finalEvent.Results[0].MatcherName, "could not get correct matcher name of results")303require.Equal(t, "1.1.1.1", finalEvent.Results[0].ExtractedResults[0], "could not get correct extracted results")304}305306const exampleRawRequest = `GET / HTTP/1.1307Host: example.com308Upgrade-Insecure-Requests: 1309User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36310Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9311Accept-Encoding: gzip, deflate312Accept-Language: en-US,en;q=0.9,hi;q=0.8313If-None-Match: "3147526947+gzip"314If-Modified-Since: Thu, 17 Oct 2019 07:18:26 GMT315Connection: close316317`318319const exampleRawResponse = exampleResponseHeader + exampleResponseBody320const exampleResponseHeader = `321HTTP/1.1 200 OK322Accept-Ranges: bytes323Age: 493322324Cache-Control: max-age=604800325Content-Type: text/html; charset=UTF-8326Date: Thu, 04 Feb 2021 12:15:51 GMT327Etag: "3147526947+ident"328Expires: Thu, 11 Feb 2021 12:15:51 GMT329Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT330Server: ECS (nyb/1D1C)331Vary: Accept-Encoding332X-Cache: HIT333Content-Length: 1256334Connection: close335`336337const exampleResponseBody = `338<!doctype html>339<html>340<head>341<title>Example Domain</title>342343<meta charset="utf-8" />344<meta http-equiv="Content-type" content="text/html; charset=utf-8" />345<meta name="viewport" content="width=device-width, initial-scale=1" />346<style type="text/css">347body {348background-color: #f0f0f2;349margin: 0;350padding: 0;351font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;352353}354div {355width: 600px;356margin: 5em auto;357padding: 2em;358background-color: #fdfdff;359border-radius: 0.5em;360box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);361}362a:link, a:visited {363color: #38488f;364text-decoration: none;365}366@media (max-width: 700px) {367div {368margin: 0 auto;369width: auto;370}371}372</style>373</head>374<a>1.1.1.1</a>375<body>376<div>377<h1>Example Domain</h1>378<p>This domain is for use in illustrative examples in documents. You may use this379domain in literature without prior coordination or asking for permission.</p>380<p><a href="https://www.iana.org/domains/example">More information...</a></p>381</div>382</body>383</html>384`385386const exampleJSONResponseBody = `387{388"id": "0001",389"type": "donut",390"name": "Cake",391"ppu": 0.55,392"array": ["hello", "world"],393"batters": {394"batter": [395{396"id": "1001",397"type": "Regular"398},399{400"id": "1002",401"type": "Chocolate"402},403{404"id": "1003",405"type": "Blueberry"406},407{408"id": "1004",409"type": "Devil's Food"410}411]412},413"topping": [414{415"id": "5001",416"type": "None"417},418{419"id": "5002",420"type": "Glazed"421},422{423"id": "5005",424"type": "Sugar"425},426{427"id": "5007",428"type": "Powdered Sugar"429},430{431"id": "5006",432"type": "Chocolate with Sprinkles"433},434{435"id": "5003",436"type": "Chocolate"437},438{439"id": "5004",440"type": "Maple"441}442]443}444`445446447