Dependency Checking (#75)

* Adds req testing methodology, needs fixes

* Improves dependency exception handling

* Better meets_requirements implementation

Still need to adjust tests to fake installation

* Changes to exception boolean to enable tool check

tests and class variables modified for new tool check

* Adjust test_get_scans to use appropriate variable

* Adds Go requirement where relevant

* Adds missing scan dependencies

* Add clarification to error message
This commit is contained in:
Ryan Good
2020-08-07 08:48:49 -05:00
committed by GitHub
parent d97315a2da
commit d7dbd1e7b3
21 changed files with 181 additions and 165 deletions

View File

@@ -298,10 +298,14 @@ class ReconShell(cmd2.Cmd):
# get_scans() returns mapping of {classname: [modulename, ...]} in the recon module # get_scans() returns mapping of {classname: [modulename, ...]} in the recon module
# each classname corresponds to a potential recon-pipeline command, i.e. AmassScan, GobusterScan ... # each classname corresponds to a potential recon-pipeline command, i.e. AmassScan, GobusterScan ...
scans = get_scans() scans = get_scans()
# command is a list that will end up looking something like what's below # command is a list that will end up looking something like what's below
# luigi --module pipeline.recon.web.webanalyze WebanalyzeScan --target abc.com --top-ports 100 --interface eth0 # luigi --module pipeline.recon.web.webanalyze WebanalyzeScan --target abc.com --top-ports 100 --interface eth0
command = ["luigi", "--module", scans.get(args.scantype)[0]] try:
command = ["luigi", "--module", scans.get(args.scantype)[0]]
except TypeError:
return self.poutput(
style(f"[!] {args.scantype} or one of its dependencies is not installed", fg="bright_red")
)
tgt_file_path = None tgt_file_path = None
if args.target: if args.target:

View File

@@ -9,7 +9,7 @@ from luigi.contrib.sqla import SQLAlchemyTarget
import pipeline.models.db_manager import pipeline.models.db_manager
from ..tools import tools from ..tools import tools
from .targets import TargetList from .targets import TargetList
from .helpers import get_tool_state from .helpers import meets_requirements
from ..models.target_model import Target from ..models.target_model import Target
@@ -43,21 +43,14 @@ class AmassScan(luigi.Task):
""" """
exempt_list = luigi.Parameter(default="") exempt_list = luigi.Parameter(default="")
requirements = ["go", "amass"]
exception = True
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.db_mgr = pipeline.models.db_manager.DBManager(db_location=self.db_location) self.db_mgr = pipeline.models.db_manager.DBManager(db_location=self.db_location)
self.results_subfolder = (Path(self.results_dir) / "amass-results").expanduser().resolve() self.results_subfolder = (Path(self.results_dir) / "amass-results").expanduser().resolve()
@staticmethod
def meets_requirements():
""" Reports whether or not this scan's needed tool(s) are installed or not """
needs = ["amass"]
tools = get_tool_state()
if tools:
return all([tools.get(x).get("installed") is True for x in needs])
def requires(self): def requires(self):
""" AmassScan depends on TargetList to run. """ AmassScan depends on TargetList to run.
@@ -66,6 +59,7 @@ class AmassScan(luigi.Task):
Returns: Returns:
luigi.ExternalTask - TargetList luigi.ExternalTask - TargetList
""" """
meets_requirements(self.requirements, self.exception)
args = {"target_file": self.target_file, "results_dir": self.results_dir, "db_location": self.db_location} args = {"target_file": self.target_file, "results_dir": self.results_dir, "db_location": self.db_location}
return TargetList(**args) return TargetList(**args)
@@ -89,7 +83,6 @@ class AmassScan(luigi.Task):
Returns: Returns:
list: list of options/arguments, beginning with the name of the executable to run list: list of options/arguments, beginning with the name of the executable to run
""" """
self.results_subfolder.mkdir(parents=True, exist_ok=True) self.results_subfolder.mkdir(parents=True, exist_ok=True)
hostnames = self.db_mgr.get_all_hostnames() hostnames = self.db_mgr.get_all_hostnames()

View File

@@ -6,11 +6,29 @@ import pkgutil
import importlib import importlib
import ipaddress import ipaddress
from pathlib import Path from pathlib import Path
from cmd2.ansi import style
from collections import defaultdict from collections import defaultdict
from ..recon.config import defaults from ..recon.config import defaults
def meets_requirements(requirements, exception):
""" Determine if tools required to perform task are installed. """
tools = get_tool_state()
for tool in requirements:
if not tools.get(tool).get("installed"):
if exception:
raise RuntimeError(
style(f"[!!] {tool} is not installed, and is required to run this scan", fg="bright_red")
)
else:
return False
return True
def get_tool_state() -> typing.Union[dict, None]: def get_tool_state() -> typing.Union[dict, None]:
""" Load current tool state from disk. """ """ Load current tool state from disk. """
tools = Path(defaults.get("tools-dir")) / ".tool-dict.pkl" tools = Path(defaults.get("tools-dir")) / ".tool-dict.pkl"
@@ -60,7 +78,9 @@ def get_scans():
# final check, this ensures that the tools necessary to AT LEAST run this scan are present # final check, this ensures that the tools necessary to AT LEAST run this scan are present
# does not consider upstream dependencies # does not consider upstream dependencies
try: try:
if not sub_obj.meets_requirements(): requirements = sub_obj.requirements
exception = False # let meets_req know we want boolean result
if not meets_requirements(requirements, exception):
continue continue
except AttributeError: except AttributeError:
# some scan's haven't implemented meets_requirements yet, silently allow them through # some scan's haven't implemented meets_requirements yet, silently allow them through

View File

@@ -14,7 +14,7 @@ from .amass import ParseAmassOutput
from ..models.port_model import Port from ..models.port_model import Port
from ..models.ip_address_model import IPAddress from ..models.ip_address_model import IPAddress
from .helpers import get_tool_state from .helpers import meets_requirements
from .config import top_tcp_ports, top_udp_ports, defaults, web_ports from .config import top_tcp_ports, top_udp_ports, defaults, web_ports
@@ -58,21 +58,14 @@ class MasscanScan(luigi.Task):
interface = luigi.Parameter(default=defaults.get("masscan-iface")) interface = luigi.Parameter(default=defaults.get("masscan-iface"))
top_ports = luigi.IntParameter(default=0) # IntParameter -> top_ports expected as int top_ports = luigi.IntParameter(default=0) # IntParameter -> top_ports expected as int
ports = luigi.Parameter(default="") ports = luigi.Parameter(default="")
requirements = ["masscan"]
exception = True
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.db_mgr = pipeline.models.db_manager.DBManager(db_location=self.db_location) self.db_mgr = pipeline.models.db_manager.DBManager(db_location=self.db_location)
self.results_subfolder = (Path(self.results_dir) / "masscan-results").expanduser().resolve() self.results_subfolder = (Path(self.results_dir) / "masscan-results").expanduser().resolve()
@staticmethod
def meets_requirements():
""" Reports whether or not this scan's needed tool(s) are installed or not """
needs = ["masscan"]
tools = get_tool_state()
if tools:
return all([tools.get(x).get("installed") is True for x in needs])
def output(self): def output(self):
""" Returns the target output for this task. """ Returns the target output for this task.
@@ -91,6 +84,7 @@ class MasscanScan(luigi.Task):
Returns: Returns:
list: list of options/arguments, beginning with the name of the executable to run list: list of options/arguments, beginning with the name of the executable to run
""" """
meets_requirements(self.requirements, self.exception)
if not self.ports and not self.top_ports: if not self.ports and not self.top_ports:
# need at least one, can't be put into argparse scanner because things like amass don't require ports option # need at least one, can't be put into argparse scanner because things like amass don't require ports option
logging.error("Must specify either --top-ports or --ports.") logging.error("Must specify either --top-ports or --ports.")

View File

@@ -3,6 +3,8 @@ import logging
import subprocess import subprocess
import concurrent.futures import concurrent.futures
from pathlib import Path from pathlib import Path
from shutil import which
from cmd2.ansi import style
import luigi import luigi
import sqlalchemy import sqlalchemy
@@ -13,10 +15,9 @@ from luigi.contrib.sqla import SQLAlchemyTarget
import pipeline.models.db_manager import pipeline.models.db_manager
from .masscan import ParseMasscanOutput from .masscan import ParseMasscanOutput
from .config import defaults from .config import defaults
from .helpers import get_ip_address_version, is_ip_address from .helpers import get_ip_address_version, is_ip_address, meets_requirements
from ..tools import tools from ..tools import tools
from .helpers import get_tool_state
from ..models.port_model import Port from ..models.port_model import Port
from ..models.nse_model import NSEResult from ..models.nse_model import NSEResult
from ..models.target_model import Target from ..models.target_model import Target
@@ -58,6 +59,8 @@ class ThreadedNmapScan(luigi.Task):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if not which("nmap"):
raise RuntimeError(style("[!] nmap is not installed", fg="bright_red"))
self.db_mgr = pipeline.models.db_manager.DBManager(db_location=self.db_location) self.db_mgr = pipeline.models.db_manager.DBManager(db_location=self.db_location)
self.results_subfolder = (Path(self.results_dir) / "nmap-results").expanduser().resolve() self.results_subfolder = (Path(self.results_dir) / "nmap-results").expanduser().resolve()
@@ -238,19 +241,13 @@ class SearchsploitScan(luigi.Task):
results_dir: specifies the directory on disk to which all Task results are written *Required by upstream Task* results_dir: specifies the directory on disk to which all Task results are written *Required by upstream Task*
""" """
requirements = ["searchsploit"]
exception = True
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.db_mgr = pipeline.models.db_manager.DBManager(db_location=self.db_location) self.db_mgr = pipeline.models.db_manager.DBManager(db_location=self.db_location)
@staticmethod
def meets_requirements():
""" Reports whether or not this scan's needed tool(s) are installed or not """
needs = ["searchsploit"]
tools = get_tool_state()
if tools:
return all([tools.get(x).get("installed") is True for x in needs])
def requires(self): def requires(self):
""" Searchsploit depends on ThreadedNmap to run. """ Searchsploit depends on ThreadedNmap to run.
@@ -261,6 +258,7 @@ class SearchsploitScan(luigi.Task):
Returns: Returns:
luigi.Task - ThreadedNmap luigi.Task - ThreadedNmap
""" """
meets_requirements(self.requirements, self.exception)
args = { args = {
"rate": self.rate, "rate": self.rate,
"ports": self.ports, "ports": self.ports,

View File

@@ -13,7 +13,7 @@ from ..config import defaults
from ...tools import tools from ...tools import tools
import pipeline.models.db_manager import pipeline.models.db_manager
from ..helpers import get_tool_state from ..helpers import meets_requirements
from ...models.port_model import Port from ...models.port_model import Port
from ...models.header_model import Header from ...models.header_model import Header
from ...models.endpoint_model import Endpoint from ...models.endpoint_model import Endpoint
@@ -58,21 +58,14 @@ class AquatoneScan(luigi.Task):
threads = luigi.Parameter(default=defaults.get("threads", "")) threads = luigi.Parameter(default=defaults.get("threads", ""))
scan_timeout = luigi.Parameter(default=defaults.get("aquatone-scan-timeout", "")) scan_timeout = luigi.Parameter(default=defaults.get("aquatone-scan-timeout", ""))
requirements = ["aquatone", "masscan"]
exception = True
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.db_mgr = pipeline.models.db_manager.DBManager(db_location=self.db_location) self.db_mgr = pipeline.models.db_manager.DBManager(db_location=self.db_location)
self.results_subfolder = Path(self.results_dir) / "aquatone-results" self.results_subfolder = Path(self.results_dir) / "aquatone-results"
@staticmethod
def meets_requirements():
""" Reports whether or not this scan's needed tool(s) are installed or not """
needs = ["aquatone"]
tools = get_tool_state()
if tools:
return all([tools.get(x).get("installed") is True for x in needs])
def requires(self): def requires(self):
""" AquatoneScan depends on GatherWebTargets to run. """ AquatoneScan depends on GatherWebTargets to run.
@@ -82,6 +75,7 @@ class AquatoneScan(luigi.Task):
Returns: Returns:
luigi.Task - GatherWebTargets luigi.Task - GatherWebTargets
""" """
meets_requirements(self.requirements, self.exception)
args = { args = {
"results_dir": self.results_dir, "results_dir": self.results_dir,
"rate": self.rate, "rate": self.rate,

View File

@@ -12,7 +12,7 @@ from luigi.contrib.sqla import SQLAlchemyTarget
import pipeline.models.db_manager import pipeline.models.db_manager
from ...tools import tools from ...tools import tools
from ..config import defaults from ..config import defaults
from ..helpers import get_tool_state from ..helpers import meets_requirements
from .targets import GatherWebTargets from .targets import GatherWebTargets
from ...models.endpoint_model import Endpoint from ...models.endpoint_model import Endpoint
from ..helpers import get_ip_address_version, is_ip_address from ..helpers import get_ip_address_version, is_ip_address
@@ -59,21 +59,14 @@ class GobusterScan(luigi.Task):
threads = luigi.Parameter(default=defaults.get("threads")) threads = luigi.Parameter(default=defaults.get("threads"))
wordlist = luigi.Parameter(default=defaults.get("gobuster-wordlist")) wordlist = luigi.Parameter(default=defaults.get("gobuster-wordlist"))
extensions = luigi.Parameter(default=defaults.get("gobuster-extensions")) extensions = luigi.Parameter(default=defaults.get("gobuster-extensions"))
requirements = ["recursive-gobuster", "go", "gobuster", "masscan"]
exception = True
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.db_mgr = pipeline.models.db_manager.DBManager(db_location=self.db_location) self.db_mgr = pipeline.models.db_manager.DBManager(db_location=self.db_location)
self.results_subfolder = Path(self.results_dir) / "gobuster-results" self.results_subfolder = Path(self.results_dir) / "gobuster-results"
@staticmethod
def meets_requirements():
""" Reports whether or not this scan's needed tool(s) are installed or not """
needs = ["recursive-gobuster", "gobuster"]
tools = get_tool_state()
if tools:
return all([tools.get(x).get("installed") is True for x in needs])
def requires(self): def requires(self):
""" GobusterScan depends on GatherWebTargets to run. """ GobusterScan depends on GatherWebTargets to run.
@@ -83,6 +76,7 @@ class GobusterScan(luigi.Task):
Returns: Returns:
luigi.Task - GatherWebTargets luigi.Task - GatherWebTargets
""" """
meets_requirements(self.requirements, self.exception)
args = { args = {
"results_dir": self.results_dir, "results_dir": self.results_dir,
"rate": self.rate, "rate": self.rate,

View File

@@ -10,7 +10,7 @@ from luigi.contrib.sqla import SQLAlchemyTarget
import pipeline.models.db_manager import pipeline.models.db_manager
from ...tools import tools from ...tools import tools
from ..config import defaults from ..config import defaults
from ..helpers import get_tool_state from ..helpers import meets_requirements
from .targets import GatherWebTargets from .targets import GatherWebTargets
@@ -47,21 +47,15 @@ class TKOSubsScan(luigi.Task):
results_dir: specifes the directory on disk to which all Task results are written *Required by upstream Task* results_dir: specifes the directory on disk to which all Task results are written *Required by upstream Task*
""" """
requirements = ["go", "tko-subs", "masscan"]
exception = True
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.db_mgr = pipeline.models.db_manager.DBManager(db_location=self.db_location) self.db_mgr = pipeline.models.db_manager.DBManager(db_location=self.db_location)
self.results_subfolder = (Path(self.results_dir) / "tkosubs-results").expanduser().resolve() self.results_subfolder = (Path(self.results_dir) / "tkosubs-results").expanduser().resolve()
self.output_file = self.results_subfolder / "tkosubs.csv" self.output_file = self.results_subfolder / "tkosubs.csv"
@staticmethod
def meets_requirements():
""" Reports whether or not this scan's needed tool(s) are installed or not """
needs = ["tko-subs"]
tools = get_tool_state()
if tools:
return all([tools.get(x).get("installed") is True for x in needs])
def requires(self): def requires(self):
""" TKOSubsScan depends on GatherWebTargets to run. """ TKOSubsScan depends on GatherWebTargets to run.
@@ -71,6 +65,7 @@ class TKOSubsScan(luigi.Task):
Returns: Returns:
luigi.Task - GatherWebTargets luigi.Task - GatherWebTargets
""" """
meets_requirements(self.requirements, self.exception)
args = { args = {
"results_dir": self.results_dir, "results_dir": self.results_dir,
"rate": self.rate, "rate": self.rate,
@@ -177,6 +172,8 @@ class SubjackScan(luigi.Task):
""" """
threads = luigi.Parameter(default=defaults.get("threads")) threads = luigi.Parameter(default=defaults.get("threads"))
requirements = ["go", "subjack", "masscan"]
exception = True
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -184,15 +181,6 @@ class SubjackScan(luigi.Task):
self.results_subfolder = (Path(self.results_dir) / "subjack-results").expanduser().resolve() self.results_subfolder = (Path(self.results_dir) / "subjack-results").expanduser().resolve()
self.output_file = self.results_subfolder / "subjack.txt" self.output_file = self.results_subfolder / "subjack.txt"
@staticmethod
def meets_requirements():
""" Reports whether or not this scan's needed tool(s) are installed or not """
needs = ["subjack"]
tools = get_tool_state()
if tools:
return all([tools.get(x).get("installed") is True for x in needs])
def requires(self): def requires(self):
""" SubjackScan depends on GatherWebTargets to run. """ SubjackScan depends on GatherWebTargets to run.
@@ -202,6 +190,7 @@ class SubjackScan(luigi.Task):
Returns: Returns:
luigi.Task - GatherWebTargets luigi.Task - GatherWebTargets
""" """
meets_requirements(self.requirements, self.exception)
args = { args = {
"results_dir": self.results_dir, "results_dir": self.results_dir,
"rate": self.rate, "rate": self.rate,

View File

@@ -8,7 +8,7 @@ from luigi.contrib.sqla import SQLAlchemyTarget
from .targets import GatherWebTargets from .targets import GatherWebTargets
from ...tools import tools from ...tools import tools
from ..helpers import get_tool_state from ..helpers import meets_requirements
from ...models.endpoint_model import Endpoint from ...models.endpoint_model import Endpoint
import pipeline.models.db_manager import pipeline.models.db_manager
@@ -44,20 +44,14 @@ class WaybackurlsScan(luigi.Task):
results_dir: specifes the directory on disk to which all Task results are written *Required by upstream Task* results_dir: specifes the directory on disk to which all Task results are written *Required by upstream Task*
""" """
requirements = ["go", "waybackurls", "masscan"]
exception = True
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.db_mgr = pipeline.models.db_manager.DBManager(db_location=self.db_location) self.db_mgr = pipeline.models.db_manager.DBManager(db_location=self.db_location)
self.results_subfolder = Path(self.results_dir) / "waybackurls-results" self.results_subfolder = Path(self.results_dir) / "waybackurls-results"
@staticmethod
def meets_requirements():
""" Reports whether or not this scan's needed tool(s) are installed or not """
needs = ["waybackurls"]
tools = get_tool_state()
if tools:
return all([tools.get(x).get("installed") is True for x in needs])
def requires(self): def requires(self):
""" WaybackurlsScan depends on GatherWebTargets to run. """ WaybackurlsScan depends on GatherWebTargets to run.
@@ -67,6 +61,7 @@ class WaybackurlsScan(luigi.Task):
Returns: Returns:
luigi.Task - GatherWebTargets luigi.Task - GatherWebTargets
""" """
meets_requirements(self.requirements, self.exception)
args = { args = {
"results_dir": self.results_dir, "results_dir": self.results_dir,
"rate": self.rate, "rate": self.rate,

View File

@@ -13,7 +13,7 @@ from luigi.contrib.sqla import SQLAlchemyTarget
import pipeline.models.db_manager import pipeline.models.db_manager
from ...tools import tools from ...tools import tools
from ..config import defaults from ..config import defaults
from ..helpers import get_tool_state from ..helpers import meets_requirements
from .targets import GatherWebTargets from .targets import GatherWebTargets
from ...models.technology_model import Technology from ...models.technology_model import Technology
from ..helpers import get_ip_address_version, is_ip_address from ..helpers import get_ip_address_version, is_ip_address
@@ -54,21 +54,14 @@ class WebanalyzeScan(luigi.Task):
""" """
threads = luigi.Parameter(default=defaults.get("threads")) threads = luigi.Parameter(default=defaults.get("threads"))
requirements = ["go", "webanalyze", "masscan"]
exception = True
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.db_mgr = pipeline.models.db_manager.DBManager(db_location=self.db_location) self.db_mgr = pipeline.models.db_manager.DBManager(db_location=self.db_location)
self.results_subfolder = Path(self.results_dir) / "webanalyze-results" self.results_subfolder = Path(self.results_dir) / "webanalyze-results"
@staticmethod
def meets_requirements():
""" Reports whether or not this scan's needed tool(s) are installed or not """
needs = ["webanalyze"]
tools = get_tool_state()
if tools:
return all([tools.get(x).get("installed") is True for x in needs])
def requires(self): def requires(self):
""" WebanalyzeScan depends on GatherWebTargets to run. """ WebanalyzeScan depends on GatherWebTargets to run.
@@ -78,6 +71,7 @@ class WebanalyzeScan(luigi.Task):
Returns: Returns:
luigi.Task - GatherWebTargets luigi.Task - GatherWebTargets
""" """
meets_requirements(self.requirements, self.exception)
args = { args = {
"results_dir": self.results_dir, "results_dir": self.results_dir,
"rate": self.rate, "rate": self.rate,

View File

@@ -2,7 +2,7 @@ import luigi
from luigi.util import inherits from luigi.util import inherits
from .nmap import SearchsploitScan from .nmap import SearchsploitScan
from .helpers import get_tool_state from .helpers import meets_requirements
from .web import AquatoneScan, GobusterScan, SubjackScan, TKOSubsScan, WaybackurlsScan, WebanalyzeScan from .web import AquatoneScan, GobusterScan, SubjackScan, TKOSubsScan, WaybackurlsScan, WebanalyzeScan
@@ -28,28 +28,24 @@ class FullScan(luigi.WrapperTask):
results_dir: specifes the directory on disk to which all Task results are written results_dir: specifes the directory on disk to which all Task results are written
""" """
@staticmethod requirements = [
def meets_requirements(): "amass",
""" Reports whether or not this scan's needed tool(s) are installed or not """ "aquatone",
needs = [ "masscan",
"amass", "tko-subs",
"aquatone", "recursive-gobuster",
"masscan", "searchsploit",
"tko-subs", "subjack",
"recursive-gobuster", "gobuster",
"searchsploit", "webanalyze",
"subjack", "waybackurls",
"gobuster", "go",
"webanalyze", ]
"waybackurls", exception = True
]
tools = get_tool_state()
if tools:
return all([tools.get(x).get("installed") is True for x in needs])
def requires(self): def requires(self):
""" FullScan is a wrapper, as such it requires any Tasks that it wraps. """ """ FullScan is a wrapper, as such it requires any Tasks that it wraps. """
meets_requirements(self.requirements, self.exception)
args = { args = {
"results_dir": self.results_dir, "results_dir": self.results_dir,
"rate": self.rate, "rate": self.rate,
@@ -111,17 +107,12 @@ class HTBScan(luigi.WrapperTask):
results_dir: specifes the directory on disk to which all Task results are written results_dir: specifes the directory on disk to which all Task results are written
""" """
@staticmethod requirements = ["aquatone", "go", "masscan", "recursive-gobuster", "searchsploit", "gobuster", "webanalyze"]
def meets_requirements(): exception = True
""" Reports whether or not this scan's needed tool(s) are installed or not """
needs = ["aquatone", "masscan", "recursive-gobuster", "searchsploit", "gobuster", "webanalyze"]
tools = get_tool_state()
if tools:
return all([tools.get(x).get("installed") is True for x in needs])
def requires(self): def requires(self):
""" HTBScan is a wrapper, as such it requires any Tasks that it wraps. """ """ HTBScan is a wrapper, as such it requires any Tasks that it wraps. """
meets_requirements(self.requirements, self.exception)
args = { args = {
"results_dir": self.results_dir, "results_dir": self.results_dir,
"rate": self.rate, "rate": self.rate,

View File

@@ -23,8 +23,9 @@ class TestAmassScan:
def test_scan_requires(self): def test_scan_requires(self):
with patch("pipeline.recon.TargetList"): with patch("pipeline.recon.TargetList"):
retval = self.scan.requires() with patch("pipeline.recon.amass.meets_requirements"):
assert isinstance(retval, TargetList) retval = self.scan.requires()
assert isinstance(retval, TargetList)
def test_scan_run(self): def test_scan_run(self):
with patch("subprocess.run") as mocked_run: with patch("subprocess.run") as mocked_run:

View File

@@ -1,34 +1,61 @@
import pytest import pytest
from unittest.mock import patch
from pipeline.recon.helpers import get_ip_address_version, get_scans, is_ip_address from pipeline.recon.helpers import get_ip_address_version, get_scans, is_ip_address, meets_requirements
from pipeline.recon import AmassScan, MasscanScan, FullScan, HTBScan, SearchsploitScan, ThreadedNmapScan from pipeline.recon import AmassScan, MasscanScan, FullScan, HTBScan, SearchsploitScan, ThreadedNmapScan
from pipeline.recon.web import GobusterScan, SubjackScan, TKOSubsScan, AquatoneScan, WaybackurlsScan, WebanalyzeScan from pipeline.recon.web import GobusterScan, SubjackScan, TKOSubsScan, AquatoneScan, WaybackurlsScan, WebanalyzeScan
def test_get_scans(): def test_get_scans():
with patch("pipeline.recon.helpers.meets_requirements"):
scan_names = [
AmassScan,
GobusterScan,
MasscanScan,
SubjackScan,
TKOSubsScan,
AquatoneScan,
FullScan,
HTBScan,
SearchsploitScan,
ThreadedNmapScan,
WebanalyzeScan,
WaybackurlsScan,
]
scan_names = [ scans = get_scans()
AmassScan, for scan in scan_names:
GobusterScan, if hasattr(scan, "requirements"):
MasscanScan, assert scan.__name__ in scans.keys()
SubjackScan, else:
TKOSubsScan, assert scan not in scans.keys()
AquatoneScan,
FullScan,
HTBScan,
SearchsploitScan,
ThreadedNmapScan,
WebanalyzeScan,
WaybackurlsScan,
]
scans = get_scans()
for scan in scan_names: @pytest.mark.parametrize(
if hasattr(scan, "meets_requirements") and scan.meets_requirements(): "requirements, exception",
assert scan.__name__ in scans.keys() [
else: (["amass"], True),
assert scan not in scans.keys() (["masscan"], True),
(
[
"amass",
"aquatone",
"masscan",
"tko-subs",
"recursive-gobuster",
"searchsploit",
"subjack",
"gobuster",
"webanalyze",
"waybackurls",
],
False,
),
],
)
def test_meets_requirements(requirements, exception):
with patch("pipeline.recon.helpers.get_tool_state"):
assert meets_requirements(requirements, exception)
@pytest.mark.parametrize( @pytest.mark.parametrize(

View File

@@ -17,6 +17,7 @@ class TestMasscanScan:
self.scan = MasscanScan( self.scan = MasscanScan(
target_file=__file__, results_dir=str(self.tmp_path), db_location=str(self.tmp_path / "testing.sqlite") target_file=__file__, results_dir=str(self.tmp_path), db_location=str(self.tmp_path / "testing.sqlite")
) )
self.scan.exception = False
def teardown_method(self): def teardown_method(self):
shutil.rmtree(self.tmp_path) shutil.rmtree(self.tmp_path)

View File

@@ -15,18 +15,23 @@ nmap_results = Path(__file__).parent.parent / "data" / "recon-results" / "nmap-r
class TestThreadedNmapScan: class TestThreadedNmapScan:
def setup_method(self): def setup_method(self):
self.tmp_path = Path(tempfile.mkdtemp()) with patch("pipeline.recon.nmap.which"):
self.scan = ThreadedNmapScan( self.tmp_path = Path(tempfile.mkdtemp())
target_file=__file__, results_dir=str(self.tmp_path), db_location=str(self.tmp_path / "testing.sqlite") shutil.which = MagicMock()
) shutil.which.return_value = True
self.scan = ThreadedNmapScan(
target_file=__file__, results_dir=str(self.tmp_path), db_location=str(self.tmp_path / "testing.sqlite")
)
self.scan.exception = False
def teardown_method(self): def teardown_method(self):
shutil.rmtree(self.tmp_path) shutil.rmtree(self.tmp_path)
def test_scan_requires(self): def test_scan_requires(self):
with patch("pipeline.recon.ParseMasscanOutput"): with patch("pipeline.recon.ParseMasscanOutput"):
retval = self.scan.requires() with patch("pipeline.recon.nmap.which"):
assert isinstance(retval, ParseMasscanOutput) retval = self.scan.requires()
assert isinstance(retval, ParseMasscanOutput)
def test_scan_run(self): def test_scan_run(self):
with patch("concurrent.futures.ThreadPoolExecutor.map") as mocked_run: with patch("concurrent.futures.ThreadPoolExecutor.map") as mocked_run:
@@ -75,14 +80,17 @@ class TestSearchsploitScan:
self.scan = SearchsploitScan( self.scan = SearchsploitScan(
target_file=__file__, results_dir=str(self.tmp_path), db_location=str(self.tmp_path / "testing.sqlite") target_file=__file__, results_dir=str(self.tmp_path), db_location=str(self.tmp_path / "testing.sqlite")
) )
self.scan.exception = False
def teardown_method(self): def teardown_method(self):
shutil.rmtree(self.tmp_path) shutil.rmtree(self.tmp_path)
def test_scan_requires(self): def test_scan_requires(self):
with patch("pipeline.recon.ThreadedNmapScan"): with patch("pipeline.recon.ThreadedNmapScan"):
retval = self.scan.requires() with patch("pipeline.recon.nmap.meets_requirements"):
assert isinstance(retval, ThreadedNmapScan) with patch("pipeline.recon.nmap.which"):
retval = self.scan.requires()
assert isinstance(retval, ThreadedNmapScan)
def test_scan_output(self): def test_scan_output(self):
retval = self.scan.output() retval = self.scan.output()

View File

@@ -15,14 +15,16 @@ class TestAquatoneScan:
self.scan = AquatoneScan( self.scan = AquatoneScan(
target_file=__file__, results_dir=str(self.tmp_path), db_location=str(self.tmp_path / "testing.sqlite") target_file=__file__, results_dir=str(self.tmp_path), db_location=str(self.tmp_path / "testing.sqlite")
) )
self.scan.exception = False
def teardown_method(self): def teardown_method(self):
shutil.rmtree(self.tmp_path) shutil.rmtree(self.tmp_path)
def test_scan_requires(self): def test_scan_requires(self):
with patch("pipeline.recon.web.GatherWebTargets"): with patch("pipeline.recon.web.GatherWebTargets"):
retval = self.scan.requires() with patch("pipeline.recon.web.aquatone.meets_requirements"):
assert isinstance(retval, GatherWebTargets) retval = self.scan.requires()
assert isinstance(retval, GatherWebTargets)
def test_scan_creates_results_dir(self): def test_scan_creates_results_dir(self):
assert self.scan.results_subfolder == self.tmp_path / "aquatone-results" assert self.scan.results_subfolder == self.tmp_path / "aquatone-results"

View File

@@ -14,14 +14,16 @@ class TestGobusterScan:
self.scan = GobusterScan( self.scan = GobusterScan(
target_file=__file__, results_dir=str(self.tmp_path), db_location=str(self.tmp_path / "testing.sqlite") target_file=__file__, results_dir=str(self.tmp_path), db_location=str(self.tmp_path / "testing.sqlite")
) )
self.scan.exception = False
def teardown_method(self): def teardown_method(self):
shutil.rmtree(self.tmp_path) shutil.rmtree(self.tmp_path)
def test_scan_requires(self): def test_scan_requires(self):
with patch("pipeline.recon.web.GatherWebTargets"): with patch("pipeline.recon.web.GatherWebTargets"):
retval = self.scan.requires() with patch("pipeline.recon.web.gobuster.meets_requirements"):
assert isinstance(retval, GatherWebTargets) retval = self.scan.requires()
assert isinstance(retval, GatherWebTargets)
def test_scan_run(self): def test_scan_run(self):
with patch("concurrent.futures.ThreadPoolExecutor.map") as mocked_run: with patch("concurrent.futures.ThreadPoolExecutor.map") as mocked_run:

View File

@@ -17,14 +17,16 @@ class TestTKOSubsScanScan:
self.scan = TKOSubsScan( self.scan = TKOSubsScan(
target_file=__file__, results_dir=str(self.tmp_path), db_location=str(self.tmp_path / "testing.sqlite") target_file=__file__, results_dir=str(self.tmp_path), db_location=str(self.tmp_path / "testing.sqlite")
) )
self.scan.exception = False
def teardown_method(self): def teardown_method(self):
shutil.rmtree(self.tmp_path) shutil.rmtree(self.tmp_path)
def test_scan_requires(self): def test_scan_requires(self):
with patch("pipeline.recon.web.GatherWebTargets"): with patch("pipeline.recon.web.GatherWebTargets"):
retval = self.scan.requires() with patch("pipeline.recon.web.subdomain_takeover.meets_requirements"):
assert isinstance(retval, GatherWebTargets) retval = self.scan.requires()
assert isinstance(retval, GatherWebTargets)
def test_scan_creates_results_dir(self): def test_scan_creates_results_dir(self):
assert self.scan.results_subfolder == self.tmp_path / "tkosubs-results" assert self.scan.results_subfolder == self.tmp_path / "tkosubs-results"
@@ -84,14 +86,16 @@ class TestSubjackScan:
self.scan = SubjackScan( self.scan = SubjackScan(
target_file=__file__, results_dir=str(self.tmp_path), db_location=str(self.tmp_path / "testing.sqlite") target_file=__file__, results_dir=str(self.tmp_path), db_location=str(self.tmp_path / "testing.sqlite")
) )
self.scan.exception = False
def teardown_method(self): def teardown_method(self):
shutil.rmtree(self.tmp_path) shutil.rmtree(self.tmp_path)
def test_scan_requires(self): def test_scan_requires(self):
with patch("pipeline.recon.web.GatherWebTargets"): with patch("pipeline.recon.web.GatherWebTargets"):
retval = self.scan.requires() with patch("pipeline.recon.web.subdomain_takeover.meets_requirements"):
assert isinstance(retval, GatherWebTargets) retval = self.scan.requires()
assert isinstance(retval, GatherWebTargets)
def test_scan_creates_results_dir(self): def test_scan_creates_results_dir(self):
assert self.scan.results_subfolder == self.tmp_path / "subjack-results" assert self.scan.results_subfolder == self.tmp_path / "subjack-results"

View File

@@ -13,6 +13,7 @@ class TestGatherWebTargets:
self.scan = GatherWebTargets( self.scan = GatherWebTargets(
target_file=__file__, results_dir=str(self.tmp_path), db_location=str(self.tmp_path / "testing.sqlite") target_file=__file__, results_dir=str(self.tmp_path), db_location=str(self.tmp_path / "testing.sqlite")
) )
self.scan.exception = False
def teardown_method(self): def teardown_method(self):
shutil.rmtree(self.tmp_path) shutil.rmtree(self.tmp_path)

View File

@@ -12,14 +12,16 @@ class TestGatherWebTargets:
self.scan = WaybackurlsScan( self.scan = WaybackurlsScan(
target_file=__file__, results_dir=str(self.tmp_path), db_location=str(self.tmp_path / "testing.sqlite") target_file=__file__, results_dir=str(self.tmp_path), db_location=str(self.tmp_path / "testing.sqlite")
) )
self.scan.exception = False
def teardown_method(self): def teardown_method(self):
shutil.rmtree(self.tmp_path) shutil.rmtree(self.tmp_path)
def test_scan_requires(self): def test_scan_requires(self):
with patch("pipeline.recon.web.GatherWebTargets"): with patch("pipeline.recon.web.GatherWebTargets"):
retval = self.scan.requires() with patch("pipeline.recon.web.waybackurls.meets_requirements"):
assert isinstance(retval, GatherWebTargets) retval = self.scan.requires()
assert isinstance(retval, GatherWebTargets)
def test_scan_creates_database(self): def test_scan_creates_database(self):
assert self.scan.db_mgr.location.exists() assert self.scan.db_mgr.location.exists()

View File

@@ -16,14 +16,16 @@ class TestWebanalyzeScan:
self.scan = WebanalyzeScan( self.scan = WebanalyzeScan(
target_file=__file__, results_dir=str(self.tmp_path), db_location=str(self.tmp_path / "testing.sqlite") target_file=__file__, results_dir=str(self.tmp_path), db_location=str(self.tmp_path / "testing.sqlite")
) )
self.scan.exception = False
def teardown_method(self): def teardown_method(self):
shutil.rmtree(self.tmp_path) shutil.rmtree(self.tmp_path)
def test_scan_requires(self): def test_scan_requires(self):
with patch("pipeline.recon.web.GatherWebTargets"): with patch("pipeline.recon.web.GatherWebTargets"):
retval = self.scan.requires() with patch("pipeline.recon.web.webanalyze.meets_requirements"):
assert isinstance(retval, GatherWebTargets) retval = self.scan.requires()
assert isinstance(retval, GatherWebTargets)
def test_scan_creates_results_dir(self): def test_scan_creates_results_dir(self):
assert self.scan.results_subfolder == self.tmp_path / "webanalyze-results" assert self.scan.results_subfolder == self.tmp_path / "webanalyze-results"