From 74e53cb738ae51d80ba4219d59adb68f5de4616c Mon Sep 17 00:00:00 2001 From: SOOS-JAlvarez Date: Wed, 11 Jan 2023 11:18:52 -0300 Subject: [PATCH 1/4] PA-7854 Added multi step/page form support --- helpers/auth.py | 11 +++++++++++ helpers/configuration.py | 2 ++ main.py | 19 +++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/helpers/auth.py b/helpers/auth.py index 20eda27..e99c81c 100644 --- a/helpers/auth.py +++ b/helpers/auth.py @@ -200,6 +200,17 @@ def login(self): if self.config.auth_username: username_element = self.fill_username() + if self.config.auth_form_type == 'MULTI_STEP': + log(f"Waiting for {self.config.auth_password_field_name} element to load") + sleep(5) + + if self.config.auth_form_type == 'MULTI_PAGE': + continue_button = self.find_element(self.config.auth_first_submit_field_name, "submit", "//*[@type='submit' or @type='button' or button]" ) + actions = ActionChains(self.driver) + actions.move_to_element(continue_button).click().perform() + log(f"Clicked the first submit element") + sleep(5) + # fill out the password field if self.config.auth_password: try: diff --git a/helpers/configuration.py b/helpers/configuration.py index 03b3cd2..64447e5 100644 --- a/helpers/configuration.py +++ b/helpers/configuration.py @@ -14,6 +14,7 @@ class DASTConfig: auth_password: Optional[str] = None auth_otp_secret: Optional[str] = None auth_submit_action: Optional[str] = None + auth_form_type: Optional[str] = None auth_token_endpoint: Optional[str] = None auth_bearer_token: Optional[str] = None auth_username_field_name: Optional[str] = None @@ -45,6 +46,7 @@ def load_config(self, extra_zap_params): self.auth_password = self._get_zap_param('auth.password') or EMPTY_STRING self.auth_otp_secret = self._get_zap_param('auth.otpsecret') or EMPTY_STRING self.auth_submit_action = self._get_zap_param('auth.submit_action') or 'click' + self.auth_form_type = self._get_zap_param('auth.form_type') or 'SIMPLE' self.auth_token_endpoint = self._get_zap_param('auth.token_endpoint') or EMPTY_STRING self.auth_bearer_token = self._get_zap_param('auth.bearer_token') or EMPTY_STRING self.auth_username_field_name = self._get_zap_param('auth.username_field') or 'username' diff --git a/main.py b/main.py index 228ca28..1c79ab6 100644 --- a/main.py +++ b/main.py @@ -98,6 +98,7 @@ def __init__(self): self.auth_submit_field_name: Optional[str] = None self.auth_first_submit_field_name: Optional[str] = None self.auth_submit_action: Optional[str] = None + self.auth_form_type: Optional[str] = None self.auth_exclude_urls: Optional[str] = None self.auth_display: bool = False self.auth_bearer_token: Optional[str] = None @@ -227,6 +228,8 @@ def parse_configuration(self, configuration: Dict, target_url: str): self.auth_first_submit_field_name = value elif key == 'authSubmitAction': self.auth_submit_action = value + elif key == 'authFormType': + self.auth_form_type = value elif key == "zapOptions": value = array_to_str(value) self.zap_options = value @@ -328,8 +331,12 @@ def __add_zap_options__(self, args: List[str]) -> None: zap_options.append(self.__add_custom_option__(label="auth.display", value=self.auth_display)) if self.auth_submit_field_name is not None: zap_options.append(self.__add_custom_option__(label="auth.submit_field", value=self.auth_submit_field_name)) + if self.auth_first_submit_field_name is not None: + zap_options.append(self.__add_custom_option__(label="auth.first_submit_field", value=self.auth_first_submit_field_name)) if self.auth_submit_action is not None: zap_options.append(self.__add_custom_option__(label="auth.submit_action", value=self.auth_submit_action)) + if self.auth_form_type is not None: + zap_options.append(self.__add_custom_option__(label="auth.form_type", value=self.auth_form_type)) if self.auth_username_field_name is not None: zap_options.append(self.__add_custom_option__(label="auth.username_field", value=self.auth_username_field_name)) if self.auth_password_field_name is not None: @@ -799,6 +806,18 @@ def parse_args(self) -> None: type=str, required=False, ) + parser.add_argument( + "--authContinueField", + help="Continue button id to use in auth apps, only required if form type is MULTI_PAGE", + required=False, + ) + parser.add_argument( + "--authFormType", + help="SIMPLE (all fields are displayed at once), MULTI_STEP (Password field is displayed only after username is filled), or MULTI_PAGE (Password field is displayed only after username is filled and submit is clicked)", + type=str, + default="SIMPLE", + required=False, + ) parser.add_argument( "--zapOptions", help="Additional ZAP Options", From d7f1b48ed439a6dadcaa5cc9439ca98318998689 Mon Sep 17 00:00:00 2001 From: SOOS-JAlvarez Date: Wed, 11 Jan 2023 11:19:07 -0300 Subject: [PATCH 2/4] version --- VERSION.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION.txt b/VERSION.txt index 8fc77d0..f8f3c08 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -1.0.17 +1.0.18 From 2ad75487aadbe97b62d5629173a9cfeff7dfbdca Mon Sep 17 00:00:00 2001 From: SOOS-JAlvarez Date: Wed, 11 Jan 2023 11:22:13 -0300 Subject: [PATCH 3/4] modified docs, cleanup --- README.md | 3 ++- main.py | 7 +------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index ebe085a..c3bef46 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,8 @@ The basic command to run a baseline scan would look like: | `--authUsernameField` | None | Username input id to use in auth apps | | `--authPasswordField` | None | Password input id to use in auth apps | | `--authSubmitField` | None | Submit button id to use in auth apps | -| `--authFirstSubmitField` | None | First submit button id to use in auth apps | +| `--authFirstSubmitField` | None | First submit button id to use in auth apps (for multi-page forms) | +| `--authFormType` | SIMPLE | SIMPLE (all fields are displayed at once), MULTI_STEP (Password field is displayed only after username is filled), or MULTI_PAGE (Password field is displayed only after username is filled and submit is clicked) | | `--authSubmitAction` | None | Submit action to perform on form filled. Options: click or submit | | `--zapOptions` | None | Additional ZAP Options | | `--requestCookies` | None | Set Cookie values for the requests to the target URL | diff --git a/main.py b/main.py index 1c79ab6..1acd125 100644 --- a/main.py +++ b/main.py @@ -797,7 +797,7 @@ def parse_args(self) -> None: ) parser.add_argument( "--authFirstSubmitField", - help="First submit button id to use in auth apps", + help="First submit button id to use in auth apps (for multi-page forms)", required=False, ) parser.add_argument( @@ -806,11 +806,6 @@ def parse_args(self) -> None: type=str, required=False, ) - parser.add_argument( - "--authContinueField", - help="Continue button id to use in auth apps, only required if form type is MULTI_PAGE", - required=False, - ) parser.add_argument( "--authFormType", help="SIMPLE (all fields are displayed at once), MULTI_STEP (Password field is displayed only after username is filled), or MULTI_PAGE (Password field is displayed only after username is filled and submit is clicked)", From 5edccb23a93b750c3a935d1d28e2cda7ab8d069f Mon Sep 17 00:00:00 2001 From: SOOS-JAlvarez Date: Fri, 13 Jan 2023 15:14:42 -0300 Subject: [PATCH 4/4] code review --- README.md | 5 +++-- helpers/auth.py | 18 ++++++++++-------- helpers/configuration.py | 8 +++++--- helpers/constants.py | 1 + main.py | 29 ++++++++++++++++++++--------- 5 files changed, 39 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index c3bef46..ea61611 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,9 @@ The basic command to run a baseline scan would look like: | `--authUsernameField` | None | Username input id to use in auth apps | | `--authPasswordField` | None | Password input id to use in auth apps | | `--authSubmitField` | None | Submit button id to use in auth apps | -| `--authFirstSubmitField` | None | First submit button id to use in auth apps (for multi-page forms) | -| `--authFormType` | SIMPLE | SIMPLE (all fields are displayed at once), MULTI_STEP (Password field is displayed only after username is filled), or MULTI_PAGE (Password field is displayed only after username is filled and submit is clicked) | +| `--authSecondSubmitField` | None | Second submit button id to use in auth apps (for multi-page forms) | +| `--authFormType` | simple | simple (all fields are displayed at once), wait_for_password (Password field is displayed only after username is filled), or multi_page (Password field is displayed only after username is filled and submit is clicked) | +| `--authDelayTime` | Delay time in seconds to wait for the page to load after performing actions in the form. (Used only on authFormType: wait_for_password and multi_page) | | `--authSubmitAction` | None | Submit action to perform on form filled. Options: click or submit | | `--zapOptions` | None | Additional ZAP Options | | `--requestCookies` | None | Set Cookie values for the requests to the target URL | diff --git a/helpers/auth.py b/helpers/auth.py index e99c81c..2824b0c 100644 --- a/helpers/auth.py +++ b/helpers/auth.py @@ -188,6 +188,7 @@ def login(self): log(f"authenticate using webdriver against URL: {self.config.auth_login_url}") self.driver.get(self.config.auth_login_url) + final_submit_button = self.config.auth_submit_field_name # wait for the page to load sleep(5) @@ -200,16 +201,17 @@ def login(self): if self.config.auth_username: username_element = self.fill_username() - if self.config.auth_form_type == 'MULTI_STEP': + if self.config.auth_form_type == 'wait_for_password': log(f"Waiting for {self.config.auth_password_field_name} element to load") - sleep(5) + sleep(self.config.auth_delay_time) - if self.config.auth_form_type == 'MULTI_PAGE': - continue_button = self.find_element(self.config.auth_first_submit_field_name, "submit", "//*[@type='submit' or @type='button' or button]" ) + if self.config.auth_form_type == 'multi_page': + continue_button = self.find_element(self.config.auth_submit_field_name, "submit", "//*[@type='submit' or @type='button' or button]" ) actions = ActionChains(self.driver) actions.move_to_element(continue_button).click().perform() - log(f"Clicked the first submit element") - sleep(5) + final_submit_button = self.config.auth_submit_second_field_name + log(f"Clicked the first submit element for multi page") + sleep(self.config.auth_delay_time) # fill out the password field if self.config.auth_password: @@ -234,12 +236,12 @@ def login(self): # if the OTP field was not found, we probably need to submit to go to the OTP page # login flow: username -> next -> password -> next -> otp -> submit self.submit_form(self.config.auth_submit_action, - self.config.auth_submit_field_name, self.config.auth_password_field_name) + final_submit_button, self.config.auth_password_field_name) self.fill_otp() # submit self.submit_form(self.config.auth_submit_action, - self.config.auth_submit_field_name, self.config.auth_password_field_name) + final_submit_button, self.config.auth_password_field_name) # wait for the page to load if self.config.auth_check_element: diff --git a/helpers/configuration.py b/helpers/configuration.py index 64447e5..703cb2f 100644 --- a/helpers/configuration.py +++ b/helpers/configuration.py @@ -15,13 +15,14 @@ class DASTConfig: auth_otp_secret: Optional[str] = None auth_submit_action: Optional[str] = None auth_form_type: Optional[str] = None + auth_delay_time: Optional[int] = None auth_token_endpoint: Optional[str] = None auth_bearer_token: Optional[str] = None auth_username_field_name: Optional[str] = None auth_password_field_name: Optional[str] = None auth_otp_field_name: Optional[str] = None auth_submit_field_name: Optional[str] = None - auth_first_submit_field_name: Optional[str] = None + auth_submit_second_field_name: Optional[str] = None auth_check_delay: Optional[float] = None auth_check_element: Optional[str] = None auth_exclude_urls: Optional[List[str]] = None @@ -46,7 +47,7 @@ def load_config(self, extra_zap_params): self.auth_password = self._get_zap_param('auth.password') or EMPTY_STRING self.auth_otp_secret = self._get_zap_param('auth.otpsecret') or EMPTY_STRING self.auth_submit_action = self._get_zap_param('auth.submit_action') or 'click' - self.auth_form_type = self._get_zap_param('auth.form_type') or 'SIMPLE' + self.auth_form_type = self._get_zap_param('auth.form_type') or 'simple' self.auth_token_endpoint = self._get_zap_param('auth.token_endpoint') or EMPTY_STRING self.auth_bearer_token = self._get_zap_param('auth.bearer_token') or EMPTY_STRING self.auth_username_field_name = self._get_zap_param('auth.username_field') or 'username' @@ -54,7 +55,8 @@ def load_config(self, extra_zap_params): self.auth_display = self._get_zap_param('auth.display') or EMPTY_STRING self.auth_otp_field_name = self._get_zap_param('auth.otp_field') or 'otp' self.auth_submit_field_name = self._get_zap_param('auth.submit_field') or 'login' - self.auth_first_submit_field_name = self._get_zap_param('auth.first_submit_field') or 'next' + self.auth_submit_second_field_name = self._get_zap_param('auth.second_submit_field') or 'login' + self.auth_delay_time = self._get_zap_param_float('auth.delay_time') or 0 self.auth_check_delay = self._get_zap_param_float('auth.check_delay') or 5 self.auth_check_element = self._get_zap_param('auth.check_element') or EMPTY_STRING self.auth_exclude_urls = self._get_zap_param_list('auth.exclude') or list() diff --git a/helpers/constants.py b/helpers/constants.py index 821ff99..ec99676 100644 --- a/helpers/constants.py +++ b/helpers/constants.py @@ -16,6 +16,7 @@ EMPTY_STRING = '' FAIL_THE_BUILD = "fail_the_build" CONTINUE_ON_FAILURE = "continue_on_failure" +AUTH_DELAY_TIME = 5 # seconds # SCAN MODES BASELINE = 'baseline' diff --git a/main.py b/main.py index 1acd125..3beb40a 100644 --- a/main.py +++ b/main.py @@ -96,9 +96,10 @@ def __init__(self): self.auth_username_field_name: Optional[str] = None self.auth_password_field_name: Optional[str] = None self.auth_submit_field_name: Optional[str] = None - self.auth_first_submit_field_name: Optional[str] = None + self.auth_submit_second_field_name: Optional[str] = None self.auth_submit_action: Optional[str] = None self.auth_form_type: Optional[str] = None + self.auth_delay_time: Optional[int] = None self.auth_exclude_urls: Optional[str] = None self.auth_display: bool = False self.auth_bearer_token: Optional[str] = None @@ -224,12 +225,14 @@ def parse_configuration(self, configuration: Dict, target_url: str): self.auth_password_field_name = value elif key == 'authSubmitField': self.auth_submit_field_name = value - elif key == 'authFirstSubmitField': - self.auth_first_submit_field_name = value + elif key == 'authFormSubmitSecondField': + self.auth_submit_second_field_name = value elif key == 'authSubmitAction': self.auth_submit_action = value elif key == 'authFormType': self.auth_form_type = value + elif key == 'authDelayTime': + self.auth_delay_time = value elif key == "zapOptions": value = array_to_str(value) self.zap_options = value @@ -331,12 +334,14 @@ def __add_zap_options__(self, args: List[str]) -> None: zap_options.append(self.__add_custom_option__(label="auth.display", value=self.auth_display)) if self.auth_submit_field_name is not None: zap_options.append(self.__add_custom_option__(label="auth.submit_field", value=self.auth_submit_field_name)) - if self.auth_first_submit_field_name is not None: - zap_options.append(self.__add_custom_option__(label="auth.first_submit_field", value=self.auth_first_submit_field_name)) + if self.auth_submit_second_field_name is not None: + zap_options.append(self.__add_custom_option__(label="auth.second_submit_field", value=self.auth_submit_second_field_name)) if self.auth_submit_action is not None: zap_options.append(self.__add_custom_option__(label="auth.submit_action", value=self.auth_submit_action)) if self.auth_form_type is not None: zap_options.append(self.__add_custom_option__(label="auth.form_type", value=self.auth_form_type)) + if self.auth_delay_time is not None: + zap_options.append(self.__add_custom_option__(label="auth.delay_time", value=self.auth_delay_time)) if self.auth_username_field_name is not None: zap_options.append(self.__add_custom_option__(label="auth.username_field", value=self.auth_username_field_name)) if self.auth_password_field_name is not None: @@ -796,8 +801,8 @@ def parse_args(self) -> None: required=False, ) parser.add_argument( - "--authFirstSubmitField", - help="First submit button id to use in auth apps (for multi-page forms)", + "--authSecondSubmitField", + help="Second submit button id to use in auth apps (for multi-page forms)", required=False, ) parser.add_argument( @@ -808,9 +813,15 @@ def parse_args(self) -> None: ) parser.add_argument( "--authFormType", - help="SIMPLE (all fields are displayed at once), MULTI_STEP (Password field is displayed only after username is filled), or MULTI_PAGE (Password field is displayed only after username is filled and submit is clicked)", + help="simple (all fields are displayed at once), wait_for_password (Password field is displayed only after username is filled), or multi_page (Password field is displayed only after username is filled and submit is clicked)", type=str, - default="SIMPLE", + default="simple", + required=False, + ) + parser.add_argument( + "--authDelayTime", + help="Delay time in seconds to wait for the page to load after performing actions in the form. (Used only on authFormType: wait_for_password and multi_page)", + default=Constants.AUTH_DELAY_TIME, required=False, ) parser.add_argument(