From 8558167b2dc0e9bb6711ce97894e5d1bef17dce9 Mon Sep 17 00:00:00 2001 From: grunny Date: Tue, 26 Sep 2017 02:22:23 +1000 Subject: [PATCH 1/3] Add context commands and the ability to scan as a user Adds commands to manage contexts as well as options to run the spider and active scans as a user. --- README.rst | 1 + tests/cli_test.py | 28 ++++++- tests/zap_helper_test.py | 168 ++++++++++++++++++++++++++++++++++++- zapcli/cli.py | 26 ++++-- zapcli/commands/context.py | 120 ++++++++++++++++++++++++++ zapcli/zap_helper.py | 100 ++++++++++++++++++++-- 6 files changed, 424 insertions(+), 19 deletions(-) create mode 100644 zapcli/commands/context.py diff --git a/README.rst b/README.rst index 2532579..9c1055f 100644 --- a/README.rst +++ b/README.rst @@ -62,6 +62,7 @@ ZAP CLI can then be used with the following commands: active-scan Run an Active Scan. ajax-spider Run the AJAX Spider against a URL. alerts Show alerts at the given alert level. + context Manage contexts for the current session. exclude Exclude a pattern from all scanners. open-url Open a URL using the ZAP proxy. policies Enable or list a set of policies. diff --git a/tests/cli_test.py b/tests/cli_test.py index 7f73091..e72f488 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -110,7 +110,7 @@ def test_open_url_no_url(self, helper_mock): def test_spider_url(self, helper_mock): """Test spider URL method.""" result = self.runner.invoke(cli.cli, ['--boring', '--api-key', '', 'spider', 'http://localhost/']) - helper_mock.assert_called_with('http://localhost/') + helper_mock.assert_called_with('http://localhost/', None, None) self.assertEqual(result.exit_code, 0) @patch('zapcli.zap_helper.ZAPHelper.run_spider') @@ -311,6 +311,32 @@ def test_html_report(self, report_mock): report_mock.assert_called_with('foo.html') self.assertEqual(result.exit_code, 0) + @patch('zapcli.zap_helper.ZAPHelper.include_in_context') + def test_context_include(self, helper_mock): + """Testing including a regex in a given context.""" + result = self.runner.invoke(cli.cli, ['--boring', '--api-key', '', '--verbose', 'context', + 'include', '--name', 'Test', '--pattern', 'zap-cli']) + self.assertEqual(result.exit_code, 0) + + def test_context_include_error(self): + """Testing that an error is reported when providing an invalid regex.""" + result = self.runner.invoke(cli.cli, ['--boring', '--api-key', '', '--verbose', 'context', + 'include', '--name', 'Test', '--pattern', '[']) + self.assertEqual(result.exit_code, 1) + + @patch('zapcli.zap_helper.ZAPHelper.exclude_from_context') + def test_context_exclude(self, helper_mock): + """Testing excluding a regex from a given context.""" + result = self.runner.invoke(cli.cli, ['--boring', '--api-key', '', '--verbose', 'context', + 'exclude', '--name', 'Test', '--pattern', 'zap-cli']) + self.assertEqual(result.exit_code, 0) + + def test_context_exclude_error(self): + """Testing that an error is reported when providing an invalid regex.""" + result = self.runner.invoke(cli.cli, ['--boring', '--api-key', '', '--verbose', 'context', + 'exclude', '--name', 'Test', '--pattern', '[']) + self.assertEqual(result.exit_code, 1) + if __name__ == '__main__': unittest.main() diff --git a/tests/zap_helper_test.py b/tests/zap_helper_test.py index aa17d0b..bdad84f 100644 --- a/tests/zap_helper_test.py +++ b/tests/zap_helper_test.py @@ -27,6 +27,7 @@ class ZAPHelperTestCase(unittest.TestCase): def setUp(self): self.zap_helper = zap_helper.ZAPHelper(api_key=self.api_key) + self.zap_helper._status_check_sleep = 0 @data( ( @@ -218,7 +219,7 @@ def status_result(): class_mock.status = status self.zap_helper.zap.spider = class_mock - self.zap_helper.run_spider('http://localhost', status_check_sleep=0) + self.zap_helper.run_spider('http://localhost') def test_run_spider_error(self): """Test running the spider when an error occurs.""" @@ -227,7 +228,44 @@ def test_run_spider_error(self): self.zap_helper.zap.spider = class_mock with self.assertRaises(ZAPError): - self.zap_helper.run_spider('http://localhost', status_check_sleep=0) + self.zap_helper.run_spider('http://localhost') + + def test_run_spider_as_user(self): + """Test running the spider as a given user.""" + def status_result(): + """Return value of the status property.""" + if status.call_count > 2: + return '100' + return '50' + + class_mock = MagicMock() + class_mock.scan_as_user.return_value = '1' + status = Mock(side_effect=status_result) + class_mock.status = status + self.zap_helper.zap.spider = class_mock + self.zap_helper.zap.context.context = Mock(return_value={'id': '1'}) + self.zap_helper.zap.users.users_list = Mock(return_value=[{'name': 'Test', 'id': '1'}]) + + self.zap_helper.run_spider('http://localhost', 'Test', 'Test') + + def test_run_spider_as_user_error(self): + """Test running the spider as a given user when an error occurs.""" + def status_result(): + """Return value of the status property.""" + if status.call_count > 2: + return '100' + return '50' + + class_mock = MagicMock() + class_mock.scan_as_user.return_value = '1' + status = Mock(side_effect=status_result) + class_mock.status = status + self.zap_helper.zap.spider = class_mock + self.zap_helper.zap.context.context = Mock(return_value={'id': '1'}) + self.zap_helper.zap.users.users_list = Mock(return_value=[]) + + with self.assertRaises(ZAPError): + self.zap_helper.run_spider('http://localhost', 'Test', 'Test') def test_run_active_scan(self): """Test running an active scan.""" @@ -243,7 +281,25 @@ def status_result(): class_mock.status = status self.zap_helper.zap.ascan = class_mock - self.zap_helper.run_active_scan('http://localhost', status_check_sleep=0) + self.zap_helper.run_active_scan('http://localhost') + + def test_run_active_scan_as_user(self): + """Test running an active scan as a given user.""" + def status_result(): + """Return value of the status property.""" + if status.call_count > 2: + return '100' + return '50' + + class_mock = MagicMock() + class_mock.scan_as_user.return_value = '1' + status = Mock(side_effect=status_result) + class_mock.status = status + self.zap_helper.zap.ascan = class_mock + self.zap_helper.zap.context.context = Mock(return_value={'id': '1'}) + self.zap_helper.zap.users.users_list = Mock(return_value=[{'name': 'Test', 'id': '1'}]) + + self.zap_helper.run_active_scan('http://localhost', False, 'Test', 'Test') def test_run_active_scan_error(self): """Test running an active scan.""" @@ -276,7 +332,7 @@ def status_result(): type(class_mock).status = status self.zap_helper.zap.ajaxSpider = class_mock - self.zap_helper.run_ajax_spider('http://localhost', status_check_sleep=0) + self.zap_helper.run_ajax_spider('http://localhost') @data( ( @@ -658,6 +714,110 @@ def test_html_report(self, htmlreport_mock): report_str = report_str.encode('utf-8') file_open_mock().write.assert_called_with(report_str) + @patch('zapv2.context.new_context') + def test_new_context(self, context_mock): + """Test creating a new context.""" + self.zap_helper.new_context('Test') + context_mock.assert_called_with(contextname='Test', apikey=self.api_key) + + @patch('zapv2.context.include_in_context') + def test_include_in_context(self, context_mock): + """Test adding a regex for URLs to include in the context.""" + include_pattern = r"\/zapcli.+" + context_name = 'Test' + context_mock.return_value = 'OK' + + self.zap_helper.include_in_context(context_name, include_pattern) + + context_mock.assert_called_with(contextname=context_name, regex=include_pattern, apikey=self.api_key) + + @patch('zapv2.context.include_in_context') + def test_include_in_context_return_error(self, context_mock): + """Test that an error is raised when the API returns an unexpected response.""" + include_pattern = r"\/zapcli.+" + context_name = 'Test' + context_mock.return_value = 'Error' + + with self.assertRaises(ZAPError): + self.zap_helper.include_in_context(context_name, include_pattern) + + def test_include_in_context_regex_error(self): + """Test that an error is raised when an invalid regex is supplied.""" + include_pattern = '[' + context_name = 'Test' + + with self.assertRaises(ZAPError): + self.zap_helper.include_in_context(context_name, include_pattern) + + @patch('zapv2.context.exclude_from_context') + def test_exclude_from_context(self, context_mock): + """Test adding a regex for URLs to exclude from the context.""" + exclude_pattern = r"\/zapcli.+" + context_name = 'Test' + context_mock.return_value = 'OK' + + self.zap_helper.exclude_from_context(context_name, exclude_pattern) + + context_mock.assert_called_with(contextname=context_name, regex=exclude_pattern, apikey=self.api_key) + + @patch('zapv2.context.exclude_from_context') + def test_exclude_from_context_return_error(self, context_mock): + """Test that an error is raised when the API returns an unexpected response.""" + exclude_pattern = r"\/zapcli.+" + context_name = 'Test' + context_mock.return_value = 'Error' + + with self.assertRaises(ZAPError): + self.zap_helper.exclude_from_context(context_name, exclude_pattern) + + def test_exclude_from_context_regex_error(self): + """Test that an error is raised when an invalid regex is supplied.""" + exclude_pattern = '[' + context_name = 'Test' + + with self.assertRaises(ZAPError): + self.zap_helper.exclude_from_context(context_name, exclude_pattern) + + @patch('zapv2.context.import_context') + def test_import_context(self, context_mock): + """Test importing a context.""" + file_path = '/tmp/Test' + context_mock.return_value = '1' + + self.zap_helper.import_context(file_path) + + context_mock.assert_called_with(file_path, apikey=self.api_key) + + @patch('zapv2.context.import_context') + def test_import_context_return_error(self, context_mock): + """Test that an error is raised when the API returns an unexpected response.""" + file_path = '/tmp/Test' + context_mock.return_value = 'Error' + + with self.assertRaises(ZAPError): + self.zap_helper.import_context(file_path) + + @patch('zapv2.context.export_context') + def test_export_context(self, context_mock): + """Test exporting a context.""" + file_path = '/tmp/Test' + context_name = 'Test' + context_mock.return_value = 'OK' + + self.zap_helper.export_context(context_name, file_path) + + context_mock.assert_called_with(context_name, file_path, apikey=self.api_key) + + @patch('zapv2.context.export_context') + def test_export_context_return_error(self, context_mock): + """Test that an error is raised when the API returns an unexpected response.""" + file_path = '/tmp/Test' + context_name = 'Test' + context_mock.return_value = 'Error' + + with self.assertRaises(ZAPError): + self.zap_helper.export_context(context_name, file_path) + if __name__ == '__main__': unittest.main() diff --git a/zapcli/cli.py b/zapcli/cli.py index 1db5cbe..c83803f 100644 --- a/zapcli/cli.py +++ b/zapcli/cli.py @@ -8,6 +8,7 @@ from zapcli import __version__ from zapcli import helpers +from zapcli.commands.context import context_group from zapcli.commands.policies import policies_group from zapcli.commands.scanners import scanner_group from zapcli.commands.scripts import scripts_group @@ -103,12 +104,16 @@ def open_url(zap_helper, url): @cli.command('spider') @click.argument('url') +@click.option('--context-name', '-c', type=str, help='Context to use if provided.') +@click.option('--user-name', '-u', type=str, + help='User to run scan as if provided. If this option is used, the context parameter must also ' + + 'be provided.') @click.pass_obj -def spider_url(zap_helper, url): +def spider_url(zap_helper, url, context_name, user_name): """Run the spider against a URL.""" console.info('Running spider...') with helpers.zap_error_handler(): - zap_helper.run_spider(url) + zap_helper.run_spider(url, context_name, user_name) @cli.command('ajax-spider') @@ -127,8 +132,12 @@ def ajax_spider_url(zap_helper, url): 'subcommand to get a list of IDs. Available groups are: {0}.'.format( ', '.join(['all'] + list(ZAPHelper.scanner_group_map.keys())))) @click.option('--recursive', '-r', is_flag=True, default=False, help='Make scan recursive.') +@click.option('--context-name', '-c', type=str, help='Context to use if provided.') +@click.option('--user-name', '-u', type=str, + help='User to run scan as if provided. If this option is used, the context parameter must also ' + + 'be provided.') @click.pass_obj -def active_scan(zap_helper, url, scanners, recursive): +def active_scan(zap_helper, url, scanners, recursive, context_name, user_name): """ Run an Active Scan against a URL. @@ -141,7 +150,7 @@ def active_scan(zap_helper, url, scanners, recursive): if scanners: zap_helper.set_enabled_scanners(scanners) - zap_helper.run_active_scan(url, recursive=recursive) + zap_helper.run_active_scan(url, recursive, context_name, user_name) @cli.command('alerts') @@ -183,6 +192,10 @@ def show_alerts(zap_helper, alert_level, output_format, exit_code): ' e.g. "-config api.key=12345"') @click.option('--output-format', '-f', default='table', type=click.Choice(['table', 'json']), help='Output format to print the alerts.') +@click.option('--context-name', '-c', type=str, help='Context to use if provided.') +@click.option('--user-name', '-u', type=str, + help='User to run scan as if provided. If this option is used, the context parameter must also ' + + 'be provided.') @click.pass_obj def quick_scan(zap_helper, url, **options): """ @@ -209,12 +222,12 @@ def quick_scan(zap_helper, url, **options): zap_helper.open_url(url) if options['spider']: - zap_helper.run_spider(url) + zap_helper.run_spider(url, options['context_name'], options['user_name']) if options['ajax_spider']: zap_helper.run_ajax_spider(url) - zap_helper.run_active_scan(url, recursive=options['recursive']) + zap_helper.run_active_scan(url, options['recursive'], options['context_name'], options['user_name']) alerts = zap_helper.alerts(options['alert_level']) @@ -255,6 +268,7 @@ def report(zap_helper, output, output_format): # Add subcommand groups +cli.add_command(context_group) cli.add_command(policies_group) cli.add_command(scanner_group) cli.add_command(scripts_group) diff --git a/zapcli/commands/context.py b/zapcli/commands/context.py new file mode 100644 index 0000000..b237d13 --- /dev/null +++ b/zapcli/commands/context.py @@ -0,0 +1,120 @@ +""" +Group of commands to manage the contexts for the current session. + +.. moduleauthor:: Daniel Grunwell (grunny) +""" + +import click + +from zapcli.helpers import zap_error_handler +from zapcli.log import console + + +@click.group(name='context', short_help='Manage contexts for the current session.') +@click.pass_context +def context_group(ctx): + """Group of commands to manage the contexts for the current session.""" + pass + + +@context_group.command('list') +@click.pass_obj +def context_list(zap_helper): + """List the available contexts.""" + contexts = zap_helper.zap.context.context_list + if len(contexts): + console.info('Available contexts: {0}'.format(contexts[1:-1])) + else: + console.info('No contexts available in the current session') + + +@context_group.command('new') +@click.argument('name') +@click.pass_obj +def context_new(zap_helper, name): + """Create a new context.""" + console.info('Creating context with name: {0}'.format(name)) + res = zap_helper.new_context(name) + console.info('Context "{0}" created with ID: {1}'.format(name, res)) + + +@context_group.command('include') +@click.option('--name', '-n', type=str, required=True, + help='Name of the context.') +@click.option('--pattern', '-p', type=str, + help='Regex to include.') +@click.pass_obj +def context_include(zap_helper, name, pattern): + """Include a pattern in a given context.""" + console.info('Including regex {0} in context with name: {1}'.format(pattern, name)) + with zap_error_handler(): + zap_helper.include_in_context(name, pattern) + + +@context_group.command('exclude') +@click.option('--name', '-n', type=str, required=True, + help='Name of the context.') +@click.option('--pattern', '-p', type=str, + help='Regex to exclude.') +@click.pass_obj +def context_exclude(zap_helper, name, pattern): + """Exclude a pattern from a given context.""" + console.info('Excluding regex {0} from context with name: {1}'.format(pattern, name)) + with zap_error_handler(): + zap_helper.exclude_from_context(name, pattern) + + +@context_group.command('info') +@click.argument('context-name') +@click.pass_obj +def context_info(zap_helper, context_name): + """Get info about the given context.""" + with zap_error_handler(): + info = zap_helper.get_context_info(context_name) + + console.info('ID: {}'.format(info['id'])) + console.info('Name: {}'.format(info['name'])) + console.info('Authentication type: {}'.format(info['authType'])) + console.info('Included regexes: {}'.format(info['includeRegexs'])) + console.info('Excluded regexes: {}'.format(info['excludeRegexs'])) + + +@context_group.command('users') +@click.argument('context-name') +@click.pass_obj +def context_list_users(zap_helper, context_name): + """List the users available for a given context.""" + with zap_error_handler(): + info = zap_helper.get_context_info(context_name) + + users = zap_helper.zap.users.users_list(info['id']) + if len(users): + user_list = ', '.join([user['name'] for user in users]) + console.info('Available users for the context {0}: {1}'.format(context_name, user_list)) + else: + console.info('No users configured for the context {}'.format(context_name)) + + +@context_group.command('import') +@click.argument('file-path') +@click.pass_obj +def context_import(zap_helper, file_path): + """Import a saved context file.""" + with zap_error_handler(): + zap_helper.import_context(file_path) + + console.info('Imported context from {}'.format(file_path)) + + +@context_group.command('export') +@click.option('--name', '-n', type=str, required=True, + help='Name of the context.') +@click.option('--file-path', '-f', type=str, + help='Output file to export the context.') +@click.pass_obj +def context_export(zap_helper, name, file_path): + """Export a given context to a file.""" + with zap_error_handler(): + zap_helper.export_context(name, file_path) + + console.info('Exported context {0} to {1}'.format(name, file_path)) diff --git a/zapcli/zap_helper.py b/zapcli/zap_helper.py index 0ab813d..e65431b 100644 --- a/zapcli/zap_helper.py +++ b/zapcli/zap_helper.py @@ -38,6 +38,7 @@ class ZAPHelper(object): } timeout = 60 + _status_check_sleep = 10 def __init__(self, zap_path='', port=8090, url='http://127.0.0.1', api_key='', logger=None): if os.path.isfile(zap_path): @@ -132,11 +133,17 @@ def open_url(self, url, sleep_after_open=2): # Give the sites tree a chance to get updated time.sleep(sleep_after_open) - def run_spider(self, target_url, status_check_sleep=10): + def run_spider(self, target_url, context_name=None, user_name=None): """Run spider against a URL.""" self.logger.debug('Spidering target {0}...'.format(target_url)) - scan_id = self.zap.spider.scan(target_url, apikey=self.api_key) + context_id, user_id = self._get_context_and_user_ids(context_name, user_name) + + if user_id: + self.logger.debug('Running spider in context {0} as user {1}'.format(context_id, user_id)) + scan_id = self.zap.spider.scan_as_user(context_id, user_id, target_url, apikey=self.api_key) + else: + scan_id = self.zap.spider.scan(target_url, apikey=self.api_key) if not scan_id: raise ZAPError('Error running spider.') @@ -147,15 +154,21 @@ def run_spider(self, target_url, status_check_sleep=10): while int(self.zap.spider.status()) < 100: self.logger.debug('Spider progress %: {0}'.format(self.zap.spider.status())) - time.sleep(status_check_sleep) + time.sleep(self._status_check_sleep) self.logger.debug('Spider #{0} completed'.format(scan_id)) - def run_active_scan(self, target_url, recursive=False, status_check_sleep=10): + def run_active_scan(self, target_url, recursive=False, context_name=None, user_name=None): """Run an active scan against a URL.""" self.logger.debug('Scanning target {0}...'.format(target_url)) - scan_id = self.zap.ascan.scan(target_url, recurse=recursive, apikey=self.api_key) + context_id, user_id = self._get_context_and_user_ids(context_name, user_name) + + if user_id: + self.logger.debug('Scanning in context {0} as user {1}'.format(context_id, user_id)) + scan_id = self.zap.ascan.scan_as_user(target_url, context_id, user_id, recursive, apikey=self.api_key) + else: + scan_id = self.zap.ascan.scan(target_url, recurse=recursive, apikey=self.api_key) if not scan_id: raise ZAPError('Error running active scan.') @@ -168,11 +181,11 @@ def run_active_scan(self, target_url, recursive=False, status_check_sleep=10): while int(self.zap.ascan.status()) < 100: self.logger.debug('Scan progress %: {0}'.format(self.zap.ascan.status())) - time.sleep(status_check_sleep) + time.sleep(self._status_check_sleep) self.logger.debug('Scan #{0} completed'.format(scan_id)) - def run_ajax_spider(self, target_url, status_check_sleep=10): + def run_ajax_spider(self, target_url): """Run AJAX Spider against a URL.""" self.logger.debug('AJAX Spidering target {0}...'.format(target_url)) @@ -180,7 +193,7 @@ def run_ajax_spider(self, target_url, status_check_sleep=10): while self.zap.ajaxSpider.status == 'running': self.logger.debug('AJAX Spider: {0}'.format(self.zap.ajaxSpider.status)) - time.sleep(status_check_sleep) + time.sleep(self._status_check_sleep) self.logger.debug('AJAX Spider completed') @@ -399,3 +412,74 @@ def _write_report(report, file_path): if not isinstance(report, binary_type): report = report.encode('utf-8') f.write(report) + + def new_context(self, context_name): + """Create a new context with the given name.""" + return self.zap.context.new_context(contextname=context_name, apikey=self.api_key) + + def include_in_context(self, context_name, regex): + """Add include regex to context.""" + try: + re.compile(regex) + except re.error: + raise ZAPError('Invalid regex "{0}" provided'.format(regex)) + + result = self.zap.context.include_in_context(contextname=context_name, regex=regex, apikey=self.api_key) + + if result != 'OK': + raise ZAPError('Including regex from context failed: {}'.format(result)) + + def exclude_from_context(self, context_name, regex): + """Add exclude regex to context.""" + try: + re.compile(regex) + except re.error: + raise ZAPError('Invalid regex "{0}" provided'.format(regex)) + + result = self.zap.context.exclude_from_context(contextname=context_name, regex=regex, apikey=self.api_key) + + if result != 'OK': + raise ZAPError('Excluding regex from context failed: {}'.format(result)) + + def get_context_info(self, context_name): + """Get the context ID for a given context name.""" + context_info = self.zap.context.context(context_name) + if not isinstance(context_info, dict): + raise ZAPError('Context with name "{0}" wasn\'t found'.format(context_name)) + + return context_info + + def import_context(self, file_path): + """Import a context from a file.""" + result = self.zap.context.import_context(file_path, apikey=self.api_key) + + if not result.isdigit(): + raise ZAPError('Importing context from file failed: {}'.format(result)) + + def export_context(self, context_name, file_path): + """Export a given context to a file.""" + result = self.zap.context.export_context(context_name, file_path, apikey=self.api_key) + + if result != 'OK': + raise ZAPError('Exporting context to file failed: {}'.format(result)) + + def _get_context_and_user_ids(self, context_name, user_name): + """Helper to get the context ID and user ID from the given names.""" + if context_name is None: + return None, None + + context_id = self.get_context_info(context_name)['id'] + user_id = None + if user_name: + user_id = self._get_user_id_from_name(context_id, user_name) + + return context_id, user_id + + def _get_user_id_from_name(self, context_id, user_name): + """Get a user ID from the user name.""" + users = self.zap.users.users_list(context_id) + for user in users: + if user['name'] == user_name: + return user['id'] + + raise ZAPError('No user with the name "{0}"" was found for context {1}'.format(user_name, context_id)) From 428795ebb187a2223f9cca752ede0e082b853161 Mon Sep 17 00:00:00 2001 From: grunny Date: Thu, 2 Nov 2017 01:09:51 +1000 Subject: [PATCH 2/3] Upgrade python-owasp-zap-v2.4 dependency to 0.0.12 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 22b8f20..8efd143 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ ], install_requires=[ 'click==4.0', - 'python-owasp-zap-v2.4==0.0.11', + 'python-owasp-zap-v2.4==0.0.12', 'requests==2.13.0', 'tabulate==0.7.5', 'termcolor==1.1.0', From 399e21b514c9122f14d84e181f874c47d5ba1740 Mon Sep 17 00:00:00 2001 From: grunny Date: Thu, 2 Nov 2017 01:34:47 +1000 Subject: [PATCH 3/3] Add an explanation of how to run authenticated scans to the README --- README.rst | 32 ++++++++++++++++++++++++++++++++ zapcli/cli.py | 6 +++--- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 9c1055f..665949c 100644 --- a/README.rst +++ b/README.rst @@ -163,3 +163,35 @@ Or to run the same scan with the API key disabled: :: $ zap-cli quick-scan -sc -o '-config api.disablekey=true' -s xss http://127.0.0.1/ + +Running scans as authenticated users +------------------------------------ +In order to run a scan as an authenticated user, first configure the authentication method and users for +a context using the ZAP UI (see the `ZAP help page `_ +for more information). Once the authentication method and users are prepared, you can then export the context +with the configured authentication method so it can be imported and used to run authenticated scans with ZAP CLI. + +You can export a context with the authentication method and users configured either through the ZAP UI or using the +``context export`` ZAP CLI command. For example, to export a context with the name DevTest to a file, you could run: + +:: + + $ zap-cli context export --name DevTest --file-path /home/user/DevTest.context + +To import the saved context for use with ZAP CLI later, you could run: + +:: + + $ zap-cli context import /home/user/DevTest.context + +After importing the context with the configured authentication method and users, you can then provide the context name +and user name to the ``spider``, ``active-scan``, and ``quick-scan`` commands to run the scans while authenticated as +the given user. For example: + +:: + + $ zap-cli context import /home/user/DevTest.context + $ zap-cli open-url "http://localhost/" + $ zap-cli spider --context-name DevTest --user-name SomeUser "http://localhost" + $ zap-cli active-scan --recursive -c DevTest -u SomeUser "http://localhost" + $ zap-cli quick-scan --recursive --spider -c DevTest -u SomeUser "http://localhost" diff --git a/zapcli/cli.py b/zapcli/cli.py index c83803f..2336358 100644 --- a/zapcli/cli.py +++ b/zapcli/cli.py @@ -106,7 +106,7 @@ def open_url(zap_helper, url): @click.argument('url') @click.option('--context-name', '-c', type=str, help='Context to use if provided.') @click.option('--user-name', '-u', type=str, - help='User to run scan as if provided. If this option is used, the context parameter must also ' + + help='Run scan as this user if provided. If this option is used, the context parameter must also ' + 'be provided.') @click.pass_obj def spider_url(zap_helper, url, context_name, user_name): @@ -134,7 +134,7 @@ def ajax_spider_url(zap_helper, url): @click.option('--recursive', '-r', is_flag=True, default=False, help='Make scan recursive.') @click.option('--context-name', '-c', type=str, help='Context to use if provided.') @click.option('--user-name', '-u', type=str, - help='User to run scan as if provided. If this option is used, the context parameter must also ' + + help='Run scan as this user if provided. If this option is used, the context parameter must also ' + 'be provided.') @click.pass_obj def active_scan(zap_helper, url, scanners, recursive, context_name, user_name): @@ -194,7 +194,7 @@ def show_alerts(zap_helper, alert_level, output_format, exit_code): help='Output format to print the alerts.') @click.option('--context-name', '-c', type=str, help='Context to use if provided.') @click.option('--user-name', '-u', type=str, - help='User to run scan as if provided. If this option is used, the context parameter must also ' + + help='Run scan as this user if provided. If this option is used, the context parameter must also ' + 'be provided.') @click.pass_obj def quick_scan(zap_helper, url, **options):