From abd0310827e532520d10349131660d151d99aa7c Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 28 Oct 2024 16:12:24 +1000 Subject: [PATCH 1/9] wip: early commit testing nessus integration --- config/config-template-nessus.yaml | 41 +++++++++++ rapidast.py | 4 ++ requirements.txt | 2 + scanners/nessus/__init__.py | 0 scanners/nessus/nessus.py | 100 +++++++++++++++++++++++++++ tests/scanners/nessus/test_nessus.py | 27 ++++++++ tests/scanners/zap/test_setup.py | 2 - 7 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 config/config-template-nessus.yaml create mode 100644 scanners/nessus/__init__.py create mode 100644 scanners/nessus/nessus.py create mode 100644 tests/scanners/nessus/test_nessus.py diff --git a/config/config-template-nessus.yaml b/config/config-template-nessus.yaml new file mode 100644 index 00000000..74470137 --- /dev/null +++ b/config/config-template-nessus.yaml @@ -0,0 +1,41 @@ +config: + # WARNING: `configVersion` indicates the schema version of the config file. + # This value tells RapiDAST what schema should be used to read this configuration. + # Therefore you should only change it if you update the configuration to a newer schema + configVersion: 6 + + # all the results of all scanners will be stored under that location + base_results_dir: "./results" + +# `application` contains data related to the application, not to the scans. +application: + shortName: "MyApp-1.0" + url: "" + +# `general` is a section that will be applied to all scanners. +# Any scanner can override a value by creating an entry of the same name in their own configuration +general: + + # remove `authentication` entirely for unauthenticated connection + authentication: + type: "oauth2_rtoken" + parameters: + client_id: "cloud-services" + token_endpoint: "" + rtoken_from_var: "RTOKEN" # referring to a env defined in general.environ.envFile + #preauth: false # set to true to pregenerate a token, and stick to it (no refresh) + +# `scanners' is a section that configures scanning options +scanners: + nessus: + server: + url: https://nessus-server.example.com # required + username: user + password: pass + scan: + name: py-nessus-test + folder: py-nessus + policy: "py-test" + targets: + - 127.0.0.1 + reportPath: ./reports diff --git a/rapidast.py b/rapidast.py index abdcf5b2..745e6a6d 100755 --- a/rapidast.py +++ b/rapidast.py @@ -59,6 +59,10 @@ def load_config_file(config_file_location: str): return open(config_file_location, mode="r", encoding="utf-8") +def load_config(config_file_location: str): + return yaml.safe_load(load_config_file(config_file_location)) + + def run_scanner(name, config, args, scan_exporter): """given the config `config`, runs scanner `name`. Returns: diff --git a/requirements.txt b/requirements.txt index c64861c3..3ba29592 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,5 @@ python-dotenv >= 1.0.0 pyyaml >= 6.0 requests >= 2.27.1 google.cloud.storage >= 2.17.0 +py_nessus_pro >= 1.2.5 +dacite >= 1.8.1 diff --git a/scanners/nessus/__init__.py b/scanners/nessus/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/scanners/nessus/nessus.py b/scanners/nessus/nessus.py new file mode 100644 index 00000000..63243c4b --- /dev/null +++ b/scanners/nessus/nessus.py @@ -0,0 +1,100 @@ +import logging +import dacite +import requests.exceptions + +from dataclasses import dataclass +from typing import List +from py_nessus_pro import PyNessusPro + +from scanners import RapidastScanner, State + + +@dataclass +class NessusServerConfig: + url: str + username: str + password: str + + +@dataclass +class NessusScanConfig: + name: str + folder: str + policy: str + targets: List[str] + reportPath: str + + +@dataclass +class NessusConfig: + server: NessusServerConfig + scan: NessusScanConfig + + +class Nessus(RapidastScanner): + def __init__(self, config): + super().__init__(config, ident="nessus") + self.nessus_client = None + nessus_config_section = config.get("scanners.nessus") + if nessus_config_section is None: + raise ValueError("'scanners.nessus' section not in config") + self.config = dacite.from_dict(data_class=NessusConfig, data=nessus_config_section) + self._connect() + + def _connect(self): + try: + self.nessus_client = PyNessusPro( + self.config.server.url, + self.config.server.username, + self.config.server.password, + log_level="debug", + ) + except requests.exceptions.RequestException as e: + logging.error(f"Failed to connect to {self.config.server.url}: {e}") + raise + + def setup(self): + if self.nessus_client is None: + raise RuntimeError(f"Nessus client not connected: {self.state}") + # XXX create scan + # # Create scan object for use by PyNessusPro + # # Scan name, scan targets, and scan folder are all retrieved from config + # scanID = nessus.new_scan( + # name=config.get("scan_name"), + # targets=config.get("scan_targets"), + # folder=config.get("scan_folder"), + # create_folder=True, + # ) + # + # # Set scan policy + # # Scan policy is retrieved from config + # # Special note: As implemented, only user-created scan policies seem to be identified and must be created with the name used in the config as a prerequisite + # if config.get("scan_policy"): + # nessus.set_scan_policy(scan_id=scanID, policy=config.get("scan_policy")) + self.state = State.READY + + def run(self): + if self.state != State.READY: + raise RuntimeError(f"[nessus] unexpected state: READY != {self.state}") + # XXX launch scan + # # State that we want the scan to launch immediately + # nessus.set_scan_launch_now(scan_id=scanID, launch_now=True) + # + # # Tell nessus to create and launch the scan + # nessus.post_scan(scan_id=scanID) + # + # # Wait for the scan to complete + # while nessus.get_scan_status(scanID)["status"] not in ["completed", "canceled", "imported", "aborted"]: + # time.sleep(20) + # print(nessus.get_scan_status(scanID)) + + def postprocess(self): + # # After scan is complete, download report in csv, nessus, and html format + # # Path and any folders must already exist in this implementation + # scan_reports = nessus.get_scan_reports(scanID, config.get("report_path")) + if not self.state == State.ERROR: + self.state = State.PROCESSED + + def cleanup(self): + if not self.state == State.PROCESSED: + raise RuntimeError(f"[nessus] unexpected state: PROCESSED != {self.state}") diff --git a/tests/scanners/nessus/test_nessus.py b/tests/scanners/nessus/test_nessus.py new file mode 100644 index 00000000..fa5512c8 --- /dev/null +++ b/tests/scanners/nessus/test_nessus.py @@ -0,0 +1,27 @@ +import configmodel +import rapidast + +import requests + +from scanners.nessus.nessus import Nessus +from unittest.mock import Mock, patch + + +class TestNessus: + @patch("requests.Session.request") + @patch("py_nessus_pro.py_nessus_pro.BeautifulSoup") # patch where imported + @patch("py_nessus_pro.py_nessus_pro.webdriver") # patch where imported + def test_setup_nessus(self, mock_driver, mock_bs4, mock_get): + # All this mocking is for PyNessusPro.__init__() which attempts to connect to Nessus + mock_soup = Mock() + mock_soup.find_all.return_value = [{"src": "foo"}] + mock_bs4.return_value = mock_soup + mock_get.return_value = Mock(spec=requests.Response) + mock_get.return_value.status_code = 200 + mock_get.return_value.text = '{"token": "foo", "folders": []}' + + config_data = rapidast.load_config("config/config-template-nessus.yaml") + config = configmodel.RapidastConfigModel(config_data) + test_nessus = Nessus(config=config) + assert test_nessus is not None + assert test_nessus.nessus_client is not None diff --git a/tests/scanners/zap/test_setup.py b/tests/scanners/zap/test_setup.py index 7da36c62..72eba6ea 100644 --- a/tests/scanners/zap/test_setup.py +++ b/tests/scanners/zap/test_setup.py @@ -1,6 +1,4 @@ import os -import pathlib -import re from pathlib import Path import pytest From 559c3ec06481d3b0b6745d7d64997acc76dbc30e Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 28 Oct 2024 16:20:56 +1000 Subject: [PATCH 2/9] lint --- scanners/nessus/nessus.py | 16 ++++++++-------- tests/scanners/nessus/test_nessus.py | 7 ++++--- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/scanners/nessus/nessus.py b/scanners/nessus/nessus.py index 63243c4b..d8bb8e44 100644 --- a/scanners/nessus/nessus.py +++ b/scanners/nessus/nessus.py @@ -1,12 +1,13 @@ import logging -import dacite -import requests.exceptions - from dataclasses import dataclass from typing import List + +import dacite +import requests.exceptions from py_nessus_pro import PyNessusPro -from scanners import RapidastScanner, State +from scanners import RapidastScanner +from scanners import State @dataclass @@ -22,7 +23,7 @@ class NessusScanConfig: folder: str policy: str targets: List[str] - reportPath: str + # reportPath: str @dataclass @@ -56,7 +57,6 @@ def _connect(self): def setup(self): if self.nessus_client is None: raise RuntimeError(f"Nessus client not connected: {self.state}") - # XXX create scan # # Create scan object for use by PyNessusPro # # Scan name, scan targets, and scan folder are all retrieved from config # scanID = nessus.new_scan( @@ -68,7 +68,8 @@ def setup(self): # # # Set scan policy # # Scan policy is retrieved from config - # # Special note: As implemented, only user-created scan policies seem to be identified and must be created with the name used in the config as a prerequisite + # # Special note: As implemented, only user-created scan policies seem to be identified and must be + # # created with the name used in the config as a prerequisite # if config.get("scan_policy"): # nessus.set_scan_policy(scan_id=scanID, policy=config.get("scan_policy")) self.state = State.READY @@ -76,7 +77,6 @@ def setup(self): def run(self): if self.state != State.READY: raise RuntimeError(f"[nessus] unexpected state: READY != {self.state}") - # XXX launch scan # # State that we want the scan to launch immediately # nessus.set_scan_launch_now(scan_id=scanID, launch_now=True) # diff --git a/tests/scanners/nessus/test_nessus.py b/tests/scanners/nessus/test_nessus.py index fa5512c8..ec8e34ac 100644 --- a/tests/scanners/nessus/test_nessus.py +++ b/tests/scanners/nessus/test_nessus.py @@ -1,10 +1,11 @@ -import configmodel -import rapidast +from unittest.mock import Mock +from unittest.mock import patch import requests +import configmodel +import rapidast from scanners.nessus.nessus import Nessus -from unittest.mock import Mock, patch class TestNessus: From a52279e94feb4286c450e9d09fc5cc495f9a88b1 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 29 Oct 2024 13:51:48 +1000 Subject: [PATCH 3/9] update nessus scanner --- config/config-template-nessus.yaml | 36 +++---- rapidast.py | 6 +- scanners/__init__.py | 3 +- scanners/nessus/nessus.py | 100 -------------------- scanners/nessus/nessus_none.py | 135 +++++++++++++++++++++++++++ tests/scanners/nessus/test_nessus.py | 2 +- 6 files changed, 160 insertions(+), 122 deletions(-) delete mode 100644 scanners/nessus/nessus.py create mode 100644 scanners/nessus/nessus_none.py diff --git a/config/config-template-nessus.yaml b/config/config-template-nessus.yaml index 74470137..5d40129e 100644 --- a/config/config-template-nessus.yaml +++ b/config/config-template-nessus.yaml @@ -2,40 +2,40 @@ config: # WARNING: `configVersion` indicates the schema version of the config file. # This value tells RapiDAST what schema should be used to read this configuration. # Therefore you should only change it if you update the configuration to a newer schema - configVersion: 6 + configVersion: 5 # all the results of all scanners will be stored under that location - base_results_dir: "./results" + # base_results_dir: "./results" # `application` contains data related to the application, not to the scans. application: - shortName: "MyApp-1.0" - url: "" + shortName: "nessus-test-1.0" + # url: "" # XXX unused for nessus # `general` is a section that will be applied to all scanners. # Any scanner can override a value by creating an entry of the same name in their own configuration general: + # XXX auth section not yet used by nessus scanner # remove `authentication` entirely for unauthenticated connection - authentication: - type: "oauth2_rtoken" - parameters: - client_id: "cloud-services" - token_endpoint: "" - rtoken_from_var: "RTOKEN" # referring to a env defined in general.environ.envFile - #preauth: false # set to true to pregenerate a token, and stick to it (no refresh) + # authentication: + # type: "oauth2_rtoken" + # parameters: + # client_id: "cloud-services" + # token_endpoint: "" + # # rtoken_from_var: "RTOKEN" # referring to a env defined in general.environ.envFile + # #preauth: false # set to true to pregenerate a token, and stick to it (no refresh) # `scanners' is a section that configures scanning options scanners: nessus: server: - url: https://nessus-server.example.com # required - username: user - password: pass + url: https://nessus-example.com/ # URL of Nessus instance + username_from_var: NESSUS_USER # Nessus credentials + password_from_var: NESSUS_PASSWORD scan: - name: py-nessus-test - folder: py-nessus - policy: "py-test" + name: test-scan # name of new scan to create + folder: test-folder # name of folder in to contain scan + policy: "py-test" # policy used for scan targets: - 127.0.0.1 - reportPath: ./reports diff --git a/rapidast.py b/rapidast.py index 745e6a6d..ffe803ff 100755 --- a/rapidast.py +++ b/rapidast.py @@ -6,6 +6,8 @@ import re import sys from datetime import datetime +from typing import Any +from typing import Dict from urllib import request import yaml @@ -59,7 +61,7 @@ def load_config_file(config_file_location: str): return open(config_file_location, mode="r", encoding="utf-8") -def load_config(config_file_location: str): +def load_config(config_file_location: str) -> Dict[str, Any]: return yaml.safe_load(load_config_file(config_file_location)) @@ -142,7 +144,7 @@ def dump_redacted_config(config_file_location: str, destination_dir: str) -> boo destination_dir: The directory where the redacted configuration file should be saved """ - logging.info("Starting the redaction and dumping process for the configuration file: {config_file_location}") + logging.info(f"Starting the redaction and dumping process for the configuration file: {config_file_location}") try: if not os.path.exists(destination_dir): diff --git a/scanners/__init__.py b/scanners/__init__.py index c0f5df05..1de6228b 100644 --- a/scanners/__init__.py +++ b/scanners/__init__.py @@ -20,12 +20,13 @@ class State(Enum): class RapidastScanner: - def __init__(self, config, ident): + def __init__(self, config: configmodel.RapidastConfigModel, ident: str): self.ident = ident self.config = config self.state = State.UNCONFIGURED self.results_dir = os.path.join(self.config.get("config.results_dir", default="results"), self.ident) + os.makedirs(self.results_dir, exist_ok=True) # When requested to create a temporary file or directory, it will be a subdir of # this temporary directory diff --git a/scanners/nessus/nessus.py b/scanners/nessus/nessus.py deleted file mode 100644 index d8bb8e44..00000000 --- a/scanners/nessus/nessus.py +++ /dev/null @@ -1,100 +0,0 @@ -import logging -from dataclasses import dataclass -from typing import List - -import dacite -import requests.exceptions -from py_nessus_pro import PyNessusPro - -from scanners import RapidastScanner -from scanners import State - - -@dataclass -class NessusServerConfig: - url: str - username: str - password: str - - -@dataclass -class NessusScanConfig: - name: str - folder: str - policy: str - targets: List[str] - # reportPath: str - - -@dataclass -class NessusConfig: - server: NessusServerConfig - scan: NessusScanConfig - - -class Nessus(RapidastScanner): - def __init__(self, config): - super().__init__(config, ident="nessus") - self.nessus_client = None - nessus_config_section = config.get("scanners.nessus") - if nessus_config_section is None: - raise ValueError("'scanners.nessus' section not in config") - self.config = dacite.from_dict(data_class=NessusConfig, data=nessus_config_section) - self._connect() - - def _connect(self): - try: - self.nessus_client = PyNessusPro( - self.config.server.url, - self.config.server.username, - self.config.server.password, - log_level="debug", - ) - except requests.exceptions.RequestException as e: - logging.error(f"Failed to connect to {self.config.server.url}: {e}") - raise - - def setup(self): - if self.nessus_client is None: - raise RuntimeError(f"Nessus client not connected: {self.state}") - # # Create scan object for use by PyNessusPro - # # Scan name, scan targets, and scan folder are all retrieved from config - # scanID = nessus.new_scan( - # name=config.get("scan_name"), - # targets=config.get("scan_targets"), - # folder=config.get("scan_folder"), - # create_folder=True, - # ) - # - # # Set scan policy - # # Scan policy is retrieved from config - # # Special note: As implemented, only user-created scan policies seem to be identified and must be - # # created with the name used in the config as a prerequisite - # if config.get("scan_policy"): - # nessus.set_scan_policy(scan_id=scanID, policy=config.get("scan_policy")) - self.state = State.READY - - def run(self): - if self.state != State.READY: - raise RuntimeError(f"[nessus] unexpected state: READY != {self.state}") - # # State that we want the scan to launch immediately - # nessus.set_scan_launch_now(scan_id=scanID, launch_now=True) - # - # # Tell nessus to create and launch the scan - # nessus.post_scan(scan_id=scanID) - # - # # Wait for the scan to complete - # while nessus.get_scan_status(scanID)["status"] not in ["completed", "canceled", "imported", "aborted"]: - # time.sleep(20) - # print(nessus.get_scan_status(scanID)) - - def postprocess(self): - # # After scan is complete, download report in csv, nessus, and html format - # # Path and any folders must already exist in this implementation - # scan_reports = nessus.get_scan_reports(scanID, config.get("report_path")) - if not self.state == State.ERROR: - self.state = State.PROCESSED - - def cleanup(self): - if not self.state == State.PROCESSED: - raise RuntimeError(f"[nessus] unexpected state: PROCESSED != {self.state}") diff --git a/scanners/nessus/nessus_none.py b/scanners/nessus/nessus_none.py new file mode 100644 index 00000000..b80a2e2f --- /dev/null +++ b/scanners/nessus/nessus_none.py @@ -0,0 +1,135 @@ +import logging +import time +from dataclasses import dataclass +from dataclasses import field +from typing import List +from typing import Optional + +import dacite +import requests.exceptions +from py_nessus_pro import PyNessusPro + +from configmodel import RapidastConfigModel +from scanners import RapidastScanner +from scanners import State + + +@dataclass +class NessusServerConfig: + url: str + username: str + password: str + + +@dataclass +class NessusScanConfig: + name: str + policy: str + targets: List[str] + folder: str = field(default="rapidast") + + def targets_as_str(self) -> str: + return " ".join(self.targets) + + +@dataclass +class NessusConfig: + server: NessusServerConfig + scan: NessusScanConfig + + +# XXX required by ./rapidast.py +CLASSNAME = "Nessus" + +END_STATUSES = [ + "completed", + "canceled", + "imported", + "aborted", +] + + +class Nessus(RapidastScanner): + def __init__(self, config: RapidastConfigModel, ident: str = "nessus"): + super().__init__(config, ident) + self._nessus_client: Optional[PyNessusPro] = None + self._scan_id: Optional[int] = None + nessus_config_section = config.subtree_to_dict("scanners.nessus") + if nessus_config_section is None: + raise ValueError("'scanners.nessus' section not in config") + # self.auth_config = config.subtree_to_dict("general.authentication") # creds for target hosts + self.config = dacite.from_dict(data_class=NessusConfig, data=nessus_config_section) + self.sleep_interval: int = 20 + self._connect() + + def _connect(self): + logging.debug(f"Connecting to nessus instance at {self.config.server.url}") + try: + self._nessus_client = PyNessusPro( + self.config.server.url, + self.config.server.username, + self.config.server.password, + log_level="debug", + ) + except requests.exceptions.RequestException as e: + logging.error(f"Failed to connect to {self.config.server.url}: {e}") + raise + + @property + def nessus_client(self) -> PyNessusPro: + if self._nessus_client is None: + raise RuntimeError(f"Nessus client not connected: {self.state}") + return self._nessus_client + + @property + def scan_id(self) -> int: + if self._scan_id is None: + raise RuntimeError("scan_id is None") + return self._scan_id + + def setup(self): + logging.debug(f"Creating new scan named {self.config.scan.folder}/{self.config.scan.name}") + self._scan_id = self.nessus_client.new_scan( + name=self.config.scan.name, + targets=self.config.scan.targets_as_str(), + folder=self.config.scan.folder, + create_folder=True, + ) + + # only user-created scan policies seem to be identified and must be + # created with the name used in the config as a prerequisite + if self.config.scan.policy: + logging.debug(f"Setting scan policy to {self.config.scan.policy}") + self.nessus_client.set_scan_policy(scan_id=self.scan_id, policy=self.config.scan.policy) + + self.state = State.READY + + def run(self): + if self.state != State.READY: + raise RuntimeError(f"[nessus] unexpected state: READY != {self.state}") + # State that we want the scan to launch immediately + logging.debug("Launching scan") + self.nessus_client.set_scan_launch_now(scan_id=self.scan_id, launch_now=True) + + # Tell nessus to create and launch the scan + self.nessus_client.post_scan(scan_id=self.scan_id) + + # Wait for the scan to complete + while self.nessus_client.get_scan_status(self.scan_id)["status"] not in END_STATUSES: + time.sleep(self.sleep_interval) + logging.debug("Waiting {self.sleep_interval}s for scan to finish") + logging.info(self.nessus_client.get_scan_status(self.scan_id)) + + def postprocess(self): + # After scan is complete, download report in csv, nessus, and html format + # Path and any folders must already exist in this implementation + logging.debug("Retrieving scan reports") + scan_reports = self.nessus_client.get_scan_reports(self.scan_id, self.results_dir) + print(scan_reports) + if not self.state == State.ERROR: + self.state = State.PROCESSED + + def cleanup(self): + logging.debug("cleaning up") + if not self.state == State.PROCESSED: + raise RuntimeError(f"[nessus] unexpected state: PROCESSED != {self.state}") diff --git a/tests/scanners/nessus/test_nessus.py b/tests/scanners/nessus/test_nessus.py index ec8e34ac..2c8c3d56 100644 --- a/tests/scanners/nessus/test_nessus.py +++ b/tests/scanners/nessus/test_nessus.py @@ -5,7 +5,7 @@ import configmodel import rapidast -from scanners.nessus.nessus import Nessus +from scanners.nessus.nessus_none import Nessus class TestNessus: From bb1172536e1dbdb64b1e330c8ae55cdb3554b376 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 29 Oct 2024 14:03:16 +1000 Subject: [PATCH 4/9] update nessus sample config --- config/config-template-nessus.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/config-template-nessus.yaml b/config/config-template-nessus.yaml index 5d40129e..1b4ed1c7 100644 --- a/config/config-template-nessus.yaml +++ b/config/config-template-nessus.yaml @@ -31,8 +31,8 @@ scanners: nessus: server: url: https://nessus-example.com/ # URL of Nessus instance - username_from_var: NESSUS_USER # Nessus credentials - password_from_var: NESSUS_PASSWORD + username: foo # OR username_from_var: NESSUS_USER + password: bar # OR password_from_var: NESSUS_PASSWORD scan: name: test-scan # name of new scan to create folder: test-folder # name of folder in to contain scan From 62807b0c31deca22392a51344d9b9bc618d119f5 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 29 Oct 2024 14:18:39 +1000 Subject: [PATCH 5/9] Add nessus_podman.py --- scanners/nessus/nessus_podman.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 scanners/nessus/nessus_podman.py diff --git a/scanners/nessus/nessus_podman.py b/scanners/nessus/nessus_podman.py new file mode 100644 index 00000000..d0f748d3 --- /dev/null +++ b/scanners/nessus/nessus_podman.py @@ -0,0 +1,6 @@ +CLASSNAME = "Nessus" + + +class Nessus: + def __init__(self, *args): + raise RuntimeError("nessus scanner is not supported with 'general.container.type=podman' config option") From 8cae29a650a819dfb923ff463e1efd0820b85e75 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 30 Oct 2024 10:02:04 +1000 Subject: [PATCH 6/9] improve nessus config handling --- scanners/nessus/nessus_none.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/scanners/nessus/nessus_none.py b/scanners/nessus/nessus_none.py index b80a2e2f..1516cb39 100644 --- a/scanners/nessus/nessus_none.py +++ b/scanners/nessus/nessus_none.py @@ -54,25 +54,26 @@ def __init__(self, config: RapidastConfigModel, ident: str = "nessus"): super().__init__(config, ident) self._nessus_client: Optional[PyNessusPro] = None self._scan_id: Optional[int] = None - nessus_config_section = config.subtree_to_dict("scanners.nessus") + nessus_config_section = config.subtree_to_dict(f"scanners.{ident}") if nessus_config_section is None: raise ValueError("'scanners.nessus' section not in config") # self.auth_config = config.subtree_to_dict("general.authentication") # creds for target hosts - self.config = dacite.from_dict(data_class=NessusConfig, data=nessus_config_section) + # XXX self.config is already a dict with raw config values + self.cfg = dacite.from_dict(data_class=NessusConfig, data=nessus_config_section) self.sleep_interval: int = 20 self._connect() def _connect(self): - logging.debug(f"Connecting to nessus instance at {self.config.server.url}") + logging.debug(f"Connecting to nessus instance at {self.cfg.server.url}") try: self._nessus_client = PyNessusPro( - self.config.server.url, - self.config.server.username, - self.config.server.password, + self.cfg.server.url, + self.cfg.server.username, + self.cfg.server.password, log_level="debug", ) except requests.exceptions.RequestException as e: - logging.error(f"Failed to connect to {self.config.server.url}: {e}") + logging.error(f"Failed to connect to {self.cfg.server.url}: {e}") raise @property @@ -88,19 +89,19 @@ def scan_id(self) -> int: return self._scan_id def setup(self): - logging.debug(f"Creating new scan named {self.config.scan.folder}/{self.config.scan.name}") + logging.debug(f"Creating new scan named {self.cfg.scan.folder}/{self.cfg.scan.name}") self._scan_id = self.nessus_client.new_scan( - name=self.config.scan.name, - targets=self.config.scan.targets_as_str(), - folder=self.config.scan.folder, + name=self.cfg.scan.name, + targets=self.cfg.scan.targets_as_str(), + folder=self.cfg.scan.folder, create_folder=True, ) # only user-created scan policies seem to be identified and must be # created with the name used in the config as a prerequisite - if self.config.scan.policy: - logging.debug(f"Setting scan policy to {self.config.scan.policy}") - self.nessus_client.set_scan_policy(scan_id=self.scan_id, policy=self.config.scan.policy) + if self.cfg.scan.policy: + logging.debug(f"Setting scan policy to {self.cfg.scan.policy}") + self.nessus_client.set_scan_policy(scan_id=self.scan_id, policy=self.cfg.scan.policy) self.state = State.READY From c6453219ed5dc9d43421260d2df78cedc2bcdfd4 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 5 Nov 2024 10:46:33 +1000 Subject: [PATCH 7/9] update based on review feedback --- scanners/nessus/nessus_none.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/scanners/nessus/nessus_none.py b/scanners/nessus/nessus_none.py index 1516cb39..43f5cc4d 100644 --- a/scanners/nessus/nessus_none.py +++ b/scanners/nessus/nessus_none.py @@ -60,7 +60,8 @@ def __init__(self, config: RapidastConfigModel, ident: str = "nessus"): # self.auth_config = config.subtree_to_dict("general.authentication") # creds for target hosts # XXX self.config is already a dict with raw config values self.cfg = dacite.from_dict(data_class=NessusConfig, data=nessus_config_section) - self.sleep_interval: int = 20 + self._sleep_interval: int = 10 + self._timeout: int = 300 self._connect() def _connect(self): @@ -97,6 +98,9 @@ def setup(self): create_folder=True, ) + if self._scan_id < 0: + raise RuntimeError(f"Unexpected scan_id {self.scan_id}") + # only user-created scan policies seem to be identified and must be # created with the name used in the config as a prerequisite if self.cfg.scan.policy: @@ -116,9 +120,15 @@ def run(self): self.nessus_client.post_scan(scan_id=self.scan_id) # Wait for the scan to complete + start = time.time() while self.nessus_client.get_scan_status(self.scan_id)["status"] not in END_STATUSES: - time.sleep(self.sleep_interval) - logging.debug("Waiting {self.sleep_interval}s for scan to finish") + if time.time() - start > self._timeout: + logging.error(f"Timeout {self._timeout}s reached waiting for scan to complete") + self.state = State.ERROR + break + + time.sleep(self._sleep_interval) + logging.debug(f"Waiting {self._sleep_interval}s for scan to finish") logging.info(self.nessus_client.get_scan_status(self.scan_id)) def postprocess(self): @@ -126,7 +136,7 @@ def postprocess(self): # Path and any folders must already exist in this implementation logging.debug("Retrieving scan reports") scan_reports = self.nessus_client.get_scan_reports(self.scan_id, self.results_dir) - print(scan_reports) + logging.debug(scan_reports) if not self.state == State.ERROR: self.state = State.PROCESSED From 76e89fd517381df453037d29b52bc5f5a7fc77fb Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 6 Nov 2024 09:48:34 +1000 Subject: [PATCH 8/9] Add timeout config option for nessus scan --- config/config-template-nessus.yaml | 1 + scanners/nessus/nessus_none.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/config/config-template-nessus.yaml b/config/config-template-nessus.yaml index 1b4ed1c7..39d5997f 100644 --- a/config/config-template-nessus.yaml +++ b/config/config-template-nessus.yaml @@ -37,5 +37,6 @@ scanners: name: test-scan # name of new scan to create folder: test-folder # name of folder in to contain scan policy: "py-test" # policy used for scan + # timeout: 600 # timeout in seconds to complete scan targets: - 127.0.0.1 diff --git a/scanners/nessus/nessus_none.py b/scanners/nessus/nessus_none.py index 43f5cc4d..6405cf0b 100644 --- a/scanners/nessus/nessus_none.py +++ b/scanners/nessus/nessus_none.py @@ -27,6 +27,7 @@ class NessusScanConfig: policy: str targets: List[str] folder: str = field(default="rapidast") + timeout: int = field(default=600) # seconds def targets_as_str(self) -> str: return " ".join(self.targets) @@ -61,7 +62,6 @@ def __init__(self, config: RapidastConfigModel, ident: str = "nessus"): # XXX self.config is already a dict with raw config values self.cfg = dacite.from_dict(data_class=NessusConfig, data=nessus_config_section) self._sleep_interval: int = 10 - self._timeout: int = 300 self._connect() def _connect(self): @@ -122,8 +122,8 @@ def run(self): # Wait for the scan to complete start = time.time() while self.nessus_client.get_scan_status(self.scan_id)["status"] not in END_STATUSES: - if time.time() - start > self._timeout: - logging.error(f"Timeout {self._timeout}s reached waiting for scan to complete") + if time.time() - start > self.cfg.scan.timeout: + logging.error(f"Timeout {self.cfg.scan.timeout}s reached waiting for scan to complete") self.state = State.ERROR break From 56d836f3a70463ae20c62509b414c0b10f0ed35f Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 6 Nov 2024 10:27:51 +1000 Subject: [PATCH 9/9] try and fix postprocess() methods --- scanners/generic/generic.py | 2 +- scanners/zap/zap.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scanners/generic/generic.py b/scanners/generic/generic.py index 6347e5fe..ef7c5274 100644 --- a/scanners/generic/generic.py +++ b/scanners/generic/generic.py @@ -65,7 +65,7 @@ def postprocess(self): logging.info(f"Extracting report, storing in {self.results_dir}") result = self.my_conf("results") try: - os.makedirs(self.results_dir) + os.makedirs(self.results_dir, exist_ok=True) if os.path.isdir(result): shutil.copytree(result, self.results_dir, dirs_exist_ok=True) else: diff --git a/scanners/zap/zap.py b/scanners/zap/zap.py index 41eb0a69..2e93b6e4 100644 --- a/scanners/zap/zap.py +++ b/scanners/zap/zap.py @@ -90,7 +90,7 @@ def postprocess(self): logging.debug(f"reports_dir: {reports_dir}") logging.info(f"Extracting report, storing in {self.results_dir}") - shutil.copytree(reports_dir, self.results_dir) + shutil.copytree(reports_dir, self.results_dir, dirs_exist_ok=True) logging.info("Saving the session as evidence") with tarfile.open(f"{self.results_dir}/session.tar.gz", "w:gz") as tar: