├── core ├── __init__.py └── storage_service.py ├── scanners ├── __init__.py ├── scanner.py ├── zap_scanner.py ├── openvas_scanner.py └── nexpose_scanner.py ├── renovate.json ├── screenshots ├── screenshot_0.png ├── screenshot_1.png └── screenshot_2.png ├── .gitignore ├── setup.sh ├── .env.example ├── requirements.txt ├── .github └── ISSUE_TEMPLATE │ └── sweep-template.yml ├── main.py ├── sweep.yaml ├── test-openvas.py ├── README.md └── test-nexpose.py /core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scanners/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /screenshots/screenshot_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vs4vijay/ScanMaster/HEAD/screenshots/screenshot_0.png -------------------------------------------------------------------------------- /screenshots/screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vs4vijay/ScanMaster/HEAD/screenshots/screenshot_1.png -------------------------------------------------------------------------------- /screenshots/screenshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vs4vijay/ScanMaster/HEAD/screenshots/screenshot_2.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | 4 | __pycache__ 5 | .venv 6 | .env 7 | 8 | .vscode 9 | 10 | *.log 11 | 12 | scans.json 13 | 14 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "[+] Setting up env." 4 | 5 | echo "[+] Installing python dependencies" 6 | 7 | pip3 install -r requirements.txt -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Application Configuration 2 | 3 | DUPLICATE_MATCH_PERCENTAGE_THRESHOLD=20 4 | 5 | 6 | # ZAP Configuration 7 | 8 | ZAP_API_KEY='' 9 | 10 | 11 | # Nexpose Configuration 12 | 13 | NEXPOSE_HOST='https://localhost:3780' 14 | NEXPOSE_USERNAME='' 15 | NEXPOSE_PASSWORD='' 16 | 17 | 18 | # OpenVAS Configuration 19 | 20 | OPENVAS_SOCKET='/var/run/openvasmd.sock' 21 | OPENVAS_USERNAME= 22 | OPENVAS_PASSWORD= 23 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asn1crypto==0.24.0 2 | bcrypt==3.1.7 3 | beautifulsoup4==4.8.0 4 | certifi==2019.6.16 5 | cffi==1.12.3 6 | chardet==3.0.4 7 | cryptography==2.7 8 | defusedxml==0.6.0 9 | gvm-tools==2.0.0b1 10 | idna==2.8 11 | lxml==4.4.1 12 | nltk==3.4.5 13 | paramiko==2.6.0 14 | pprint==0.1 15 | pycparser==2.19 16 | PyNaCl==1.3.0 17 | python-dateutil==2.8.0 18 | python-dotenv==0.10.3 19 | python-gvm==1.0.0b3 20 | python-owasp-zap-v2.4==0.0.15 21 | rapid7-vm-console===1.0.0-6.5.50 22 | requests==2.22.0 23 | six==1.12.0 24 | soupsieve==1.9.2 25 | terminaltables==3.1.0 26 | tinydb==3.13.0 27 | urllib3==1.25.3 28 | xmltodict==0.12.0 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/sweep-template.yml: -------------------------------------------------------------------------------- 1 | name: Sweep Issue 2 | title: 'Sweep: ' 3 | description: For small bugs, features, refactors, and tests to be handled by Sweep, an AI-powered junior developer. 4 | labels: sweep 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Details 10 | description: Tell Sweep where and what to edit and provide enough context for a new developer to the codebase 11 | placeholder: | 12 | Unit Tests: Write unit tests for . Test each function in the file. Make sure to test edge cases. 13 | Bugs: The bug might be in . Here are the logs: ... 14 | Features: the new endpoint should use the ... class from because it contains ... logic. 15 | Refactors: We are migrating this function to ... version because ... -------------------------------------------------------------------------------- /core/storage_service.py: -------------------------------------------------------------------------------- 1 | from tinydb import TinyDB, Query 2 | import sys 3 | 4 | sys.path.append('.') 5 | 6 | class StorageService: 7 | 8 | STORAGE_LOCATION = 'scans.json' 9 | 10 | def __init__(self): 11 | self.db = TinyDB(self.STORAGE_LOCATION, indent=4, separators=(',', ': ')) 12 | 13 | def add(self, data, scanner='ZAP'): 14 | self.db.insert(data) 15 | 16 | def get_by_name(self, scan_name): 17 | return self.db.get(Query().scan_name == scan_name) 18 | 19 | def get_by_id(self, scan_id): 20 | return self.db.get(Query().scan_id == scan_id) 21 | 22 | def update_by_name(self, scan_name, data, scanner='ZAP'): 23 | # nexpose_id and site_id 24 | self.db.update(data, Query().scan_name == scan_name) 25 | 26 | def update_by_id(self, scan_id, data, scanner='ZAP'): 27 | self.db.update(data, Query().scan_id == scan_id) 28 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import time 5 | import logging 6 | import argparse 7 | 8 | from dotenv import load_dotenv, find_dotenv 9 | 10 | from scanners.zap_scanner import ZapScanner 11 | from scanners.nexpose_scanner import NexposeScanner 12 | from scanners.openvas_scanner import OpenVASScanner 13 | 14 | 15 | load_dotenv(find_dotenv()) 16 | 17 | logging.basicConfig(filename='scanner.log', level=logging.INFO) 18 | 19 | def main(config): 20 | 21 | print("hello") 22 | 23 | scanners = [ZapScanner(), NexposeScanner(), OpenVASScanner()] 24 | 25 | scan_results = {} 26 | scan_status_list = [] 27 | 28 | if config['target']: 29 | for scanner in scanners: 30 | scanner.start(config['scan_name'], config['target']) 31 | time.sleep(1) 32 | 33 | elif config['pause']: 34 | for scanner in scanners: 35 | scanner.pause(config['scan_name']) 36 | time.sleep(1) 37 | 38 | elif config['resume']: 39 | for scanner in scanners: 40 | scanner.resume(config['scan_name']) 41 | time.sleep(1) 42 | else: 43 | for scanner in scanners: 44 | scanner.get_scan_status(config.get('scan_name'), scan_status_list) 45 | scanner.get_scan_results(config.get('scan_name'), scan_results) 46 | time.sleep(2) 47 | 48 | scanner.print_scan_status(scan_status_list) 49 | scanner.print_report(scan_results) 50 | 51 | return True 52 | 53 | 54 | if __name__ == '__main__': 55 | parser = argparse.ArgumentParser() 56 | parser.add_argument('-s', '--scan-name', required=True, help='Specify a scan name') 57 | parser.add_argument('-i', '--scan-id', help='Specify the scan id', type=int) 58 | parser.add_argument('-t', '--target', help='Specify the Target URL or IP') 59 | parser.add_argument('-p', '--pause', action='store_true', help='Pause a specified scan') 60 | parser.add_argument('-r', '--resume', action='store_true', help='Resume a specified scan') 61 | parser.add_argument('-v', '--version', action='version', version='MultiScanner 1.0') 62 | args = parser.parse_args() 63 | 64 | config = { 65 | 'scan_name': args.scan_name, 66 | 'target': args.target, 67 | 'pause': args.pause, 68 | 'resume': args.resume 69 | } 70 | 71 | main(config) 72 | 73 | exit(0) -------------------------------------------------------------------------------- /scanners/scanner.py: -------------------------------------------------------------------------------- 1 | 2 | import re 3 | 4 | from terminaltables import SingleTable, DoubleTable 5 | 6 | class Scanner: 7 | 8 | def __init__(): 9 | pass 10 | 11 | def scan(self): 12 | pass 13 | 14 | def get_scan_status(self): 15 | pass 16 | 17 | def get_scan_results(self): 18 | pass 19 | 20 | def is_valid_scan(self): 21 | pass 22 | 23 | def list_scans(self): 24 | pass 25 | 26 | def pause(self): 27 | pass 28 | 29 | def resume(self): 30 | pass 31 | 32 | def stop(self): 33 | pass 34 | 35 | def _get_address(self, target): 36 | return re.sub('http[s]*://', '', target) 37 | 38 | def _process_for_duplicates(self, scan_results): 39 | return scan_results 40 | 41 | def print_scan_status(self, scan_status_list): 42 | status = [] 43 | status.append([ '#', 'Scanner', 'Status']) 44 | count = 0 45 | 46 | for scan_status in scan_status_list: 47 | count += 1 48 | status.append([ count, scan_status['scanner'], scan_status['status'] ]) 49 | 50 | status_table = DoubleTable(status) 51 | status_table.title = 'Scan Status' 52 | print(status_table.table) 53 | 54 | def print_report(self, scan_results): 55 | 56 | if not scan_results: 57 | return False 58 | 59 | results = list(scan_results.values()) 60 | 61 | scan_report = [] 62 | scan_report.append([ '#', 'Vuln. Name', 'Risk', 'Severity', 'CVE/CWE ID', 'URLs', 'Desc.', 'Sol.', 'Scanner' ]) 63 | 64 | count = 0 65 | for vuln in sorted(results, key = lambda x: x['severity'], reverse=True): 66 | count += 1 67 | 68 | name = vuln['name'] 69 | risk = vuln['risk'] 70 | severity = vuln['severity'] 71 | cve_id = vuln.get('cweid') or vuln.get('cveid', '') 72 | urls = list(vuln.get('urls', [])) 73 | description = vuln['description'] 74 | solution = vuln['solution'] 75 | reported_by = vuln['reported_by'] 76 | 77 | urls = f'({len(urls)} URLs) {urls[0]}' if urls else '' 78 | 79 | scan_report.append([ count, name, risk, severity, cve_id, urls, 'desc', 'sol', reported_by ]) 80 | 81 | scan_report_table = SingleTable(scan_report) 82 | scan_report_table.title = 'Vuln. Alerts' 83 | print(scan_report_table.table) 84 | -------------------------------------------------------------------------------- /sweep.yaml: -------------------------------------------------------------------------------- 1 | # Sweep AI turns bugs & feature requests into code changes (https://sweep.dev) 2 | # For details on our config file, check out our docs at https://docs.sweep.dev/usage/config 3 | 4 | # This setting contains a list of rules that Sweep will check for. If any of these rules are broken in a new commit, Sweep will create an pull request to fix the broken rule. 5 | rules: 6 | - "All docstrings and comments should be up to date." 7 | - "Code should be properly indented and follow consistent indentation style." 8 | - "Variable and function names should be descriptive and follow a consistent naming convention." 9 | - "There should be no commented out code or unused code in the repository." 10 | - "Code should be organized into logical modules and classes." 11 | - "There should be no hard-coded values in the code. Use constants or configuration files instead." 12 | 13 | # This is the branch that Sweep will develop from and make pull requests to. Most people use 'main' or 'master' but some users also use 'dev' or 'staging'. 14 | branch: 'main' 15 | 16 | # By default Sweep will read the logs and outputs from your existing Github Actions. To disable this, set this to false. 17 | gha_enabled: True 18 | 19 | # This is the description of your project. It will be used by sweep when creating PRs. You can tell Sweep what's unique about your project, what frameworks you use, or anything else you want. 20 | # 21 | # Example: 22 | # 23 | # description: sweepai/sweep is a python project. The main api endpoints are in sweepai/api.py. Write code that adheres to PEP8. 24 | description: '' 25 | 26 | # This sets whether to create pull requests as drafts. If this is set to True, then all pull requests will be created as drafts and GitHub Actions will not be triggered. 27 | draft: False 28 | 29 | # This is a list of directories that Sweep will not be able to edit. 30 | blocked_dirs: [] 31 | 32 | # This is a list of documentation links that Sweep will use to help it understand your code. You can add links to documentation for any packages you use here. 33 | # 34 | # Example: 35 | # 36 | # docs: 37 | # - PyGitHub: ["https://pygithub.readthedocs.io/en/latest/", "We use pygithub to interact with the GitHub API"] 38 | docs: [] 39 | 40 | # Sandbox executes commands in a sandboxed environment to validate code changes after every edit to guarantee pristine code. For more details, see the [Sandbox](./sandbox) page. 41 | sandbox: 42 | install: 43 | - trunk init 44 | check: 45 | - trunk fmt {file_path} || return 0 46 | - trunk check --fix --print-failures {file_path} 47 | -------------------------------------------------------------------------------- /test-openvas.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | from xml.etree import ElementTree 5 | 6 | import xmltodict 7 | 8 | from gvm.connections import UnixSocketConnection 9 | from gvm.protocols.latest import Gmp 10 | from gvm.transforms import EtreeTransform 11 | from gvm.xml import pretty_print 12 | 13 | 14 | 15 | def _process_results(report_response, scan_results={}): 16 | report_response_str = ElementTree.tostring(report_response, encoding='unicode') 17 | report_response_dict = xmltodict.parse(report_response_str) 18 | 19 | 20 | # print(json.dumps(report_results, indent=2)) 21 | 22 | 23 | report_results = report_response_dict.get('get_reports_response', {}).get('report', {}).get('report', {}).get('results', {}).get('result', []) 24 | 25 | print(json.dumps(report_results, indent=2)) 26 | 27 | print('report_results', report_results) 28 | 29 | for vuln in report_results: 30 | name = vuln.get('name') 31 | print('name: ', name) 32 | if scan_results.get(name): 33 | print('-------- Dup title', name) 34 | continue 35 | nvt = vuln.get('nvt', {}) 36 | scan_result = {} 37 | scan_result['name'] = name 38 | scan_result['severity'] = float(nvt.get('cvss_base', 0)) 39 | scan_result['risk'] = vuln.get('threat') 40 | scan_result['cve_id'] = nvt.get('cve', 'N/A') if nvt.get('cve') != 'NOCVE' else 'N/A' 41 | scan_result['description'] = '' # vuln.get('description') 42 | scan_result['solution'] = 'N/A' 43 | scan_result['reported_by'] = 'OpenVAS' 44 | scan_results[name] = scan_result 45 | return scan_results 46 | 47 | 48 | 49 | connection = UnixSocketConnection(path='/var/run/openvasmd.sock') 50 | transform = EtreeTransform() 51 | gmp = Gmp(connection, transform=transform) 52 | 53 | # Retrieve GMP version supported by the remote daemon 54 | version = gmp.get_version() 55 | 56 | # Prints the XML in beautiful form 57 | pretty_print(version) 58 | 59 | # Login 60 | gmp.authenticate('admin', 'admin') 61 | 62 | 63 | name = 'name-5' 64 | ip_address = 'scanme.nmap.org' 65 | # ip_address = 'webscantest.com' 66 | # ip_address = 'slack.com' 67 | 68 | 69 | # response = gmp.create_target(name=name, hosts=[ip_address]) 70 | # pretty_print(response) 71 | # target_id = response.get('id') 72 | # print('target_id: ', target_id) 73 | # target_id = 'b7b3b26d-5e19-482c-a1b5-d5c46b89edaa' 74 | target_id = '69ca3c65-af09-48b8-bb3a-59e2e6cccb96' 75 | 76 | scan_config_id = 'daba56c8-73ec-11df-a475-002264764cea' 77 | scanner_id = '08b69003-5fc2-4037-a479-93b440211c73' 78 | 79 | # response = gmp.create_task(name=name, config_id=scan_config_id, target_id=target_id, scanner_id=scanner_id) 80 | # task_id = response.get('id') 81 | 82 | 83 | # response = gmp.start_task(task_id) 84 | # report_id = response[0].text 85 | 86 | # print('report_id', report_id) 87 | 88 | report_id = '79bf4984-9a0e-435a-807b-9dd530fb532f' 89 | 90 | report_format_id = 'a994b278-1f62-11e1-96ac-406186ea4fc5' 91 | 92 | 93 | report_response = gmp.get_report(report_id=report_id, report_format_id=report_format_id) 94 | # pretty_print(report_response) 95 | 96 | scan_results={} 97 | _process_results(report_response, scan_results) 98 | 99 | print('scan_results: ') 100 | print(scan_results) 101 | 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ScanMaster 2 | 3 | A security tool designed to perform thorough scans on a target using OpenVAS, Zap, and Nexpose. It seamlessly consolidates and integrates the scan results, providing a comprehensive overview of the security vulnerabilities identified. 4 | 5 | --- 6 | 7 | ## Usage 8 | 9 | ### Start a scan against a Target 10 | 11 | `./main.py --scan-name --target ` 12 | 13 | 14 | ### Get scan result 15 | 16 | `./main.py --scan-name ` 17 | 18 | 19 | ### Pause/Resume a scan result 20 | 21 | - `./main.py --scan-name --pause` 22 | - `./main.py --scan-name --resume` 23 | 24 | --- 25 | 26 | ## Screenshots 27 | 28 | Scanner Options: 29 | 30 | ![Scanner Options](/screenshots/screenshot_0.png) 31 | 32 | Scanner Report: 33 | 34 | ![Scanner Report](/screenshots/screenshot_1.png) 35 | 36 | Final Output: 37 | 38 | ![Final Output](/screenshots/screenshot_2.png) 39 | 40 | --- 41 | 42 | ## Prerequisites 43 | 44 | - Python 3 45 | - Zap 46 | - Nexpose 47 | - OpenVAS 48 | 49 | --- 50 | 51 | ## Installation 52 | 53 | `pip3 install -r requirements.txt` 54 | 55 | OR 56 | 57 | Run in Virtual Env: 58 | 59 | ```console 60 | python3 -m venv .venv 61 | 62 | source .venv/bin/activate 63 | 64 | pip3 install -r requirements.txt 65 | ``` 66 | 67 | --- 68 | 69 | ## Configuration 70 | 71 | The configuration of scanners will be in Environment File `.env`. There is sample `.env.example` file in the codebase, update the values with the proper API Keys and Credentials details before using. Rename it to `.env`. 72 | 73 | --- 74 | 75 | ## Targets to Test 76 | - http://scanme.nmap.org 77 | - http://webscantest.com 78 | 79 | --- 80 | 81 | ## ToDo 82 | - [ ] Dockerize 83 | - [ ] Add Nessus 84 | - [ ] Error Stack 85 | - [ ] auto reload 86 | - [ ] Remove logs 87 | - [ ] Save to CSV 88 | - [ ] Make it interactive 89 | - [ ] OOPs 90 | - [ ] Improve Scan Results and Output 91 | - [ ] Color logging 92 | 93 | --- 94 | 95 | ### Scanner Interface: 96 | 97 | - start 98 | - scan 99 | - get_scan_status 100 | - get_scan_results 101 | - is_valid_scan 102 | - list_scans 103 | - pause 104 | - resume 105 | - stop 106 | 107 | 108 | ### Development Notes 109 | 110 | ```python3 111 | 112 | pprint(core.htmlreport()) 113 | 114 | 115 | # address = rapid7vmconsole.Address(ip=target) 116 | # asset = rapid7vmconsole.Asset(addresses=[address]) 117 | 118 | 119 | scan_targets = rapid7vmconsole.IncludedScanTargets(addresses=[target]) 120 | 121 | asset = rapid7vmconsole.StaticSite(included_targets=scan_targets) 122 | 123 | scan_scope = rapid7vmconsole.ScanScope(assets=asset) 124 | 125 | site_create_resource = rapid7vmconsole.SiteCreateResource(name=scan_name, scan=scan_scope) 126 | 127 | site = self.nexpose_site.create_site(site=site_create_resource) 128 | 129 | print('Site Created', site) 130 | 131 | adhoc_scan = rapid7vmconsole.AdhocScan(hosts=[target]) 132 | print('adhoc_scan', adhoc_scan) 133 | 134 | site_id = site.id 135 | 136 | scan = self.nexpose.start_scan(site_id, scan=adhoc_scan) 137 | print('start scan response id', scan.id) 138 | # scan['vulnerabilities'] 139 | pprint(scan) 140 | 141 | if shutdownOnceFinished: 142 | # Shutdown ZAP once finished 143 | pprint('Shutdown ZAP -> ' + core.shutdown()) 144 | 145 | report_config_scope = rapid7vmconsole.ReportConfigScopeResource(scan=nexpose_id) 146 | 147 | report_config_categories = rapid7vmconsole.ReportConfigCategoryFilters(included=[]) 148 | 149 | report_config_filters = rapid7vmconsole.ReportConfigFiltersResource(categories=report_config_categories) 150 | 151 | report_config = rapid7vmconsole.Report(name=f'{scan_name}-Report', template='audit-report', format='csv-export', scope=report_config_scope) 152 | 153 | report_config = rapid7vmconsole.Report(name=f'{scan_name}-Report', format='sql-query', query='select * from dim_asset', version='2.3.0') 154 | 155 | report_config = rapid7vmconsole.Report(name=f'{scan_name}-SampleXML-Report', format='nexpose-simple-xml', scope=report_config_scope) 156 | report = nexpose_report.create_report(report=report_config) 157 | report_instance = nexpose_report.generate_report(report.id) 158 | nexpose_report.download_report(report.id, report_instance.id) 159 | 160 | 161 | 162 | report_config = rapid7vmconsole.Report(name=f'{scan_name}-sml2-Report', format='xml-export-v2', scope=report_config_scope) 163 | report = nexpose_report.create_report(report=report_config) 164 | report_instance = nexpose_report.generate_report(report.id) 165 | dd = nexpose_report.download_report(report.id, report_instance.id) 166 | 167 | 168 | report_config = rapid7vmconsole.Report(name=f'{scan_name}-html-Report', format='html', template='audit-report', scope=report_config_scope) 169 | report = nexpose_report.create_report(report=report_config) 170 | report_instance = nexpose_report.generate_report(report.id) 171 | dd = nexpose_report.download_report(report.id, report_instance.id) 172 | 173 | 174 | report_config.id = 42 175 | report_config.timezone = 'Asia/Calcutta' 176 | 177 | report_config.language = 'en-US' 178 | report_config.owner = 1 179 | report_config.organization = 'Organization' 180 | 181 | # report_config.component = 'Component' 182 | # report_config.email = rapid7vmconsole.ReportEmail(additional_recipients=['asd@asd.asd']) 183 | 184 | 185 | # print('self.zap.spider.results', self.zap.spider.results(scan_id)) 186 | 187 | 188 | 189 | 190 | # Retrieve all tasks 191 | tasks = gmp.get_tasks() 192 | 193 | # Get names of tasks 194 | task_names = tasks.xpath('task/name/text()') 195 | pretty_print(task_names) 196 | ``` 197 | -------------------------------------------------------------------------------- /test-nexpose.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import base64 4 | 5 | import rapid7vmconsole 6 | from bs4 import BeautifulSoup 7 | from terminaltables import AsciiTable, SingleTable, DoubleTable 8 | 9 | 10 | config = { 11 | 'HOST': 'https://life.do:3780', 12 | 'USERNAME': '', 13 | 'PASSWORD': '' 14 | } 15 | 16 | nexpose_config = rapid7vmconsole.Configuration(name='Scanner') 17 | nexpose_config.username = config['USERNAME'] 18 | nexpose_config.password = config['PASSWORD'] 19 | nexpose_config.host = config['HOST'] 20 | nexpose_config.assert_hostname = False 21 | nexpose_config.verify_ssl = False 22 | nexpose_config.ssl_ca_cert = None 23 | nexpose_config.connection_pool_maxsize = None 24 | nexpose_config.proxy = None 25 | nexpose_config.cert_file = None 26 | nexpose_config.key_file = None 27 | nexpose_config.safe_chars_for_path_param = '' 28 | 29 | auth = f'{config["USERNAME"]}:{config["PASSWORD"]}' 30 | auth = base64.b64encode(auth.encode('ascii')).decode() 31 | api_client = rapid7vmconsole.ApiClient(configuration=nexpose_config) 32 | api_client.default_headers['Authorization'] = f'Basic {auth}' 33 | nexpose_report = rapid7vmconsole.ReportApi(api_client) 34 | nexpose_site = rapid7vmconsole.SiteApi(api_client) 35 | 36 | scan_name='sixyfive7' 37 | nexpose_id=1 38 | 39 | report_config_scope = rapid7vmconsole.ReportConfigScopeResource(scan=nexpose_id) 40 | 41 | ### HTML 42 | # report_config = rapid7vmconsole.Report(name=f'{scan_name}-html-Report', format='html', template='audit-report', scope=report_config_scope) 43 | # report = nexpose_report.create_report(report=report_config) 44 | # report_instance = nexpose_report.generate_report(report.id) 45 | # hh = nexpose_report.download_report(report.id, report_instance.id) 46 | 47 | ### XML 48 | # report_config = rapid7vmconsole.Report(name=f'{scan_name}-xml-Report', format='xml-export', scope=report_config_scope) 49 | # print('report_config', report_config) 50 | 51 | # report = nexpose_report.create_report(report=report_config) 52 | # print('report', report) 53 | 54 | # report_instance = nexpose_report.generate_report(report.id) 55 | # print('report_instance', report_instance) 56 | 57 | # print('report.id, report_instance.id', report.id, report_instance.id) 58 | xx = nexpose_report.download_report(23, 26) 59 | 60 | # xx = nexpose_report.download_report(12, 1) 61 | 62 | # soup = BeautifulSoup(xx, 'xml.parser') 63 | soup = BeautifulSoup(xx, features='xml') 64 | 65 | print(soup) 66 | 67 | 68 | scan_data_map = {} 69 | scan_data = [] 70 | scan_data.append([ '#', 'Name', 'Risk', 'Severity', 'CVE/CWE ID','URLs', 'Solution' ]) 71 | count = 0 72 | 73 | for vuln in soup.find_all('vulnerability'): 74 | title = vuln.get('title') 75 | 76 | if scan_data_map.get(title): 77 | pass 78 | 79 | count += 1 80 | 81 | cvss_score = vuln.get('cvssScore') 82 | pci_severity = vuln.get('pciSeverity') 83 | severity = vuln.get('severity') 84 | 85 | if vuln.references.find('reference', source='CVE'): 86 | cve = vuln.references.find('reference', source='CVE').text 87 | else: 88 | cve = '' 89 | 90 | if vuln.description.ContainerBlockElement.Paragraph: 91 | description = vuln.description.ContainerBlockElement.Paragraph.text 92 | else: 93 | description = '' 94 | 95 | if vuln.solution.ContainerBlockElement.Paragraph: 96 | solution = vuln.solution.ContainerBlockElement.Paragraph.text 97 | else: 98 | solution = '' 99 | 100 | scan_data.append([count, title, cve, cvss_score, 'description', pci_severity, severity, 'solution']) 101 | 102 | 103 | 104 | # print('scan_data', scan_data) 105 | 106 | report_table = SingleTable(scan_data) 107 | report_table.title = 'Alerts' 108 | print(report_table.table) 109 | 110 | 111 | 112 | ### SQL 113 | # report_config = rapid7vmconsole.Report(name=f'{scan_name}-sql-Report', format='sql-query', query='select * from dim_asset', version='2.3.0', scope=report_config_scope) 114 | # report = nexpose_report.create_report(report=report_config) 115 | # report_instance = nexpose_report.generate_report(report.id) 116 | # ss = nexpose_report.download_report(report.id, report_instance.id) 117 | 118 | # ### CSV 119 | # report_config_scope = rapid7vmconsole.ReportConfigScopeResource(sites=[2]) 120 | 121 | # report_config = rapid7vmconsole.Report(name=f'{scan_name}-csv-Report', format='csv-export', template='audit-report', scope=report_config_scope) 122 | # report = nexpose_report.create_report(report=report_config) 123 | # report_instance = nexpose_report.generate_report(report.id) 124 | # cc = nexpose_report.download_report(report.id, report_instance.id) 125 | 126 | 127 | # asset_api = rapid7vmconsole.AssetApi(api_client) 128 | # assets = asset_api.get_assets() 129 | 130 | # for a in assets.resources: 131 | # print("Asset ID: %s; Hostname: %s; IP Address: %s" % (a.id, a.host_name, a.ip)) 132 | 133 | 134 | 135 | aa = [{'format': 'pdf', 136 | 'templates': ['audit-report', 137 | 'executive-overview', 138 | 'prioritized-remediations', 139 | 'prioritized-remediations-with-details', 140 | 'r7-discovered-assets', 141 | 'r7-vulnerability-exceptions', 142 | 'top-riskiest-assets', 143 | 'top-vulnerable-assets', 144 | 'vulnerability-trends']}, 145 | {'format': 'html', 146 | 'templates': ['audit-report', 147 | 'executive-overview', 148 | 'prioritized-remediations', 149 | 'prioritized-remediations-with-details', 150 | 'r7-discovered-assets', 151 | 'r7-vulnerability-exceptions', 152 | 'top-riskiest-assets', 153 | 'top-vulnerable-assets', 154 | 'vulnerability-trends']}, 155 | {'format': 'nexpose-simple-xml', 'templates': None}, 156 | {'format': 'xml-export', 'templates': None}, 157 | {'format': 'xml-export-v2', 'templates': None}] 158 | 159 | 160 | # print('HTML report:') 161 | # pprint(self.zap.core.htmlreport()) 162 | 163 | # print('JSON report:') 164 | # pprint(self.zap.core.jsonreport()) 165 | 166 | # with open('report.xml', 'w') as f: 167 | # f.write(self.zap.core.xmlreport()) 168 | 169 | # with open('report.html', 'w') as f: 170 | # f.write(self.zap.core.htmlreport()) 171 | 172 | 173 | 174 | # print(self.zap.spider.scanProgress(scan_id)) 175 | # print(self.zap.ascan.scanProgress(scan_id)) -------------------------------------------------------------------------------- /scanners/zap_scanner.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | 5 | 6 | from pprint import pprint 7 | from zapv2 import ZAPv2 8 | from dotenv import load_dotenv, find_dotenv 9 | 10 | from .scanner import Scanner 11 | from core.storage_service import StorageService 12 | 13 | load_dotenv(find_dotenv()) 14 | 15 | API_KEY=os.getenv('ZAP_API_KEY') 16 | SLEEP_INTERVAL=2 17 | 18 | class ZapScanner(Scanner): 19 | 20 | name = 'ZAP' 21 | 22 | RISK_SEVERITY_MAP = { 23 | 'Low': 2.0, 24 | 'Medium': 5.49, 25 | 'High': 8.5 26 | } 27 | 28 | def __init__(self): 29 | self.zap = ZAPv2(apikey=API_KEY) 30 | self.storage_service = StorageService() 31 | 32 | def start(self, scan_name, target): 33 | print(f'[{self.name}] Starting Scan for Target: {target}') 34 | 35 | try: 36 | return self.scan(scan_name, target) 37 | except: 38 | print(f'[{self.name}] Not able to connect to the {self.name}: ', sys.exc_info()) 39 | return False 40 | 41 | def pause(self, scan_name): 42 | if not self.is_valid_scan(scan_name): 43 | return False 44 | 45 | scan = self.storage_service.get_by_name(scan_name) 46 | self.zap.spider.pause(scan['scan_id']) 47 | print(f'[{self.name}] Spider Scan Paused') 48 | self.zap.ascan.pause(scan['scan_id']) 49 | print(f'[{self.name}] Active Scan Paused') 50 | 51 | # self.storage_service.update_by_name(scan_name, { status: 'PAUSED' }) 52 | return scan 53 | 54 | def resume(self, scan_name): 55 | if not self.is_valid_scan(scan_name): 56 | return False 57 | 58 | scan = self.storage_service.get_by_name(scan_name) 59 | self.zap.spider.resume(scan['scan_id']) 60 | print(f'[{self.name}] Spider Scan Resumed') 61 | self.zap.ascan.resume(scan['scan_id']) 62 | print(f'[{self.name}] Active Scan Resumed') 63 | 64 | # self.storage_service.update_by_name(scan_name, { status: 'RESUMED' }) 65 | return scan 66 | 67 | def stop(self, scan_name): 68 | if not self.is_valid_scan(scan_name): 69 | return False 70 | 71 | scan = self.storage_service.get_by_name(scan_name) 72 | self.zap.spider.stop(scan['scan_id']) 73 | print(f'[{self.name}] Spider Scan Stopped') 74 | self.zap.ascan.stop(scan['scan_id']) 75 | print(f'[{self.name}] Active Scan Stopped') 76 | 77 | # self.storage_service.update_by_name(scan_name, { status: 'STOPPED' }) 78 | return scan 79 | 80 | def remove(self, scan_name): 81 | if not self.is_valid_scan(scan_name): 82 | return False 83 | 84 | scan = self.storage_service.get_by_name(scan_name) 85 | self.zap.spider.removeScan(scan['scan_id']) 86 | print(f'[{self.name}] Spider Scan Removed') 87 | self.zap.ascan.removeScan(scan['scan_id']) 88 | print(f'[{self.name}] Active Scan Removed') 89 | return scan['scan_id'] 90 | 91 | def scan(self, scan_name, target): 92 | print(f'[{self.name}] Starting Scan: {scan_name}') 93 | 94 | self.zap.urlopen(target) 95 | time.sleep(SLEEP_INTERVAL) 96 | 97 | scan_id = self.zap.spider.scan(target) 98 | scan_id = int(scan_id) 99 | print(f'[{self.name}] Scan Started: {scan_id}') 100 | time.sleep(SLEEP_INTERVAL) 101 | 102 | # self.zap.ascan.enable_all_scanners() 103 | active_scan_id = self.zap.ascan.scan(target) 104 | # a_scan_id = self.zap.ascan.scan(target, recurse=True, inscopeonly=None, scanpolicyname=None, method=None, postdata=True) 105 | 106 | scan_data = self.storage_service.get_by_name(scan_name) 107 | 108 | if not scan_data: 109 | scan_data = { 110 | 'scan_name': scan_name, 111 | 'scan_id': '', 112 | 'target': target, 113 | 'status': '' 114 | } 115 | self.storage_service.add(scan_data) 116 | 117 | scan_data['ZAP'] = { 118 | 'zap_id': scan_id, 119 | 'active_scan_id': active_scan_id, 120 | 'scan_status': { 121 | 'status': 'INPROGRESS' 122 | } 123 | } 124 | self.storage_service.update_by_name(scan_name, scan_data) 125 | 126 | return scan_data 127 | 128 | 129 | def get_scan_status(self, scan_name, scan_status_list=[]): 130 | 131 | if not self.is_valid_scan(scan_name): 132 | return False 133 | 134 | scan_data = self.storage_service.get_by_name(scan_name) 135 | scan_status = scan_data.get('ZAP', {}).get('scan_status', {}) 136 | zap_id = scan_data.get('ZAP', {})['zap_id'] 137 | target = scan_data['target'] 138 | 139 | print(f'[{self.name}] Getting Scan Status for Target: {target}') 140 | print(f'[{self.name}] Scan Name: {scan_name}') 141 | print(f'[{self.name}] Scan Id: {zap_id}') 142 | 143 | spider_scan_status = self.zap.spider.status(zap_id) 144 | passive_scan_records_pending = self.zap.pscan.records_to_scan 145 | active_scan_status = self.zap.ascan.status(zap_id) 146 | 147 | scan_status['spider_scan'] = self._parse_status(spider_scan_status) 148 | scan_status['active_scan'] = self._parse_status(active_scan_status) 149 | scan_status['passive_scan'] = { 150 | 'scan_pending': int(passive_scan_records_pending), 151 | 'status': 'COMPLETE' if int(passive_scan_records_pending) == 0 else 'INPROGRESS' 152 | } 153 | 154 | scan_data['ZAP']['scan_status'] = scan_status 155 | 156 | self.storage_service.update_by_name(scan_name, scan_data) 157 | 158 | for scan_type in ['spider_scan', 'passive_scan', 'active_scan']: 159 | scan_type_data = scan_status.get(scan_type) 160 | if scan_type_data: 161 | scan_status_list.append({ 162 | 'scanner': f'{self.name} ({scan_type})', 163 | 'status': f'{scan_type_data["status"]} ({scan_type_data.get("progress")})' 164 | }) 165 | 166 | return scan_status_list 167 | 168 | def get_scan_results(self, scan_name, scan_results={}): 169 | 170 | if not self.is_valid_scan(scan_name): 171 | return False 172 | 173 | scan_data = self.storage_service.get_by_name(scan_name) 174 | 175 | target = scan_data['target'] 176 | 177 | alerts = self.zap.core.alerts(baseurl=target) 178 | # print (f'Alerts: {len(alerts)}') 179 | 180 | self._process_alerts(alerts, scan_results) 181 | 182 | return scan_results 183 | 184 | def _parse_status(self, status): 185 | if status == 'does_not_exist': 186 | progress = 0 187 | status = 'NOT_STARTED' 188 | else: 189 | progress = int(status) 190 | status = 'COMPLETE' if progress == 100 else 'INPROGRESS' 191 | 192 | data = { 193 | 'progress': f'{progress}%', 194 | 'status': status 195 | } 196 | return data 197 | 198 | def list_scans(self): 199 | scans = self.zap.ascan.scans() 200 | print(f'[{self.name}] Scans:', len(scans)) 201 | 202 | def is_valid_scan(self, scan_name): 203 | 204 | scan_data = self.storage_service.get_by_name(scan_name) 205 | if not scan_data: 206 | print(f'[{self.name}] Invalid Scan Name: {scan_name}') 207 | return False 208 | 209 | if not scan_data.get('ZAP'): 210 | print(f'[{self.name}] No Scan Details found for {scan_name}') 211 | return False 212 | 213 | zap_id = scan_data['ZAP']['zap_id'] 214 | 215 | try: 216 | ascan_status = self.zap.ascan.status(zap_id) 217 | except: 218 | print(f'[{self.name}] Could not get the scan {zap_id}: ', sys.exc_info()) 219 | return False 220 | 221 | if(ascan_status == 'does_not_exist'): 222 | print(f'[{self.name}] No Scans found for Scan Id {zap_id}') 223 | return False 224 | 225 | return True 226 | 227 | def _process_alerts(self, alerts, scan_results): 228 | 229 | for alert in alerts: 230 | name = alert['name'] 231 | if scan_results.get(name) is None: 232 | alert['reported_by'] = self.name 233 | alert['urls'] = set([ alert['url'] ]) 234 | alert['severity'] = self.RISK_SEVERITY_MAP.get(alert['risk']) 235 | scan_results[name] = alert 236 | else: 237 | scan_results[name]['urls'].add(alert['url']) 238 | 239 | return scan_results 240 | -------------------------------------------------------------------------------- /scanners/openvas_scanner.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import json 5 | 6 | import xmltodict 7 | from xml.etree import ElementTree 8 | from gvm.connections import UnixSocketConnection 9 | from gvm.protocols.latest import Gmp 10 | from gvm.transforms import EtreeTransform 11 | from gvm.xml import pretty_print 12 | from dotenv import load_dotenv, find_dotenv 13 | 14 | from .scanner import Scanner 15 | from core.storage_service import StorageService 16 | 17 | load_dotenv(find_dotenv()) 18 | 19 | config = { 20 | 'OPENVAS_SOCKET': os.getenv('OPENVAS_SOCKET'), 21 | 'OPENVAS_USERNAME': os.getenv('OPENVAS_USERNAME'), 22 | 'OPENVAS_PASSWORD': os.getenv('OPENVAS_PASSWORD'), 23 | 'REPORT_FORMAT_ID': 'a994b278-1f62-11e1-96ac-406186ea4fc5', 24 | 'SCAN_CONFIG_ID': 'daba56c8-73ec-11df-a475-002264764cea', 25 | 'SCANNER_ID': '08b69003-5fc2-4037-a479-93b440211c73' 26 | } 27 | 28 | class OpenVASScanner(Scanner): 29 | 30 | name = 'OpenVAS' 31 | 32 | def __init__(self): 33 | connection = UnixSocketConnection(path=config['OPENVAS_SOCKET']) 34 | transform = EtreeTransform() 35 | self.gmp = Gmp(connection, transform=transform) 36 | self.storage_service = StorageService() 37 | 38 | # Login 39 | try: 40 | self.gmp.authenticate(config['OPENVAS_USERNAME'], config['OPENVAS_PASSWORD']) 41 | except: 42 | print(f'[{self.name}] Not able to connect to the {self.name}: ', sys.exc_info()) 43 | return 44 | 45 | # Getting Version 46 | # version = self.gmp.get_version() 47 | # pretty_print(version) 48 | 49 | 50 | def start(self, scan_name, target): 51 | print(f'[{self.name}] Starting Scan for Target: {target}') 52 | 53 | try: 54 | return self.scan(scan_name, target) 55 | except: 56 | print(f'[{self.name}] Not able to connect to the {self.name}: ', sys.exc_info()) 57 | return False 58 | 59 | 60 | def scan(self, scan_name, target): 61 | print(f'[{self.name}] Scan Name: {scan_name}') 62 | 63 | address = self._get_address(target) 64 | 65 | # Creating Target 66 | target_response = self.gmp.create_target(name=scan_name, hosts=[address]) 67 | # print('target_response') 68 | pretty_print(target_response) 69 | target_id = target_response.get('id') 70 | 71 | if not target_id: 72 | print(f'[{self.name}] could not able to create target: ', target_response.get('status_text')) 73 | return False 74 | 75 | # target_id = '69ca3c65-af09-48b8-bb3a-59e2e6cccb96' 76 | 77 | print(f'[{self.name}] Target Created: {target_id}') 78 | 79 | scan_data = self.storage_service.get_by_name(scan_name) 80 | 81 | if not scan_data: 82 | scan_data = { 83 | 'scan_name': scan_name, 84 | 'scan_id': '', 85 | 'target': target, 86 | 'status': '' 87 | } 88 | self.storage_service.add(scan_data) 89 | 90 | scan_data['OPENVAS'] = { 91 | 'openvas_id': target_id, 92 | 'target_id': target_id, 93 | 'scan_status': { 94 | 'status': 'INPROGRESS' 95 | } 96 | } 97 | self.storage_service.update_by_name(scan_name, scan_data) 98 | 99 | time.sleep(4) 100 | self._create_report(scan_name) 101 | 102 | return scan_data 103 | 104 | 105 | def _create_report(self, scan_name): 106 | 107 | scan_data = self.storage_service.get_by_name(scan_name) 108 | openvas_id = scan_data['OPENVAS']['openvas_id'] 109 | 110 | scan_config_id = config['SCAN_CONFIG_ID'] 111 | scanner_id = config['SCANNER_ID'] 112 | report_format_id = config['REPORT_FORMAT_ID'] 113 | 114 | # Creating Task 115 | task_response = self.gmp.create_task(name=scan_name, config_id=scan_config_id, target_id=openvas_id, scanner_id=scanner_id) 116 | # print('task_response') 117 | # pretty_print(task_response) 118 | task_id = task_response.get('id') 119 | 120 | # Starting Task 121 | start_task_response = self.gmp.start_task(task_id) 122 | # print('start_task_response') 123 | # pretty_print(start_task_response) 124 | report_id = start_task_response[0].text 125 | 126 | scan_data['OPENVAS']['report_id'] = report_id 127 | scan_data['OPENVAS']['report_format_id'] = report_format_id 128 | scan_data['OPENVAS']['scan_config_id'] = scan_config_id 129 | scan_data['OPENVAS']['scanner_id'] = scanner_id 130 | scan_data['OPENVAS']['task_id'] = task_id 131 | 132 | print(f'[{self.name}] Created Report: {report_id} with Task: {task_id}') 133 | 134 | self.storage_service.update_by_name(scan_name, scan_data) 135 | 136 | return scan_data 137 | 138 | 139 | def get_scan_status(self, scan_name, scan_status_list=[]): 140 | 141 | if not self.is_valid_scan(scan_name): 142 | return False 143 | 144 | scan_data = self.storage_service.get_by_name(scan_name) 145 | scan_status = scan_data.get('OPENVAS', {}).get('scan_status', {}) 146 | openvas_id = scan_data.get('OPENVAS', {})['openvas_id'] 147 | target = scan_data['target'] 148 | 149 | print(f'[{self.name}] Getting Scan Status for Target: {target}') 150 | print(f'[{self.name}] Scan Name: {scan_name}') 151 | print(f'[{self.name}] Scan Id: {openvas_id}') 152 | 153 | try: 154 | scan_info = self.get_scan_results(scan_name) 155 | # print('scan_info') 156 | # pretty_print(scan_info) 157 | except: 158 | print(f'[{self.name}] Could not get the scan {openvas_id}: ', sys.exc_info()) 159 | return False 160 | 161 | scan_status['status'] = 'COMPLETE' if scan_info else 'INPROGRESS' 162 | scan_data['OPENVAS']['scan_status'] = scan_status 163 | 164 | self.storage_service.update_by_name(scan_name, scan_data) 165 | 166 | if scan_status['status'] is 'COMPLETE': 167 | print(f'[{self.name}] Scan {scan_name} Completed') 168 | 169 | scan_status_list.append({ 170 | 'scanner': self.name, 171 | 'status': scan_status['status'] 172 | }) 173 | 174 | return scan_status_list 175 | 176 | 177 | def get_scan_results(self, scan_name, scan_results={}): 178 | 179 | if not self.is_valid_scan(scan_name): 180 | return False 181 | 182 | scan_data = self.storage_service.get_by_name(scan_name) 183 | 184 | # if scan_data.get('OPENVAS', {}).get('scan_status').get('status', None) != 'COMPLETE': 185 | # print(f'[{self.name}] Scan is in progress') 186 | # return False 187 | 188 | openvas_id = scan_data.get('OPENVAS', {})['openvas_id'] 189 | report_id = scan_data.get('OPENVAS', {})['report_id'] 190 | report_format_id = scan_data.get('OPENVAS', {})['report_format_id'] 191 | # report_id = '79bf4984-9a0e-435a-807b-9dd530fb532f' 192 | 193 | try: 194 | report_response = self.gmp.get_report(report_id=report_id, report_format_id=report_format_id) 195 | # print('report_response') 196 | # pretty_print(report_response) 197 | except: 198 | print(f'[{self.name}] Could not get the scan {openvas_id}: ', sys.exc_info()) 199 | return False 200 | 201 | self._process_results(report_response, scan_results) 202 | 203 | return scan_results 204 | 205 | 206 | def _process_results(self, report_response, scan_results={}): 207 | report_response_str = ElementTree.tostring(report_response, encoding='unicode') 208 | report_response_dict = xmltodict.parse(report_response_str) 209 | 210 | report_results = report_response_dict.get('get_reports_response', {}).get('report', {}).get('report', {}).get('results', {}).get('result', []) 211 | 212 | # print(json.dumps(report_results, indent=2)) 213 | # print('report_results', report_results) 214 | 215 | for vuln in report_results: 216 | name = vuln.get('name') 217 | print('name: ', name) 218 | if scan_results.get(name): 219 | print('--- Duplicate name: ', name) 220 | continue 221 | nvt = vuln.get('nvt', {}) 222 | scan_result = {} 223 | scan_result['name'] = name 224 | scan_result['severity'] = float(nvt.get('cvss_base', 0)) 225 | scan_result['risk'] = vuln.get('threat') 226 | scan_result['cve_id'] = nvt.get('cve', 'N/A') if nvt.get('cve') != 'NOCVE' else 'N/A' 227 | scan_result['description'] = vuln.get('description') 228 | scan_result['solution'] = 'N/A' 229 | scan_result['reported_by'] = 'OpenVAS' 230 | scan_results[name] = scan_result 231 | return scan_results 232 | 233 | 234 | def is_valid_scan(self, scan_name): 235 | 236 | scan_data = self.storage_service.get_by_name(scan_name) 237 | if not scan_data: 238 | print(f'[{self.name}] Invalid Scan Name: {scan_name}') 239 | return False 240 | 241 | if not scan_data.get('OPENVAS'): 242 | print(f'[{self.name}] No Scan Details found for {scan_name}') 243 | return False 244 | 245 | return True 246 | 247 | 248 | def pause(self, scan_name): 249 | if not self.is_valid_scan(scan_name): 250 | return False 251 | 252 | # scan = self.storage_service.get_by_name(scan_name) 253 | # nexpose_id = scan['nexpose_id'] 254 | 255 | # response = self.nexpose.set_scan_status(nexpose_id, 'pause') 256 | # pprint(response) 257 | # return response 258 | 259 | 260 | def resume(self, scan_name): 261 | if not self.is_valid_scan(scan_name): 262 | return False 263 | 264 | # scan = self.storage_service.get_by_name(scan_name) 265 | # nexpose_id = scan['nexpose_id'] 266 | 267 | # response = self.nexpose.set_scan_status(nexpose_id, 'resume') 268 | # pprint(response) 269 | # return response 270 | 271 | 272 | def stop(self, scan_name): 273 | if not self.is_valid_scan(scan_name): 274 | return False 275 | 276 | # scan = self.storage_service.get_by_name(scan_name) 277 | # nexpose_id = scan['nexpose_id'] 278 | 279 | # response = self.nexpose.set_scan_status(nexpose_id, 'stop') 280 | # pprint(response) 281 | # return response 282 | 283 | 284 | def remove(self, scan_name): 285 | if not self.is_valid_scan(scan_name): 286 | return False 287 | 288 | # scan = self.storage_service.get_by_name(scan_name) 289 | # nexpose_id = scan['nexpose_id'] 290 | 291 | # response = self.nexpose.set_scan_status(nexpose_id, 'remove') 292 | # pprint(response) 293 | # return response 294 | 295 | 296 | def list_scans(self): 297 | pass 298 | 299 | -------------------------------------------------------------------------------- /scanners/nexpose_scanner.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import base64 5 | 6 | import rapid7vmconsole 7 | from pprint import pprint 8 | from bs4 import BeautifulSoup 9 | from dotenv import load_dotenv, find_dotenv 10 | 11 | from .scanner import Scanner 12 | from core.storage_service import StorageService 13 | 14 | load_dotenv(find_dotenv()) 15 | 16 | config = { 17 | 'HOST': os.getenv('NEXPOSE_HOST'), 18 | 'USERNAME': os.getenv('NEXPOSE_USERNAME'), 19 | 'PASSWORD': os.getenv('NEXPOSE_PASSWORD') 20 | } 21 | 22 | class NexposeScanner(Scanner): 23 | 24 | name = 'Nexpose' 25 | 26 | def __init__(self): 27 | self.nexpose_config = rapid7vmconsole.Configuration(name='Scanner') 28 | self.nexpose_config.username = config['USERNAME'] 29 | self.nexpose_config.password = config['PASSWORD'] 30 | self.nexpose_config.host = config['HOST'] 31 | self.nexpose_config.assert_hostname = False 32 | self.nexpose_config.verify_ssl = False 33 | self.nexpose_config.ssl_ca_cert = None 34 | self.nexpose_config.connection_pool_maxsize = None 35 | self.nexpose_config.proxy = None 36 | self.nexpose_config.cert_file = None 37 | self.nexpose_config.key_file = None 38 | self.nexpose_config.safe_chars_for_path_param = '' 39 | 40 | auth_token = f'{config["USERNAME"]}:{config["PASSWORD"]}' 41 | auth_token = base64.b64encode(auth_token.encode('ascii')).decode() 42 | 43 | api_client = rapid7vmconsole.ApiClient(configuration=self.nexpose_config) 44 | api_client.default_headers['Authorization'] = f'Basic {auth_token}' 45 | 46 | self.nexpose_admin = rapid7vmconsole.AdministrationApi(api_client) 47 | self.nexpose = rapid7vmconsole.ScanApi(api_client) 48 | self.nexpose_site = rapid7vmconsole.SiteApi(api_client) 49 | self.nexpose_assets = rapid7vmconsole.AssetApi(api_client) 50 | self.nexpose_report = rapid7vmconsole.ReportApi(api_client) 51 | self.storage_service = StorageService() 52 | 53 | 54 | def start(self, scan_name, target): 55 | print(f'[{self.name}] Starting Scan for Target: {target}') 56 | 57 | try: 58 | return self.scan(scan_name, target) 59 | except: 60 | print(f'[{self.name}] Not able to connect to the {self.name}: ', sys.exc_info()) 61 | return False 62 | 63 | def scan(self, scan_name, target): 64 | print(f'[{self.name}] Scan Name: {scan_name}') 65 | 66 | address = self._get_address(target) 67 | 68 | # Creating Site 69 | scan_targets = rapid7vmconsole.IncludedScanTargets(addresses=[address]) 70 | asset = rapid7vmconsole.StaticSite(included_targets=scan_targets) 71 | 72 | scan_scope = rapid7vmconsole.ScanScope(assets=asset) 73 | site_create_resource = rapid7vmconsole.SiteCreateResource(name=scan_name, scan=scan_scope) 74 | 75 | site = self.nexpose_site.create_site(site=site_create_resource) 76 | print(f'[{self.name}] Site Created: {site.id}') 77 | 78 | # Creating Scan 79 | adhoc_scan = rapid7vmconsole.AdhocScan(hosts=[address]) 80 | 81 | # Starting Scan 82 | scan = self.nexpose.start_scan(site.id, scan=adhoc_scan) 83 | print(f'[{self.name}] Scan Started: {scan.id}') 84 | 85 | scan_data = self.storage_service.get_by_name(scan_name) 86 | 87 | if not scan_data: 88 | scan_data = { 89 | 'scan_name': scan_name, 90 | 'scan_id': '', 91 | 'target': target, 92 | 'status': '' 93 | } 94 | self.storage_service.add(scan_data) 95 | 96 | scan_data['NEXPOSE'] = { 97 | 'nexpose_id': scan.id, 98 | 'site_id': site.id, 99 | 'scan_status': { 100 | 'status': 'INPROGRESS' 101 | } 102 | } 103 | self.storage_service.update_by_name(scan_name, scan_data) 104 | 105 | return scan_data 106 | 107 | 108 | def _create_report(self, scan_name): 109 | 110 | scan_data = self.storage_service.get_by_name(scan_name) 111 | nexpose_id = scan_data['NEXPOSE']['nexpose_id'] 112 | 113 | # Creating Report 114 | report_config_scope = rapid7vmconsole.ReportConfigScopeResource(scan=nexpose_id) 115 | report_config = rapid7vmconsole.Report(name=f'{scan_name}-xml-Report', format='xml-export', scope=report_config_scope) 116 | report = self.nexpose_report.create_report(report=report_config) 117 | 118 | # Generate Report Instance 119 | report_instance = self.nexpose_report.generate_report(report.id) 120 | 121 | scan_data['NEXPOSE']['report_id'] = report.id 122 | scan_data['NEXPOSE']['report_instance_id'] = report_instance.id 123 | 124 | print(f'[{self.name}] Created Report: {report.id} with Instance: {report_instance.id}') 125 | 126 | self.storage_service.update_by_name(scan_name, scan_data) 127 | 128 | return scan_data 129 | 130 | 131 | def get_scan_status(self, scan_name, scan_status_list=[]): 132 | 133 | if not self.is_valid_scan(scan_name): 134 | return False 135 | 136 | scan_data = self.storage_service.get_by_name(scan_name) 137 | scan_status = scan_data.get('NEXPOSE', {}).get('scan_status', {}) 138 | nexpose_id = scan_data.get('NEXPOSE', {})['nexpose_id'] 139 | target = scan_data['target'] 140 | 141 | print(f'[{self.name}] Getting Scan Status for Target: {target}') 142 | print(f'[{self.name}] Scan Name: {scan_name}') 143 | print(f'[{self.name}] Scan Id: {nexpose_id}') 144 | 145 | try: 146 | scan_info = self.nexpose.get_scan(nexpose_id) 147 | except: 148 | print(f'[{self.name}] Could not get the scan {nexpose_id}: ', sys.exc_info()) 149 | return False 150 | 151 | scan_status['vulnerabilities'] = scan_info.vulnerabilities.__dict__ 152 | scan_status['status'] = 'COMPLETE' if scan_info.status == 'finished' else 'INPROGRESS' if scan_info.status == 'running' else scan_info.status 153 | 154 | scan_data['NEXPOSE']['scan_status'] = scan_status 155 | 156 | self.storage_service.update_by_name(scan_name, scan_data) 157 | 158 | if scan_status['status'] is 'COMPLETE' and scan_data['NEXPOSE'].get('report_id', None) is None: 159 | print(f'[{self.name}] Scan {scan_name} Completed, Generating Report') 160 | 161 | # Starting the Report Generate Process 162 | self._create_report(scan_name) 163 | time.sleep(2) 164 | 165 | scan_status_list.append({ 166 | 'scanner': self.name, 167 | 'status': scan_status['status'] 168 | }) 169 | 170 | return scan_status_list 171 | 172 | 173 | def get_scan_results(self, scan_name, scan_results={}): 174 | 175 | if not self.is_valid_scan(scan_name): 176 | return False 177 | 178 | scan_data = self.storage_service.get_by_name(scan_name) 179 | 180 | if scan_data.get('NEXPOSE', {}).get('scan_status').get('status', None) != 'COMPLETE': 181 | print(f'[{self.name}] Scan is in progress') 182 | return False 183 | 184 | nexpose_id = scan_data.get('NEXPOSE', {})['nexpose_id'] 185 | report_id = scan_data.get('NEXPOSE', {})['report_id'] 186 | report_instance_id = scan_data.get('NEXPOSE', {})['report_instance_id'] 187 | 188 | try: 189 | downloaded_report = self.nexpose_report.download_report(report_id, report_instance_id) 190 | except: 191 | print(f'[{self.name}] Could not get the scan {nexpose_id}: ', sys.exc_info()) 192 | return False 193 | 194 | parsed_report = BeautifulSoup(downloaded_report, features='xml') 195 | 196 | self._process_results(parsed_report, scan_results) 197 | 198 | return scan_results 199 | 200 | def _process_results(self, report, scan_results): 201 | 202 | for vuln in report.find_all('vulnerability'): 203 | name = vuln.get('title') 204 | 205 | if scan_results.get(name): 206 | print('-------- Dup title', name) 207 | 208 | self.common_service.is_duplicate(scan_results.get(name), vuln) 209 | 210 | continue 211 | 212 | scan_result = {} 213 | scan_result['name'] = name 214 | scan_result['severity'] = float(vuln.get('cvssScore')) 215 | scan_result['risk'] = self._get_nexpose_risk(scan_result['severity']) 216 | scan_result['cve_id'] = '' 217 | scan_result['description'] = '' 218 | scan_result['solution'] = '' 219 | scan_result['reported_by'] = self.name 220 | 221 | if vuln.references.find('reference', source='CVE'): 222 | scan_result['cve_id'] = vuln.references.find('reference', source='CVE').text 223 | 224 | if vuln.description.ContainerBlockElement.Paragraph: 225 | scan_result['description'] = vuln.description.ContainerBlockElement.Paragraph.text 226 | 227 | if vuln.solution.ContainerBlockElement.Paragraph: 228 | scan_result['solution'] = vuln.solution.ContainerBlockElement.Paragraph.text 229 | 230 | scan_results[name] = scan_result 231 | 232 | return scan_results 233 | 234 | 235 | def is_valid_scan(self, scan_name): 236 | 237 | scan_data = self.storage_service.get_by_name(scan_name) 238 | if not scan_data: 239 | print(f'[{self.name}] Invalid Scan Name: {scan_name}') 240 | return False 241 | 242 | if not scan_data.get('NEXPOSE'): 243 | print(f'[{self.name}] No Scan Details found for {scan_name}') 244 | return False 245 | 246 | return True 247 | 248 | def _get_nexpose_risk(self, severity): 249 | if 0.1 <= severity <= 3.9: 250 | return 'Low' 251 | elif 4 <= severity <= 6.9: 252 | return 'Low' 253 | elif 7 <= severity <= 10: 254 | return 'Low' 255 | else: 256 | return 'N/A' 257 | 258 | 259 | def pause(self, scan_name): 260 | if not self.is_valid_scan(scan_name): 261 | return False 262 | 263 | scan = self.storage_service.get_by_name(scan_name) 264 | nexpose_id = scan['nexpose_id'] 265 | 266 | response = self.nexpose.set_scan_status(nexpose_id, 'pause') 267 | pprint(response) 268 | return response 269 | 270 | 271 | def resume(self, scan_name): 272 | if not self.is_valid_scan(scan_name): 273 | return False 274 | 275 | scan = self.storage_service.get_by_name(scan_name) 276 | nexpose_id = scan['nexpose_id'] 277 | 278 | response = self.nexpose.set_scan_status(nexpose_id, 'resume') 279 | pprint(response) 280 | return response 281 | 282 | 283 | def stop(self, scan_name): 284 | if not self.is_valid_scan(scan_name): 285 | return False 286 | 287 | scan = self.storage_service.get_by_name(scan_name) 288 | nexpose_id = scan['nexpose_id'] 289 | 290 | response = self.nexpose.set_scan_status(nexpose_id, 'stop') 291 | pprint(response) 292 | return response 293 | 294 | 295 | def remove(self, scan_name): 296 | if not self.is_valid_scan(scan_name): 297 | return False 298 | 299 | scan = self.storage_service.get_by_name(scan_name) 300 | nexpose_id = scan['nexpose_id'] 301 | 302 | response = self.nexpose.set_scan_status(nexpose_id, 'remove') 303 | pprint(response) 304 | return response 305 | 306 | 307 | def list_scans(self): 308 | active = False 309 | scans = self.nexpose.get_scans(active=active) 310 | print(f'[{self.name}] Scans: {len(scans)}', scans) 311 | 312 | --------------------------------------------------------------------------------