Path: blob/dev/pkg/protocols/headless/engine/page_actions.go
2072 views
package engine12import (3"context"4"fmt"5"os"6"path/filepath"7"reflect"8"strconv"9"strings"10"sync"11"time"1213"github.com/go-rod/rod"14"github.com/go-rod/rod/lib/input"15"github.com/go-rod/rod/lib/proto"16"github.com/go-rod/rod/lib/utils"17"github.com/kitabisa/go-ci"18"github.com/pkg/errors"19"github.com/projectdiscovery/gologger"20"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"21"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/expressions"22"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"23contextutil "github.com/projectdiscovery/utils/context"24"github.com/projectdiscovery/utils/errkit"25fileutil "github.com/projectdiscovery/utils/file"26folderutil "github.com/projectdiscovery/utils/folder"27stringsutil "github.com/projectdiscovery/utils/strings"28urlutil "github.com/projectdiscovery/utils/url"29"github.com/segmentio/ksuid"30)3132var (33errinvalidArguments = errkit.New("invalid arguments provided")34ErrLFAccessDenied = errkit.New("Use -allow-local-file-access flag to enable local file access")35// ErrActionExecDealine is the error returned when alloted time for action execution exceeds36ErrActionExecDealine = errkit.New("headless action execution deadline exceeded").SetKind(errkit.ErrKindDeadline).Build()37)3839const (40errCouldNotGetElement = "could not get element"41errCouldNotScroll = "could not scroll into view"42errElementDidNotAppear = "Element did not appear in the given amount of time"43)4445// ExecuteActions executes a list of actions on a page.46func (p *Page) ExecuteActions(input *contextargs.Context, actions []*Action) (outData ActionData, err error) {47outData = make(ActionData)4849// waitFuncs are function that needs to be executed after navigation50// typically used for waitEvent51waitFuncs := make([]func() error, 0)5253// avoid any future panics caused due to go-rod library54// TODO(dwisiswant0): remove this once we get the RCA.55defer func() {56if ci.IsCI() {57return58}5960if r := recover(); r != nil {61err = errkit.Newf("panic on headless action: %v", r)62}63}()6465for _, act := range actions {66switch act.ActionType.ActionType {67case ActionNavigate:68err = p.NavigateURL(act, outData)69if err == nil {70// if navigation successful trigger all waitFuncs (if any)71for _, waitFunc := range waitFuncs {72if waitFunc != nil {73if err := waitFunc(); err != nil {74return nil, errkit.Wrap(err, "error occurred while executing waitFunc")75}76}77}7879p.lastActionNavigate = act80}81case ActionScript:82err = p.RunScript(act, outData)83case ActionClick:84err = p.ClickElement(act, outData)85case ActionRightClick:86err = p.RightClickElement(act, outData)87case ActionTextInput:88err = p.InputElement(act, outData)89case ActionScreenshot:90err = p.Screenshot(act, outData)91case ActionTimeInput:92err = p.TimeInputElement(act, outData)93case ActionSelectInput:94err = p.SelectInputElement(act, outData)95case ActionWaitDOM:96event := proto.PageLifecycleEventNameDOMContentLoaded97err = p.WaitPageLifecycleEvent(act, outData, event)98case ActionWaitFCP:99event := proto.PageLifecycleEventNameFirstContentfulPaint100err = p.WaitPageLifecycleEvent(act, outData, event)101case ActionWaitFMP:102event := proto.PageLifecycleEventNameFirstMeaningfulPaint103err = p.WaitPageLifecycleEvent(act, outData, event)104case ActionWaitIdle:105event := proto.PageLifecycleEventNameNetworkIdle106err = p.WaitPageLifecycleEvent(act, outData, event)107case ActionWaitLoad:108event := proto.PageLifecycleEventNameLoad109err = p.WaitPageLifecycleEvent(act, outData, event)110case ActionWaitStable:111err = p.WaitStable(act, outData)112// NOTE(dwisiswant0): Mapping `ActionWaitLoad` to `Page.WaitStable`,113// just in case waiting for the `proto.PageLifecycleEventNameLoad` event114// doesn't meet expectations.115// case ActionWaitLoad, ActionWaitStable:116// err = p.WaitStable(act, outData)117case ActionGetResource:118err = p.GetResource(act, outData)119case ActionExtract:120err = p.ExtractElement(act, outData)121case ActionWaitEvent:122var waitFunc func() error123waitFunc, err = p.WaitEvent(act, outData)124if waitFunc != nil {125waitFuncs = append(waitFuncs, waitFunc)126}127case ActionWaitDialog:128err = p.HandleDialog(act, outData)129case ActionFilesInput:130if p.options.Options.AllowLocalFileAccess {131err = p.FilesInput(act, outData)132} else {133err = ErrLFAccessDenied134}135case ActionAddHeader:136err = p.ActionAddHeader(act, outData)137case ActionSetHeader:138err = p.ActionSetHeader(act, outData)139case ActionDeleteHeader:140err = p.ActionDeleteHeader(act, outData)141case ActionSetBody:142err = p.ActionSetBody(act, outData)143case ActionSetMethod:144err = p.ActionSetMethod(act, outData)145case ActionKeyboard:146err = p.KeyboardAction(act, outData)147case ActionDebug:148err = p.DebugAction(act, outData)149case ActionSleep:150err = p.SleepAction(act, outData)151case ActionWaitVisible:152err = p.WaitVisible(act, outData)153default:154continue155}156if err != nil {157return nil, errors.Wrap(err, "error occurred executing action")158}159}160return outData, nil161}162163type rule struct {164*sync.Once165Action ActionType166Part string167Args map[string]string168}169170// WaitVisible waits until an element appears.171func (p *Page) WaitVisible(act *Action, out ActionData) error {172timeout, err := getTimeout(p, act)173if err != nil {174return errors.Wrap(err, "Wrong timeout given")175}176177pollTime, err := getTimeParameter(p, act, "pollTime", 100, time.Millisecond)178if err != nil {179return errors.Wrap(err, "Wrong polling time given")180}181182element, _ := p.Sleeper(pollTime, timeout).183Timeout(timeout).184pageElementBy(act.Data)185186if element != nil {187if err := element.WaitVisible(); err != nil {188return errors.Wrap(err, errElementDidNotAppear)189}190} else {191return errors.New(errElementDidNotAppear)192}193194return nil195}196197func (p *Page) Sleeper(pollTimeout, timeout time.Duration) *Page {198page := *p199page.page = page.Page().Sleeper(func() utils.Sleeper {200return createBackOffSleeper(pollTimeout, timeout)201})202return &page203}204205func (p *Page) Timeout(timeout time.Duration) *Page {206page := *p207page.page = page.Page().Timeout(timeout)208return &page209}210211func createBackOffSleeper(pollTimeout, timeout time.Duration) utils.Sleeper {212backoffSleeper := utils.BackoffSleeper(pollTimeout, timeout, func(duration time.Duration) time.Duration {213return duration214})215216return func(ctx context.Context) error {217if ctx.Err() != nil {218return ctx.Err()219}220221return backoffSleeper(ctx)222}223}224225func getNavigationFunc(p *Page, act *Action, event proto.PageLifecycleEventName) (func(), error) {226dur, err := getTimeout(p, act)227if err != nil {228return nil, errors.Wrap(err, "Wrong timeout given")229}230231fn := p.page.Timeout(dur).WaitNavigation(event)232233return fn, nil234}235236func getTimeout(p *Page, act *Action) (time.Duration, error) {237return getTimeParameter(p, act, "timeout", 5, time.Second)238}239240// getTimeParameter returns a time parameter from an action. It first tries to241// get the parameter as an integer, then as a time.Duration, and finally falls242// back to the default value (multiplied by the unit).243func getTimeParameter(p *Page, act *Action, argName string, defaultValue, unit time.Duration) (time.Duration, error) {244argValue, err := p.getActionArg(act, argName)245if err != nil {246return time.Duration(0), err247}248249convertedValue, err := strconv.Atoi(argValue)250if err == nil {251return time.Duration(convertedValue) * unit, nil252}253254// fallback to time.ParseDuration255parsedTimeValue, err := time.ParseDuration(argValue)256if err == nil {257return parsedTimeValue, nil258}259260return defaultValue * unit, nil261}262263// ActionAddHeader executes a AddHeader action.264func (p *Page) ActionAddHeader(act *Action, out ActionData) error {265args := make(map[string]string)266267part, err := p.getActionArg(act, "part")268if err != nil {269return err270}271272args["key"], err = p.getActionArg(act, "key")273if err != nil {274return err275}276277args["value"], err = p.getActionArg(act, "value")278if err != nil {279return err280}281282p.rules = append(p.rules, rule{283Action: ActionAddHeader,284Part: part,285Args: args,286})287288return nil289}290291// ActionSetHeader executes a SetHeader action.292func (p *Page) ActionSetHeader(act *Action, out ActionData) error {293args := make(map[string]string)294295part, err := p.getActionArg(act, "part")296if err != nil {297return err298}299300args["key"], err = p.getActionArg(act, "key")301if err != nil {302return err303}304305args["value"], err = p.getActionArg(act, "value")306if err != nil {307return err308}309310p.rules = append(p.rules, rule{311Action: ActionSetHeader,312Part: part,313Args: args,314})315316return nil317}318319// ActionDeleteHeader executes a DeleteHeader action.320func (p *Page) ActionDeleteHeader(act *Action, out ActionData) error {321args := make(map[string]string)322323part, err := p.getActionArg(act, "part")324if err != nil {325return err326}327328args["key"], err = p.getActionArg(act, "key")329if err != nil {330return err331}332333p.rules = append(p.rules, rule{334Action: ActionDeleteHeader,335Part: part,336Args: args,337})338339return nil340}341342// ActionSetBody executes a SetBody action.343func (p *Page) ActionSetBody(act *Action, out ActionData) error {344args := make(map[string]string)345346part, err := p.getActionArg(act, "part")347if err != nil {348return err349}350351args["body"], err = p.getActionArg(act, "body")352if err != nil {353return err354}355356p.rules = append(p.rules, rule{357Action: ActionSetBody,358Part: part,359Args: args,360})361362return nil363}364365// ActionSetMethod executes an SetMethod action.366func (p *Page) ActionSetMethod(act *Action, out ActionData) error {367args := make(map[string]string)368369part, err := p.getActionArg(act, "part")370if err != nil {371return err372}373374args["method"], err = p.getActionArg(act, "method")375if err != nil {376return err377}378379p.rules = append(p.rules, rule{380Action: ActionSetMethod,381Part: part,382Args: args,383Once: &sync.Once{},384})385386return nil387}388389// NavigateURL executes an ActionLoadURL actions loading a URL for the page.390func (p *Page) NavigateURL(action *Action, out ActionData) error {391url, err := p.getActionArg(action, "url")392if err != nil {393return err394}395396if url == "" {397return errinvalidArguments398}399400parsedURL, err := urlutil.ParseURL(url, true)401if err != nil {402return errkit.Newf("failed to parse url %v while creating http request", url)403}404405// ===== parameter automerge =====406// while merging parameters first preference is given to target params407finalparams := parsedURL.Params.Clone()408finalparams.Merge(p.inputURL.Params.Encode())409parsedURL.Params = finalparams410411if err := p.page.Navigate(parsedURL.String()); err != nil {412return errkit.Wrapf(err, "could not navigate to url %s", parsedURL.String())413}414415p.updateLastNavigatedURL()416417return nil418}419420// RunScript runs a script on the loaded page421func (p *Page) RunScript(act *Action, out ActionData) error {422code, err := p.getActionArg(act, "code")423if err != nil {424return err425}426427if code == "" {428return errinvalidArguments429}430431hook, err := p.getActionArg(act, "hook")432if err != nil {433return err434}435436if hook == "true" {437if _, err := p.page.EvalOnNewDocument(code); err != nil {438return err439}440}441442data, err := p.page.Eval(code)443if err != nil {444return err445}446447if data != nil && act.Name != "" {448out[act.Name] = data.Value.String()449}450451return nil452}453454// ClickElement executes click actions for an element.455func (p *Page) ClickElement(act *Action, out ActionData) error {456element, err := p.pageElementBy(act.Data)457if err != nil {458return errors.Wrap(err, errCouldNotGetElement)459}460if err = element.ScrollIntoView(); err != nil {461return errors.Wrap(err, errCouldNotScroll)462}463if err = element.Click(proto.InputMouseButtonLeft, 1); err != nil {464return errors.Wrap(err, "could not click element")465}466return nil467}468469// KeyboardAction executes a keyboard action on the page.470func (p *Page) KeyboardAction(act *Action, out ActionData) error {471keys, err := p.getActionArg(act, "keys")472if err != nil {473return err474}475476return p.page.Keyboard.Type([]input.Key(keys)...)477}478479// RightClickElement executes right click actions for an element.480func (p *Page) RightClickElement(act *Action, out ActionData) error {481element, err := p.pageElementBy(act.Data)482if err != nil {483return errors.Wrap(err, errCouldNotGetElement)484}485if err = element.ScrollIntoView(); err != nil {486return errors.Wrap(err, errCouldNotScroll)487}488if err = element.Click(proto.InputMouseButtonRight, 1); err != nil {489return errors.Wrap(err, "could not right click element")490}491return nil492}493494// Screenshot executes screenshot action on a page495func (p *Page) Screenshot(act *Action, out ActionData) error {496to, err := p.getActionArg(act, "to")497if err != nil {498return err499}500501if to == "" {502to = ksuid.New().String()503if act.Name != "" {504out[act.Name] = to505}506}507508var data []byte509510fullpage, err := p.getActionArg(act, "fullpage")511if err != nil {512return err513}514515if fullpage == "true" {516data, err = p.page.Screenshot(true, &proto.PageCaptureScreenshot{})517} else {518data, err = p.page.Screenshot(false, &proto.PageCaptureScreenshot{})519}520if err != nil {521return errors.Wrap(err, "could not take screenshot")522}523524to, err = fileutil.CleanPath(to)525if err != nil {526return errkit.Newf("could not clean output screenshot path %s", to)527}528529// allow if targetPath is child of current working directory530if !protocolstate.IsLfaAllowed(p.options.Options) {531cwd, err := os.Getwd()532if err != nil {533return errkit.Wrap(err, "could not get current working directory")534}535536if !strings.HasPrefix(to, cwd) {537// writing outside of cwd requires -lfa flag538return ErrLFAccessDenied539}540}541542mkdir, err := p.getActionArg(act, "mkdir")543if err != nil {544return err545}546547// edgecase create directory if mkdir=true and path contains directory548if mkdir == "true" && stringsutil.ContainsAny(to, folderutil.UnixPathSeparator, folderutil.WindowsPathSeparator) {549// creates new directory if needed based on path `to`550// TODO: replace all permission bits with fileutil constants (https://github.com/projectdiscovery/utils/issues/113)551if err := os.MkdirAll(filepath.Dir(to), 0700); err != nil {552return errkit.Wrap(err, "failed to create directory while writing screenshot")553}554}555556// actual file path to write557filePath := to558if !strings.HasSuffix(filePath, ".png") {559filePath += ".png"560}561562if fileutil.FileExists(filePath) {563// return custom error as overwriting files is not supported564return errkit.Newf("failed to write screenshot, file %v already exists", filePath)565}566err = os.WriteFile(filePath, data, 0540)567if err != nil {568return errors.Wrap(err, "could not write screenshot")569}570gologger.Info().Msgf("Screenshot successfully saved at %v\n", filePath)571return nil572}573574// InputElement executes input element actions for an element.575func (p *Page) InputElement(act *Action, out ActionData) error {576value, err := p.getActionArg(act, "value")577if err != nil {578return err579}580if value == "" {581return errinvalidArguments582}583element, err := p.pageElementBy(act.Data)584if err != nil {585return errors.Wrap(err, errCouldNotGetElement)586}587if err = element.ScrollIntoView(); err != nil {588return errors.Wrap(err, errCouldNotScroll)589}590if err = element.Input(value); err != nil {591return errors.Wrap(err, "could not input element")592}593return nil594}595596// TimeInputElement executes time input on an element597func (p *Page) TimeInputElement(act *Action, out ActionData) error {598value, err := p.getActionArg(act, "value")599if err != nil {600return err601}602if value == "" {603return errinvalidArguments604}605element, err := p.pageElementBy(act.Data)606if err != nil {607return errors.Wrap(err, errCouldNotGetElement)608}609if err = element.ScrollIntoView(); err != nil {610return errors.Wrap(err, errCouldNotScroll)611}612t, err := time.Parse(time.RFC3339, value)613if err != nil {614return errors.Wrap(err, "could not parse time")615}616if err := element.InputTime(t); err != nil {617return errors.Wrap(err, "could not input element")618}619return nil620}621622// SelectInputElement executes select input statement action on a element623func (p *Page) SelectInputElement(act *Action, out ActionData) error {624value, err := p.getActionArg(act, "value")625if err != nil {626return err627}628if value == "" {629return errinvalidArguments630}631element, err := p.pageElementBy(act.Data)632if err != nil {633return errors.Wrap(err, errCouldNotGetElement)634}635if err = element.ScrollIntoView(); err != nil {636return errors.Wrap(err, errCouldNotScroll)637}638639var selectedBool bool640641selected, err := p.getActionArg(act, "selected")642if err != nil {643return err644}645646if selected == "true" {647selectedBool = true648}649650selector, err := p.getActionArg(act, "selector")651if err != nil {652return err653}654655if err := element.Select([]string{value}, selectedBool, selectorBy(selector)); err != nil {656return errors.Wrap(err, "could not select input")657}658659return nil660}661662// WaitPageLifecycleEvent waits for specified page lifecycle event name663func (p *Page) WaitPageLifecycleEvent(act *Action, out ActionData, event proto.PageLifecycleEventName) error {664fn, err := getNavigationFunc(p, act, event)665if err != nil {666return err667}668669fn()670671// log the navigated request (even if it is a redirect)672p.updateLastNavigatedURL()673674return nil675}676677// WaitStable waits until the page is stable678func (p *Page) WaitStable(act *Action, out ActionData) error {679dur := time.Second // default stable page duration: 1s680681timeout, err := getTimeout(p, act)682if err != nil {683return errors.Wrap(err, "Wrong timeout given")684}685686argDur := act.Data["duration"]687if argDur != "" {688dur, err = time.ParseDuration(argDur)689if err != nil {690dur = time.Second691}692}693694if err := p.page.Timeout(timeout).WaitStable(dur); err != nil {695return err696}697698// log the navigated request (even if it is a redirect)699p.updateLastNavigatedURL()700701return nil702}703704// GetResource gets a resource from an element from page.705func (p *Page) GetResource(act *Action, out ActionData) error {706element, err := p.pageElementBy(act.Data)707if err != nil {708return errors.Wrap(err, errCouldNotGetElement)709}710resource, err := element.Resource()711if err != nil {712return errors.Wrap(err, "could not get src for element")713}714if act.Name != "" {715out[act.Name] = string(resource)716}717return nil718}719720// FilesInput acts with a file input element on page721func (p *Page) FilesInput(act *Action, out ActionData) error {722element, err := p.pageElementBy(act.Data)723if err != nil {724return errors.Wrap(err, errCouldNotGetElement)725}726727if err = element.ScrollIntoView(); err != nil {728return errors.Wrap(err, errCouldNotScroll)729}730731value, err := p.getActionArg(act, "value")732if err != nil {733return err734}735filesPaths := strings.Split(value, ",")736737if err := element.SetFiles(filesPaths); err != nil {738return errors.Wrap(err, "could not set files")739}740741return nil742}743744// ExtractElement extracts from an element on the page.745func (p *Page) ExtractElement(act *Action, out ActionData) error {746element, err := p.pageElementBy(act.Data)747if err != nil {748return errors.Wrap(err, errCouldNotGetElement)749}750751if err = element.ScrollIntoView(); err != nil {752return errors.Wrap(err, errCouldNotScroll)753}754755target, err := p.getActionArg(act, "target")756if err != nil {757return err758}759760switch target {761case "attribute":762attribute, err := p.getActionArg(act, "attribute")763if err != nil {764return err765}766767if attribute == "" {768return errors.New("attribute can't be empty")769}770771attrValue, err := element.Attribute(attribute)772if err != nil {773return errors.Wrap(err, "could not get attribute")774}775776if act.Name != "" {777out[act.Name] = *attrValue778}779default:780text, err := element.Text()781if err != nil {782return errors.Wrap(err, "could not get element text node")783}784785if act.Name != "" {786out[act.Name] = text787}788}789return nil790}791792// WaitEvent waits for an event to happen on the page.793func (p *Page) WaitEvent(act *Action, out ActionData) (func() error, error) {794event, err := p.getActionArg(act, "event")795if err != nil {796return nil, err797}798799if event == "" {800return nil, errors.New("event not recognized")801}802803var waitEvent proto.Event804805gotType := proto.GetType(event)806if gotType == nil {807return nil, errkit.Newf("event %q does not exist", event)808}809810tmp, ok := reflect.New(gotType).Interface().(proto.Event)811if !ok {812return nil, errkit.Newf("event %q is not a page event", event)813}814815waitEvent = tmp816817// allow user to specify max-duration for wait-event818maxDuration, err := getTimeParameter(p, act, "max-duration", 5, time.Second)819if err != nil {820return nil, err821}822823// Just wait the event to happen824waitFunc := func() (err error) {825// execute actual wait event826ctx, cancel := context.WithTimeoutCause(context.Background(), maxDuration, ErrActionExecDealine)827defer cancel()828829err = contextutil.ExecFunc(ctx, p.page.WaitEvent(waitEvent))830831return832}833834return waitFunc, nil835}836837// HandleDialog handles JavaScript dialog (alert, confirm, prompt, or onbeforeunload).838func (p *Page) HandleDialog(act *Action, out ActionData) error {839maxDuration, err := getTimeParameter(p, act, "max-duration", 10, time.Second)840if err != nil {841return err842}843844ctx, cancel := context.WithTimeout(context.Background(), maxDuration)845defer cancel()846847wait, handle := p.page.HandleDialog()848fn := func() (*proto.PageJavascriptDialogOpening, error) {849dialog := wait()850err := handle(&proto.PageHandleJavaScriptDialog{851Accept: true,852PromptText: "",853})854855return dialog, err856}857858dialog, err := contextutil.ExecFuncWithTwoReturns(ctx, fn)859if err == nil && act.Name != "" {860out[act.Name] = true861out[act.Name+"_type"] = string(dialog.Type)862out[act.Name+"_message"] = dialog.Message863}864865return nil866}867868// pageElementBy returns a page element from a variety of inputs.869//870// Supported values for by: r -> selector & regex, x -> xpath, js -> eval js,871// search => query, default ("") => selector.872func (p *Page) pageElementBy(data map[string]string) (*rod.Element, error) {873by, ok := data["by"]874if !ok {875by = ""876}877page := p.page878879switch by {880case "r", "regex":881return page.ElementR(data["selector"], data["regex"])882case "x", "xpath":883return page.ElementX(data["xpath"])884case "js":885return page.ElementByJS(&rod.EvalOptions{JS: data["js"]})886case "search":887elms, err := page.Search(data["query"])888if err != nil {889return nil, err890}891892if elms.First != nil {893return elms.First, nil894}895return nil, errors.New("no such element")896default:897return page.Element(data["selector"])898}899}900901// DebugAction enables debug action on a page.902func (p *Page) DebugAction(act *Action, out ActionData) error {903p.instance.browser.engine.SlowMotion(5 * time.Second)904p.instance.browser.engine.Trace(true)905return nil906}907908// SleepAction sleeps on the page for a specified duration909func (p *Page) SleepAction(act *Action, out ActionData) error {910duration, err := getTimeParameter(p, act, "duration", 5, time.Second)911if err != nil {912return err913}914915time.Sleep(duration)916917return nil918}919920// selectorBy returns a selector from a representation.921func selectorBy(selector string) rod.SelectorType {922switch selector {923case "r":924return rod.SelectorTypeRegex925case "css":926return rod.SelectorTypeCSSSector927case "regex":928return rod.SelectorTypeRegex929default:930return rod.SelectorTypeText931}932}933934func (p *Page) getActionArg(action *Action, arg string) (string, error) {935var err error936937argValue := action.GetArg(arg)938939if p.instance.interactsh != nil {940var interactshURLs []string941argValue, interactshURLs = p.instance.interactsh.Replace(argValue, p.InteractshURLs)942p.addInteractshURL(interactshURLs...)943}944945exprs := getExpressions(argValue, p.variables)946947err = expressions.ContainsUnresolvedVariables(exprs...)948if err != nil {949return "", errkit.Wrapf(err, "argument %q, value: %q", arg, argValue)950}951952argValue, err = expressions.Evaluate(argValue, p.variables)953if err != nil {954return "", fmt.Errorf("could not get value for argument %q: %s", arg, err)955}956957return argValue, nil958}959960961