feat(headless): add ActionWaitDialog type (#5545)

* feat(headless): add `dialog` action type

also implement it

Signed-off-by: Dwi Siswanto <git@dw1.io>

* refactor(headless): add `ActionData` for action output datas

Signed-off-by: Dwi Siswanto <git@dw1.io>

* refactor(headless): rm `value` arg for `*Page.HandleDialog`

also:
* expose `err` from \*proto.PageHandleJavaScriptDialog`
* conditional ActionData assignment based on

Signed-off-by: Dwi Siswanto <git@dw1.io>

* refactor(headless): rename to `ActionWaitDialog`

Signed-off-by: Dwi Siswanto <git@dw1.io>

* test(headless): fix mismatch assertion of `src` output of `ActionGetResource`

Signed-off-by: Dwi Siswanto <git@dw1.io>

* test(headless): add TestActionWaitDialog test case

Signed-off-by: Dwi Siswanto <git@dw1.io>

* feat(headless): add `GetActionDataWithDefault` generic func

Signed-off-by: Dwi Siswanto <git@dw1.io>

* feat(headless): implement `GetActionDataWithDefault`

to `header` & `status_code`

Signed-off-by: Dwi Siswanto <git@dw1.io>

* refactor(headless): use `mapsutil.Map` instead

Signed-off-by: Dwi Siswanto <git@dw1.io>

* Revert "feat(headless): add `GetActionDataWithDefault` generic func"

This reverts commit fa12e0d6a221c8a7bf62200f69814ee27681f08f.

---------

Signed-off-by: Dwi Siswanto <git@dw1.io>
This commit is contained in:
Dwi Siswanto 2024-09-02 16:59:52 +07:00 committed by GitHub
parent 9a5272985c
commit 841d8913e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 171 additions and 53 deletions

View File

@ -6,11 +6,15 @@ import (
"strings" "strings"
"github.com/invopop/jsonschema" "github.com/invopop/jsonschema"
mapsutil "github.com/projectdiscovery/utils/maps"
) )
// ActionType defines the action type for a browser action // ActionType defines the action type for a browser action
type ActionType int8 type ActionType int8
// ActionData stores the action output data
type ActionData = mapsutil.Map[string, any]
// Types to be executed by the user. // Types to be executed by the user.
// name:ActionType // name:ActionType
const ( const (
@ -68,6 +72,9 @@ const (
// ActionWaitEvent waits for a specific event. // ActionWaitEvent waits for a specific event.
// name:waitevent // name:waitevent
ActionWaitEvent ActionWaitEvent
// ActionWaitDialog waits for JavaScript dialog (alert, confirm, prompt, or onbeforeunload).
// name:dialog
ActionWaitDialog
// ActionKeyboard performs a keyboard action event on a page. // ActionKeyboard performs a keyboard action event on a page.
// name:keyboard // name:keyboard
ActionKeyboard ActionKeyboard
@ -104,6 +111,7 @@ var ActionStringToAction = map[string]ActionType{
"deleteheader": ActionDeleteHeader, "deleteheader": ActionDeleteHeader,
"setbody": ActionSetBody, "setbody": ActionSetBody,
"waitevent": ActionWaitEvent, "waitevent": ActionWaitEvent,
"waitdialog": ActionWaitDialog,
"keyboard": ActionKeyboard, "keyboard": ActionKeyboard,
"debug": ActionDebug, "debug": ActionDebug,
"sleep": ActionSleep, "sleep": ActionSleep,
@ -130,6 +138,7 @@ var ActionToActionString = map[ActionType]string{
ActionDeleteHeader: "deleteheader", ActionDeleteHeader: "deleteheader",
ActionSetBody: "setbody", ActionSetBody: "setbody",
ActionWaitEvent: "waitevent", ActionWaitEvent: "waitevent",
ActionWaitDialog: "waitdialog",
ActionKeyboard: "keyboard", ActionKeyboard: "keyboard",
ActionDebug: "debug", ActionDebug: "debug",
ActionSleep: "sleep", ActionSleep: "sleep",

View File

@ -45,7 +45,7 @@ type Options struct {
} }
// Run runs a list of actions by creating a new page in the browser. // Run runs a list of actions by creating a new page in the browser.
func (i *Instance) Run(input *contextargs.Context, actions []*Action, payloads map[string]interface{}, options *Options) (map[string]string, *Page, error) { func (i *Instance) Run(input *contextargs.Context, actions []*Action, payloads map[string]interface{}, options *Options) (ActionData, *Page, error) {
page, err := i.engine.Page(proto.TargetCreateTarget{}) page, err := i.engine.Page(proto.TargetCreateTarget{})
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err

View File

@ -48,8 +48,8 @@ const (
) )
// ExecuteActions executes a list of actions on a page. // ExecuteActions executes a list of actions on a page.
func (p *Page) ExecuteActions(input *contextargs.Context, actions []*Action, variables map[string]interface{}) (outData map[string]string, err error) { func (p *Page) ExecuteActions(input *contextargs.Context, actions []*Action, variables map[string]interface{}) (outData ActionData, err error) {
outData = make(map[string]string) outData = make(ActionData)
// waitFuncs are function that needs to be executed after navigation // waitFuncs are function that needs to be executed after navigation
// typically used for waitEvent // typically used for waitEvent
waitFuncs := make([]func() error, 0) waitFuncs := make([]func() error, 0)
@ -106,6 +106,8 @@ func (p *Page) ExecuteActions(input *contextargs.Context, actions []*Action, var
if waitFunc != nil { if waitFunc != nil {
waitFuncs = append(waitFuncs, waitFunc) waitFuncs = append(waitFuncs, waitFunc)
} }
case ActionWaitDialog:
err = p.HandleDialog(act, outData)
case ActionFilesInput: case ActionFilesInput:
if p.options.Options.AllowLocalFileAccess { if p.options.Options.AllowLocalFileAccess {
err = p.FilesInput(act, outData) err = p.FilesInput(act, outData)
@ -148,7 +150,7 @@ type rule struct {
} }
// WaitVisible waits until an element appears. // WaitVisible waits until an element appears.
func (p *Page) WaitVisible(act *Action, out map[string]string) error { func (p *Page) WaitVisible(act *Action, out ActionData) error {
timeout, err := getTimeout(p, act) timeout, err := getTimeout(p, act)
if err != nil { if err != nil {
return errors.Wrap(err, "Wrong timeout given") return errors.Wrap(err, "Wrong timeout given")
@ -223,7 +225,7 @@ func geTimeParameter(p *Page, act *Action, parameterName string, defaultValue ti
} }
// ActionAddHeader executes a AddHeader action. // ActionAddHeader executes a AddHeader action.
func (p *Page) ActionAddHeader(act *Action, out map[string]string) error { func (p *Page) ActionAddHeader(act *Action, out ActionData) error {
in := p.getActionArgWithDefaultValues(act, "part") in := p.getActionArgWithDefaultValues(act, "part")
args := make(map[string]string) args := make(map[string]string)
@ -234,7 +236,7 @@ func (p *Page) ActionAddHeader(act *Action, out map[string]string) error {
} }
// ActionSetHeader executes a SetHeader action. // ActionSetHeader executes a SetHeader action.
func (p *Page) ActionSetHeader(act *Action, out map[string]string) error { func (p *Page) ActionSetHeader(act *Action, out ActionData) error {
in := p.getActionArgWithDefaultValues(act, "part") in := p.getActionArgWithDefaultValues(act, "part")
args := make(map[string]string) args := make(map[string]string)
@ -245,7 +247,7 @@ func (p *Page) ActionSetHeader(act *Action, out map[string]string) error {
} }
// ActionDeleteHeader executes a DeleteHeader action. // ActionDeleteHeader executes a DeleteHeader action.
func (p *Page) ActionDeleteHeader(act *Action, out map[string]string) error { func (p *Page) ActionDeleteHeader(act *Action, out ActionData) error {
in := p.getActionArgWithDefaultValues(act, "part") in := p.getActionArgWithDefaultValues(act, "part")
args := make(map[string]string) args := make(map[string]string)
@ -255,7 +257,7 @@ func (p *Page) ActionDeleteHeader(act *Action, out map[string]string) error {
} }
// ActionSetBody executes a SetBody action. // ActionSetBody executes a SetBody action.
func (p *Page) ActionSetBody(act *Action, out map[string]string) error { func (p *Page) ActionSetBody(act *Action, out ActionData) error {
in := p.getActionArgWithDefaultValues(act, "part") in := p.getActionArgWithDefaultValues(act, "part")
args := make(map[string]string) args := make(map[string]string)
@ -265,7 +267,7 @@ func (p *Page) ActionSetBody(act *Action, out map[string]string) error {
} }
// ActionSetMethod executes an SetMethod action. // ActionSetMethod executes an SetMethod action.
func (p *Page) ActionSetMethod(act *Action, out map[string]string) error { func (p *Page) ActionSetMethod(act *Action, out ActionData) error {
in := p.getActionArgWithDefaultValues(act, "part") in := p.getActionArgWithDefaultValues(act, "part")
args := make(map[string]string) args := make(map[string]string)
@ -275,7 +277,7 @@ func (p *Page) ActionSetMethod(act *Action, out map[string]string) error {
} }
// NavigateURL executes an ActionLoadURL actions loading a URL for the page. // NavigateURL executes an ActionLoadURL actions loading a URL for the page.
func (p *Page) NavigateURL(action *Action, out map[string]string, allvars map[string]interface{}) error { func (p *Page) NavigateURL(action *Action, out ActionData, allvars map[string]interface{}) error {
// input <- is input url from cli // input <- is input url from cli
// target <- is the url from template (ex: {{BaseURL}}/test) // target <- is the url from template (ex: {{BaseURL}}/test)
input, err := urlutil.Parse(p.input.MetaInput.Input) input, err := urlutil.Parse(p.input.MetaInput.Input)
@ -331,7 +333,7 @@ func (p *Page) NavigateURL(action *Action, out map[string]string, allvars map[st
} }
// RunScript runs a script on the loaded page // RunScript runs a script on the loaded page
func (p *Page) RunScript(action *Action, out map[string]string) error { func (p *Page) RunScript(action *Action, out ActionData) error {
code := p.getActionArgWithDefaultValues(action, "code") code := p.getActionArgWithDefaultValues(action, "code")
if code == "" { if code == "" {
return errinvalidArguments return errinvalidArguments
@ -352,7 +354,7 @@ func (p *Page) RunScript(action *Action, out map[string]string) error {
} }
// ClickElement executes click actions for an element. // ClickElement executes click actions for an element.
func (p *Page) ClickElement(act *Action, out map[string]string) error { func (p *Page) ClickElement(act *Action, out ActionData) error {
element, err := p.pageElementBy(act.Data) element, err := p.pageElementBy(act.Data)
if err != nil { if err != nil {
return errors.Wrap(err, errCouldNotGetElement) return errors.Wrap(err, errCouldNotGetElement)
@ -367,12 +369,12 @@ func (p *Page) ClickElement(act *Action, out map[string]string) error {
} }
// KeyboardAction executes a keyboard action on the page. // KeyboardAction executes a keyboard action on the page.
func (p *Page) KeyboardAction(act *Action, out map[string]string) error { func (p *Page) KeyboardAction(act *Action, out ActionData) error {
return p.page.Keyboard.Type([]input.Key(p.getActionArgWithDefaultValues(act, "keys"))...) return p.page.Keyboard.Type([]input.Key(p.getActionArgWithDefaultValues(act, "keys"))...)
} }
// RightClickElement executes right click actions for an element. // RightClickElement executes right click actions for an element.
func (p *Page) RightClickElement(act *Action, out map[string]string) error { func (p *Page) RightClickElement(act *Action, out ActionData) error {
element, err := p.pageElementBy(act.Data) element, err := p.pageElementBy(act.Data)
if err != nil { if err != nil {
return errors.Wrap(err, errCouldNotGetElement) return errors.Wrap(err, errCouldNotGetElement)
@ -387,7 +389,7 @@ func (p *Page) RightClickElement(act *Action, out map[string]string) error {
} }
// Screenshot executes screenshot action on a page // Screenshot executes screenshot action on a page
func (p *Page) Screenshot(act *Action, out map[string]string) error { func (p *Page) Screenshot(act *Action, out ActionData) error {
to := p.getActionArgWithDefaultValues(act, "to") to := p.getActionArgWithDefaultValues(act, "to")
if to == "" { if to == "" {
to = ksuid.New().String() to = ksuid.New().String()
@ -450,7 +452,7 @@ func (p *Page) Screenshot(act *Action, out map[string]string) error {
} }
// InputElement executes input element actions for an element. // InputElement executes input element actions for an element.
func (p *Page) InputElement(act *Action, out map[string]string) error { func (p *Page) InputElement(act *Action, out ActionData) error {
value := p.getActionArgWithDefaultValues(act, "value") value := p.getActionArgWithDefaultValues(act, "value")
if value == "" { if value == "" {
return errinvalidArguments return errinvalidArguments
@ -469,7 +471,7 @@ func (p *Page) InputElement(act *Action, out map[string]string) error {
} }
// TimeInputElement executes time input on an element // TimeInputElement executes time input on an element
func (p *Page) TimeInputElement(act *Action, out map[string]string) error { func (p *Page) TimeInputElement(act *Action, out ActionData) error {
value := p.getActionArgWithDefaultValues(act, "value") value := p.getActionArgWithDefaultValues(act, "value")
if value == "" { if value == "" {
return errinvalidArguments return errinvalidArguments
@ -492,7 +494,7 @@ func (p *Page) TimeInputElement(act *Action, out map[string]string) error {
} }
// SelectInputElement executes select input statement action on a element // SelectInputElement executes select input statement action on a element
func (p *Page) SelectInputElement(act *Action, out map[string]string) error { func (p *Page) SelectInputElement(act *Action, out ActionData) error {
value := p.getActionArgWithDefaultValues(act, "value") value := p.getActionArgWithDefaultValues(act, "value")
if value == "" { if value == "" {
return errinvalidArguments return errinvalidArguments
@ -517,7 +519,7 @@ func (p *Page) SelectInputElement(act *Action, out map[string]string) error {
} }
// WaitLoad waits for the page to load // WaitLoad waits for the page to load
func (p *Page) WaitLoad(act *Action, out map[string]string) error { func (p *Page) WaitLoad(act *Action, out ActionData) error {
p.page.Timeout(2 * time.Second).WaitNavigation(proto.PageLifecycleEventNameFirstMeaningfulPaint)() p.page.Timeout(2 * time.Second).WaitNavigation(proto.PageLifecycleEventNameFirstMeaningfulPaint)()
// Wait for the window.onload event and also wait for the network requests // Wait for the window.onload event and also wait for the network requests
@ -531,7 +533,7 @@ func (p *Page) WaitLoad(act *Action, out map[string]string) error {
} }
// GetResource gets a resource from an element from page. // GetResource gets a resource from an element from page.
func (p *Page) GetResource(act *Action, out map[string]string) error { func (p *Page) GetResource(act *Action, out ActionData) error {
element, err := p.pageElementBy(act.Data) element, err := p.pageElementBy(act.Data)
if err != nil { if err != nil {
return errors.Wrap(err, errCouldNotGetElement) return errors.Wrap(err, errCouldNotGetElement)
@ -547,7 +549,7 @@ func (p *Page) GetResource(act *Action, out map[string]string) error {
} }
// FilesInput acts with a file input element on page // FilesInput acts with a file input element on page
func (p *Page) FilesInput(act *Action, out map[string]string) error { func (p *Page) FilesInput(act *Action, out ActionData) error {
element, err := p.pageElementBy(act.Data) element, err := p.pageElementBy(act.Data)
if err != nil { if err != nil {
return errors.Wrap(err, errCouldNotGetElement) return errors.Wrap(err, errCouldNotGetElement)
@ -564,7 +566,7 @@ func (p *Page) FilesInput(act *Action, out map[string]string) error {
} }
// ExtractElement extracts from an element on the page. // ExtractElement extracts from an element on the page.
func (p *Page) ExtractElement(act *Action, out map[string]string) error { func (p *Page) ExtractElement(act *Action, out ActionData) error {
element, err := p.pageElementBy(act.Data) element, err := p.pageElementBy(act.Data)
if err != nil { if err != nil {
return errors.Wrap(err, errCouldNotGetElement) return errors.Wrap(err, errCouldNotGetElement)
@ -598,7 +600,7 @@ func (p *Page) ExtractElement(act *Action, out map[string]string) error {
} }
// WaitEvent waits for an event to happen on the page. // WaitEvent waits for an event to happen on the page.
func (p *Page) WaitEvent(act *Action, out map[string]string) (func() error, error) { func (p *Page) WaitEvent(act *Action, out ActionData) (func() error, error) {
event := p.getActionArgWithDefaultValues(act, "event") event := p.getActionArgWithDefaultValues(act, "event")
if event == "" { if event == "" {
return nil, errors.New("event not recognized") return nil, errors.New("event not recognized")
@ -636,6 +638,43 @@ func (p *Page) WaitEvent(act *Action, out map[string]string) (func() error, erro
return waitFunc, nil return waitFunc, nil
} }
// HandleDialog handles JavaScript dialog (alert, confirm, prompt, or onbeforeunload).
func (p *Page) HandleDialog(act *Action, out ActionData) error {
maxDuration := 10 * time.Second
if dur := p.getActionArgWithDefaultValues(act, "max-duration"); dur != "" {
var err error
maxDuration, err = time.ParseDuration(dur)
if err != nil {
return errorutil.NewWithErr(err).Msgf("could not parse max-duration")
}
}
ctx, cancel := context.WithTimeout(context.Background(), maxDuration)
defer cancel()
wait, handle := p.page.HandleDialog()
fn := func() (*proto.PageJavascriptDialogOpening, error) {
dialog := wait()
err := handle(&proto.PageHandleJavaScriptDialog{
Accept: true,
PromptText: "",
})
return dialog, err
}
dialog, err := contextutil.ExecFuncWithTwoReturns(ctx, fn)
if err == nil && act.Name != "" {
out[act.Name] = true
out[act.Name+"_type"] = string(dialog.Type)
out[act.Name+"_message"] = dialog.Message
}
return nil
}
// pageElementBy returns a page element from a variety of inputs. // pageElementBy returns a page element from a variety of inputs.
// //
// Supported values for by: r -> selector & regex, x -> xpath, js -> eval js, // Supported values for by: r -> selector & regex, x -> xpath, js -> eval js,
@ -670,14 +709,14 @@ func (p *Page) pageElementBy(data map[string]string) (*rod.Element, error) {
} }
// DebugAction enables debug action on a page. // DebugAction enables debug action on a page.
func (p *Page) DebugAction(act *Action, out map[string]string) error { func (p *Page) DebugAction(act *Action, out ActionData) error {
p.instance.browser.engine.SlowMotion(5 * time.Second) p.instance.browser.engine.SlowMotion(5 * time.Second)
p.instance.browser.engine.Trace(true) p.instance.browser.engine.Trace(true)
return nil return nil
} }
// SleepAction sleeps on the page for a specified duration // SleepAction sleeps on the page for a specified duration
func (p *Page) SleepAction(act *Action, out map[string]string) error { func (p *Page) SleepAction(act *Action, out ActionData) error {
seconds := act.Data["duration"] seconds := act.Data["duration"]
if seconds == "" { if seconds == "" {
seconds = "5" seconds = "5"

View File

@ -38,7 +38,7 @@ func TestActionNavigate(t *testing.T) {
actions := []*Action{{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}}, {ActionType: ActionTypeHolder{ActionType: ActionWaitLoad}}} actions := []*Action{{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}}, {ActionType: ActionTypeHolder{ActionType: ActionWaitLoad}}}
testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) { testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out ActionData) {
require.Nilf(t, err, "could not run page actions") require.Nilf(t, err, "could not run page actions")
require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly") require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly")
}) })
@ -63,7 +63,7 @@ func TestActionScript(t *testing.T) {
{ActionType: ActionTypeHolder{ActionType: ActionScript}, Name: "test", Data: map[string]string{"code": "() => window.test"}}, {ActionType: ActionTypeHolder{ActionType: ActionScript}, Name: "test", Data: map[string]string{"code": "() => window.test"}},
} }
testHeadlessSimpleResponse(t, response, actions, timeout, func(page *Page, err error, out map[string]string) { testHeadlessSimpleResponse(t, response, actions, timeout, func(page *Page, err error, out ActionData) {
require.Nil(t, err, "could not run page actions") require.Nil(t, err, "could not run page actions")
require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly") require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly")
require.Equal(t, "some-data", out["test"], "could not run js and get results correctly") require.Equal(t, "some-data", out["test"], "could not run js and get results correctly")
@ -77,7 +77,7 @@ func TestActionScript(t *testing.T) {
{ActionType: ActionTypeHolder{ActionType: ActionWaitLoad}}, {ActionType: ActionTypeHolder{ActionType: ActionWaitLoad}},
{ActionType: ActionTypeHolder{ActionType: ActionScript}, Name: "test", Data: map[string]string{"code": "() => window.test"}}, {ActionType: ActionTypeHolder{ActionType: ActionScript}, Name: "test", Data: map[string]string{"code": "() => window.test"}},
} }
testHeadlessSimpleResponse(t, response, actions, timeout, func(page *Page, err error, out map[string]string) { testHeadlessSimpleResponse(t, response, actions, timeout, func(page *Page, err error, out ActionData) {
require.Nil(t, err, "could not run page actions") require.Nil(t, err, "could not run page actions")
require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly") require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly")
require.Equal(t, "some-data", out["test"], "could not run js and get results correctly with js hook") require.Equal(t, "some-data", out["test"], "could not run js and get results correctly with js hook")
@ -101,7 +101,7 @@ func TestActionClick(t *testing.T) {
{ActionType: ActionTypeHolder{ActionType: ActionClick}, Data: map[string]string{"selector": "button"}}, // Use css selector for clicking {ActionType: ActionTypeHolder{ActionType: ActionClick}, Data: map[string]string{"selector": "button"}}, // Use css selector for clicking
} }
testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) { testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out ActionData) {
require.Nil(t, err, "could not run page actions") require.Nil(t, err, "could not run page actions")
require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly") require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly")
el := page.Page().MustElement("button") el := page.Page().MustElement("button")
@ -134,7 +134,7 @@ func TestActionRightClick(t *testing.T) {
{ActionType: ActionTypeHolder{ActionType: ActionRightClick}, Data: map[string]string{"selector": "button"}}, // Use css selector for clicking {ActionType: ActionTypeHolder{ActionType: ActionRightClick}, Data: map[string]string{"selector": "button"}}, // Use css selector for clicking
} }
testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) { testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out ActionData) {
require.Nil(t, err, "could not run page actions") require.Nil(t, err, "could not run page actions")
require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly") require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly")
el := page.Page().MustElement("button") el := page.Page().MustElement("button")
@ -159,7 +159,7 @@ func TestActionTextInput(t *testing.T) {
{ActionType: ActionTypeHolder{ActionType: ActionTextInput}, Data: map[string]string{"selector": "input", "value": "test"}}, {ActionType: ActionTypeHolder{ActionType: ActionTextInput}, Data: map[string]string{"selector": "input", "value": "test"}},
} }
testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) { testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out ActionData) {
require.Nil(t, err, "could not run page actions") require.Nil(t, err, "could not run page actions")
require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly") require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly")
el := page.Page().MustElement("input") el := page.Page().MustElement("input")
@ -182,7 +182,7 @@ func TestActionHeadersChange(t *testing.T) {
} }
} }
testHeadless(t, actions, 20*time.Second, handler, func(page *Page, err error, out map[string]string) { testHeadless(t, actions, 20*time.Second, handler, func(page *Page, err error, out ActionData) {
require.Nil(t, err, "could not run page actions") require.Nil(t, err, "could not run page actions")
require.Equal(t, "found", strings.ToLower(strings.TrimSpace(page.Page().MustElement("html").MustText())), "could not set header correctly") require.Equal(t, "found", strings.ToLower(strings.TrimSpace(page.Page().MustElement("html").MustText())), "could not set header correctly")
}) })
@ -205,7 +205,7 @@ func TestActionScreenshot(t *testing.T) {
{ActionType: ActionTypeHolder{ActionType: ActionScreenshot}, Data: map[string]string{"to": filePath}}, {ActionType: ActionTypeHolder{ActionType: ActionScreenshot}, Data: map[string]string{"to": filePath}},
} }
testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) { testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out ActionData) {
require.Nil(t, err, "could not run page actions") require.Nil(t, err, "could not run page actions")
require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly") require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly")
_ = page.Page() _ = page.Page()
@ -233,7 +233,7 @@ func TestActionScreenshotToDir(t *testing.T) {
{ActionType: ActionTypeHolder{ActionType: ActionScreenshot}, Data: map[string]string{"to": filePath, "mkdir": "true"}}, {ActionType: ActionTypeHolder{ActionType: ActionScreenshot}, Data: map[string]string{"to": filePath, "mkdir": "true"}},
} }
testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) { testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out ActionData) {
require.Nil(t, err, "could not run page actions") require.Nil(t, err, "could not run page actions")
require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly") require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly")
_ = page.Page() _ = page.Page()
@ -260,7 +260,7 @@ func TestActionTimeInput(t *testing.T) {
{ActionType: ActionTypeHolder{ActionType: ActionTimeInput}, Data: map[string]string{"selector": "input", "value": "2006-01-02T15:04:05Z"}}, {ActionType: ActionTypeHolder{ActionType: ActionTimeInput}, Data: map[string]string{"selector": "input", "value": "2006-01-02T15:04:05Z"}},
} }
testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) { testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out ActionData) {
require.Nil(t, err, "could not run page actions") require.Nil(t, err, "could not run page actions")
require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly") require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly")
el := page.Page().MustElement("input") el := page.Page().MustElement("input")
@ -288,7 +288,7 @@ func TestActionSelectInput(t *testing.T) {
{ActionType: ActionTypeHolder{ActionType: ActionSelectInput}, Data: map[string]string{"by": "x", "xpath": "//select[@id='test']", "value": "Test2", "selected": "true"}}, {ActionType: ActionTypeHolder{ActionType: ActionSelectInput}, Data: map[string]string{"by": "x", "xpath": "//select[@id='test']", "value": "Test2", "selected": "true"}},
} }
testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) { testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out ActionData) {
require.Nil(t, err, "could not run page actions") require.Nil(t, err, "could not run page actions")
el := page.Page().MustElement("select") el := page.Page().MustElement("select")
require.Equal(t, "Test2", el.MustText(), "could not get input change value") require.Equal(t, "Test2", el.MustText(), "could not get input change value")
@ -311,7 +311,7 @@ func TestActionFilesInput(t *testing.T) {
{ActionType: ActionTypeHolder{ActionType: ActionFilesInput}, Data: map[string]string{"selector": "input", "value": "test1.pdf"}}, {ActionType: ActionTypeHolder{ActionType: ActionFilesInput}, Data: map[string]string{"selector": "input", "value": "test1.pdf"}},
} }
testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) { testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out ActionData) {
require.Nil(t, err, "could not run page actions") require.Nil(t, err, "could not run page actions")
require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly") require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly")
el := page.Page().MustElement("input") el := page.Page().MustElement("input")
@ -337,7 +337,7 @@ func TestActionFilesInputNegative(t *testing.T) {
} }
t.Setenv("LOCAL_FILE_ACCESS", "false") t.Setenv("LOCAL_FILE_ACCESS", "false")
testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) { testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out ActionData) {
require.ErrorContains(t, err, ErrLFAccessDenied.Error(), "got file access when -lfa is false") require.ErrorContains(t, err, ErrLFAccessDenied.Error(), "got file access when -lfa is false")
}) })
} }
@ -359,7 +359,7 @@ func TestActionWaitLoad(t *testing.T) {
{ActionType: ActionTypeHolder{ActionType: ActionWaitLoad}}, {ActionType: ActionTypeHolder{ActionType: ActionWaitLoad}},
} }
testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) { testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out ActionData) {
require.Nil(t, err, "could not run page actions") require.Nil(t, err, "could not run page actions")
el := page.Page().MustElement("button") el := page.Page().MustElement("button")
style, attributeErr := el.Attribute("style") style, attributeErr := el.Attribute("style")
@ -384,9 +384,12 @@ func TestActionGetResource(t *testing.T) {
{ActionType: ActionTypeHolder{ActionType: ActionGetResource}, Data: map[string]string{"by": "x", "xpath": "//img[@id='test']"}, Name: "src"}, {ActionType: ActionTypeHolder{ActionType: ActionGetResource}, Data: map[string]string{"by": "x", "xpath": "//img[@id='test']"}, Name: "src"},
} }
testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) { testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out ActionData) {
require.Nil(t, err, "could not run page actions") require.Nil(t, err, "could not run page actions")
require.Equal(t, len(out["src"]), 121808, "could not find resource")
src, ok := out["src"].(string)
require.True(t, ok, "could not assert src to string")
require.Equal(t, len(src), 121808, "could not find resource")
}) })
} }
@ -404,7 +407,7 @@ func TestActionExtract(t *testing.T) {
{ActionType: ActionTypeHolder{ActionType: ActionExtract}, Data: map[string]string{"by": "x", "xpath": "//button[@id='test']"}, Name: "extract"}, {ActionType: ActionTypeHolder{ActionType: ActionExtract}, Data: map[string]string{"by": "x", "xpath": "//button[@id='test']"}, Name: "extract"},
} }
testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) { testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out ActionData) {
require.Nil(t, err, "could not run page actions") require.Nil(t, err, "could not run page actions")
require.Equal(t, "Wait for me!", out["extract"], "could not extract text") require.Equal(t, "Wait for me!", out["extract"], "could not extract text")
}) })
@ -423,7 +426,7 @@ func TestActionSetMethod(t *testing.T) {
{ActionType: ActionTypeHolder{ActionType: ActionSetMethod}, Data: map[string]string{"part": "x", "method": "SET"}}, {ActionType: ActionTypeHolder{ActionType: ActionSetMethod}, Data: map[string]string{"part": "x", "method": "SET"}},
} }
testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) { testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out ActionData) {
require.Nil(t, err, "could not run page actions") require.Nil(t, err, "could not run page actions")
require.Equal(t, "SET", page.rules[0].Args["method"], "could not find resource") require.Equal(t, "SET", page.rules[0].Args["method"], "could not find resource")
}) })
@ -442,7 +445,7 @@ func TestActionAddHeader(t *testing.T) {
} }
} }
testHeadless(t, actions, 20*time.Second, handler, func(page *Page, err error, out map[string]string) { testHeadless(t, actions, 20*time.Second, handler, func(page *Page, err error, out ActionData) {
require.Nil(t, err, "could not run page actions") require.Nil(t, err, "could not run page actions")
require.Equal(t, "found", strings.ToLower(strings.TrimSpace(page.Page().MustElement("html").MustText())), "could not set header correctly") require.Equal(t, "found", strings.ToLower(strings.TrimSpace(page.Page().MustElement("html").MustText())), "could not set header correctly")
}) })
@ -463,7 +466,7 @@ func TestActionDeleteHeader(t *testing.T) {
} }
} }
testHeadless(t, actions, 20*time.Second, handler, func(page *Page, err error, out map[string]string) { testHeadless(t, actions, 20*time.Second, handler, func(page *Page, err error, out ActionData) {
require.Nil(t, err, "could not run page actions") require.Nil(t, err, "could not run page actions")
require.Equal(t, "header deleted", strings.ToLower(strings.TrimSpace(page.Page().MustElement("html").MustText())), "could not delete header correctly") require.Equal(t, "header deleted", strings.ToLower(strings.TrimSpace(page.Page().MustElement("html").MustText())), "could not delete header correctly")
}) })
@ -481,7 +484,7 @@ func TestActionSetBody(t *testing.T) {
_, _ = fmt.Fprintln(w, string(body)) _, _ = fmt.Fprintln(w, string(body))
} }
testHeadless(t, actions, 20*time.Second, handler, func(page *Page, err error, out map[string]string) { testHeadless(t, actions, 20*time.Second, handler, func(page *Page, err error, out ActionData) {
require.Nil(t, err, "could not run page actions") require.Nil(t, err, "could not run page actions")
require.Equal(t, "hello", strings.ToLower(strings.TrimSpace(page.Page().MustElement("html").MustText())), "could not set header correctly") require.Equal(t, "hello", strings.ToLower(strings.TrimSpace(page.Page().MustElement("html").MustText())), "could not set header correctly")
}) })
@ -505,7 +508,7 @@ func TestActionKeyboard(t *testing.T) {
{ActionType: ActionTypeHolder{ActionType: ActionKeyboard}, Data: map[string]string{"keys": "Test2"}}, {ActionType: ActionTypeHolder{ActionType: ActionKeyboard}, Data: map[string]string{"keys": "Test2"}},
} }
testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) { testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out ActionData) {
require.Nil(t, err, "could not run page actions") require.Nil(t, err, "could not run page actions")
el := page.Page().MustElement("input") el := page.Page().MustElement("input")
require.Equal(t, "Test2", el.MustText(), "could not get input change value") require.Equal(t, "Test2", el.MustText(), "could not get input change value")
@ -529,7 +532,7 @@ func TestActionSleep(t *testing.T) {
{ActionType: ActionTypeHolder{ActionType: ActionSleep}, Data: map[string]string{"duration": "2"}}, {ActionType: ActionTypeHolder{ActionType: ActionSleep}, Data: map[string]string{"duration": "2"}},
} }
testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) { testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out ActionData) {
require.Nil(t, err, "could not run page actions") require.Nil(t, err, "could not run page actions")
require.True(t, page.Page().MustElement("button").MustVisible(), "could not get button") require.True(t, page.Page().MustElement("button").MustVisible(), "could not get button")
}) })
@ -553,7 +556,7 @@ func TestActionWaitVisible(t *testing.T) {
} }
t.Run("wait for an element being visible", func(t *testing.T) { t.Run("wait for an element being visible", func(t *testing.T) {
testHeadlessSimpleResponse(t, response, actions, 2*time.Second, func(page *Page, err error, out map[string]string) { testHeadlessSimpleResponse(t, response, actions, 2*time.Second, func(page *Page, err error, out ActionData) {
require.Nil(t, err, "could not run page actions") require.Nil(t, err, "could not run page actions")
page.Page().MustElement("button").MustVisible() page.Page().MustElement("button").MustVisible()
@ -562,21 +565,82 @@ func TestActionWaitVisible(t *testing.T) {
t.Run("timeout because of element not visible", func(t *testing.T) { t.Run("timeout because of element not visible", func(t *testing.T) {
// increased timeout from time.Second/2 to time.Second due to random fails (probably due to overhead and system) // increased timeout from time.Second/2 to time.Second due to random fails (probably due to overhead and system)
testHeadlessSimpleResponse(t, response, actions, time.Second, func(page *Page, err error, out map[string]string) { testHeadlessSimpleResponse(t, response, actions, time.Second, func(page *Page, err error, out ActionData) {
require.Error(t, err) require.Error(t, err)
require.Contains(t, err.Error(), "Element did not appear in the given amount of time") require.Contains(t, err.Error(), "Element did not appear in the given amount of time")
}) })
}) })
} }
func testHeadlessSimpleResponse(t *testing.T, response string, actions []*Action, timeout time.Duration, assert func(page *Page, pageErr error, out map[string]string)) { func TestActionWaitDialog(t *testing.T) {
response := `<html>
<head>
<title>Nuclei Test Page</title>
</head>
<body>
<script type="text/javascript">
const urlParams = new URLSearchParams(window.location.search);
const scriptContent = urlParams.get('script');
if (scriptContent) {
const scriptElement = document.createElement('script');
scriptElement.textContent = scriptContent;
document.body.appendChild(scriptElement);
}
</script>
</body>
</html>`
t.Run("Triggered", func(t *testing.T) {
actions := []*Action{
{
ActionType: ActionTypeHolder{ActionType: ActionNavigate},
Data: map[string]string{"url": "{{BaseURL}}/?script=alert%281%29"},
},
{
ActionType: ActionTypeHolder{ActionType: ActionWaitDialog},
Name: "test",
},
}
testHeadlessSimpleResponse(t, response, actions, 1*time.Second, func(page *Page, err error, out ActionData) {
require.Nil(t, err, "could not run page actions")
test, ok := out["test"].(bool)
require.True(t, ok, "could not assert test to bool")
require.True(t, test, "could not find test")
})
})
t.Run("Invalid", func(t *testing.T) {
actions := []*Action{
{
ActionType: ActionTypeHolder{ActionType: ActionNavigate},
Data: map[string]string{"url": "{{BaseURL}}/?script=foo"},
},
{
ActionType: ActionTypeHolder{ActionType: ActionWaitDialog},
Name: "test",
},
}
testHeadlessSimpleResponse(t, response, actions, 1*time.Second, func(page *Page, err error, out ActionData) {
require.Nil(t, err, "could not run page actions")
_, ok := out["test"].(bool)
require.False(t, ok, "output assertion is success")
})
})
}
func testHeadlessSimpleResponse(t *testing.T, response string, actions []*Action, timeout time.Duration, assert func(page *Page, pageErr error, out ActionData)) {
t.Helper() t.Helper()
testHeadless(t, actions, timeout, func(w http.ResponseWriter, r *http.Request) { testHeadless(t, actions, timeout, func(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprintln(w, response) _, _ = fmt.Fprintln(w, response)
}, assert) }, assert)
} }
func testHeadless(t *testing.T, actions []*Action, timeout time.Duration, handler func(w http.ResponseWriter, r *http.Request), assert func(page *Page, pageErr error, extractedData map[string]string)) { func testHeadless(t *testing.T, actions []*Action, timeout time.Duration, handler func(w http.ResponseWriter, r *http.Request), assert func(page *Page, pageErr error, extractedData ActionData)) {
t.Helper() t.Helper()
lfa := getBoolFromEnv("LOCAL_FILE_ACCESS", true) lfa := getBoolFromEnv("LOCAL_FILE_ACCESS", true)

View File

@ -183,7 +183,13 @@ func (request *Request) executeRequestWithPayloads(input *contextargs.Context, p
responseBody, _ = html.HTML() responseBody, _ = html.HTML()
} }
outputEvent := request.responseToDSLMap(responseBody, out["header"], out["status_code"], reqBuilder.String(), input.MetaInput.Input, navigatedURL, page.DumpHistory()) header := out.GetOrDefault("header", "").(string)
// NOTE(dwisiswant0): `status_code` key should be an integer type.
// Ref: https://github.com/projectdiscovery/nuclei/pull/5545#discussion_r1721291013
statusCode := out.GetOrDefault("status_code", "").(string)
outputEvent := request.responseToDSLMap(responseBody, header, statusCode, reqBuilder.String(), input.MetaInput.Input, navigatedURL, page.DumpHistory())
// add response fields to template context and merge templatectx variables to output event // add response fields to template context and merge templatectx variables to output event
request.options.AddTemplateVars(input.MetaInput, request.Type(), request.ID, outputEvent) request.options.AddTemplateVars(input.MetaInput, request.Type(), request.ID, outputEvent)
if request.options.HasTemplateCtx(input.MetaInput) { if request.options.HasTemplateCtx(input.MetaInput) {