Limit tab completion of scan command to scans with installed components (#74)

* scans without installed requirements dont tab-complete

* added meets_requirements functions to classes

* updated tests

* added check for None case
This commit is contained in:
epi052
2020-06-28 15:33:34 -05:00
committed by GitHub
parent 6ed51a19be
commit 4d1aef2d34
14 changed files with 179 additions and 45 deletions

View File

@@ -7,8 +7,9 @@ from luigi.util import inherits
from luigi.contrib.sqla import SQLAlchemyTarget from luigi.contrib.sqla import SQLAlchemyTarget
import pipeline.models.db_manager import pipeline.models.db_manager
from .targets import TargetList
from ..tools import tools from ..tools import tools
from .targets import TargetList
from .helpers import get_tool_state
from ..models.target_model import Target from ..models.target_model import Target
@@ -48,6 +49,15 @@ class AmassScan(luigi.Task):
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.

View File

@@ -1,4 +1,6 @@
import sys import sys
import pickle
import typing
import inspect import inspect
import pkgutil import pkgutil
import importlib import importlib
@@ -6,6 +8,16 @@ import ipaddress
from pathlib import Path from pathlib import Path
from collections import defaultdict from collections import defaultdict
from ..recon.config import defaults
def get_tool_state() -> typing.Union[dict, None]:
""" Load current tool state from disk. """
tools = Path(defaults.get("tools-dir")) / ".tool-dict.pkl"
if tools.exists():
return pickle.loads(tools.read_bytes())
def get_scans(): def get_scans():
""" Iterates over the recon package and its modules to find all of the classes that end in [Ss]can. """ Iterates over the recon package and its modules to find all of the classes that end in [Ss]can.
@@ -42,10 +54,19 @@ def get_scans():
if inspect.ismodule(obj) and not name.startswith("_"): if inspect.ismodule(obj) and not name.startswith("_"):
# we're only interested in modules that don't begin with _ i.e. magic methods __len__ etc... # we're only interested in modules that don't begin with _ i.e. magic methods __len__ etc...
for subname, subobj in inspect.getmembers(obj): for sub_name, sub_obj in inspect.getmembers(obj):
if inspect.isclass(subobj) and subname.lower().endswith("scan"):
# now we only care about classes that end in [Ss]can # now we only care about classes that end in [Ss]can
scans[subname].append(f"{__package__}.{name}") if inspect.isclass(sub_obj) and sub_name.lower().endswith("scan"):
# final check, this ensures that the tools necessary to AT LEAST run this scan are present
# does not consider upstream dependencies
try:
if not sub_obj.meets_requirements():
continue
except AttributeError:
# some scan's haven't implemented meets_requirements yet, silently allow them through
pass
scans[sub_name].append(f"{__package__}.{name}")
return scans return scans

View File

@@ -14,6 +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 .config import top_tcp_ports, top_udp_ports, defaults, web_ports from .config import top_tcp_ports, top_udp_ports, defaults, web_ports
@@ -63,6 +64,15 @@ class MasscanScan(luigi.Task):
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.

View File

@@ -16,6 +16,7 @@ from .config import defaults
from .helpers import get_ip_address_version, is_ip_address from .helpers import get_ip_address_version, is_ip_address
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
@@ -241,6 +242,15 @@ class SearchsploitScan(luigi.Task):
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.

View File

@@ -13,6 +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 ...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
@@ -63,6 +64,15 @@ class AquatoneScan(luigi.Task):
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.

View File

@@ -10,9 +10,10 @@ from luigi.util import inherits
from luigi.contrib.sqla import SQLAlchemyTarget from luigi.contrib.sqla import SQLAlchemyTarget
import pipeline.models.db_manager import pipeline.models.db_manager
from .targets import GatherWebTargets
from ..config import defaults
from ...tools import tools from ...tools import tools
from ..config import defaults
from ..helpers import get_tool_state
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
@@ -64,6 +65,15 @@ class GobusterScan(luigi.Task):
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.

View File

@@ -9,8 +9,9 @@ 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 GatherWebTargets
from ..config import defaults from ..config import defaults
from ..helpers import get_tool_state
from .targets import GatherWebTargets
@inherits(GatherWebTargets) @inherits(GatherWebTargets)
@@ -52,6 +53,15 @@ class TKOSubsScan(luigi.Task):
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.
@@ -174,6 +184,15 @@ 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.

View File

@@ -8,6 +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 ...models.endpoint_model import Endpoint from ...models.endpoint_model import Endpoint
import pipeline.models.db_manager import pipeline.models.db_manager
@@ -48,6 +49,15 @@ class WaybackurlsScan(luigi.Task):
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.

View File

@@ -12,8 +12,9 @@ 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 GatherWebTargets
from ..config import defaults from ..config import defaults
from ..helpers import get_tool_state
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
@@ -59,6 +60,15 @@ class WebanalyzeScan(luigi.Task):
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.

View File

@@ -2,6 +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 .web import AquatoneScan, GobusterScan, SubjackScan, TKOSubsScan, WaybackurlsScan, WebanalyzeScan from .web import AquatoneScan, GobusterScan, SubjackScan, TKOSubsScan, WaybackurlsScan, WebanalyzeScan
@@ -27,6 +28,26 @@ 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
def meets_requirements():
""" Reports whether or not this scan's needed tool(s) are installed or not """
needs = [
"amass",
"aquatone",
"masscan",
"tko-subs",
"recursive-gobuster",
"searchsploit",
"subjack",
"gobuster",
"webanalyze",
"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):
""" 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. """
args = { args = {
@@ -90,6 +111,15 @@ 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
def meets_requirements():
""" 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. """
args = { args = {

View File

@@ -1,48 +1,34 @@
import pytest import pytest
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
from pipeline.recon import AmassScan, MasscanScan, FullScan, HTBScan, SearchsploitScan, ThreadedNmapScan
from pipeline.recon.web import GobusterScan, SubjackScan, TKOSubsScan, AquatoneScan, WaybackurlsScan, WebanalyzeScan
def test_get_scans(): def test_get_scans():
scans = get_scans()
scan_names = [ scan_names = [
"AmassScan", AmassScan,
"GobusterScan", GobusterScan,
"MasscanScan", MasscanScan,
"SubjackScan", SubjackScan,
"TKOSubsScan", TKOSubsScan,
"AquatoneScan", AquatoneScan,
"FullScan", FullScan,
"HTBScan", HTBScan,
"SearchsploitScan", SearchsploitScan,
"ThreadedNmapScan", ThreadedNmapScan,
"WebanalyzeScan", WebanalyzeScan,
"WaybackurlsScan", WaybackurlsScan,
] ]
assert len(scan_names) == len(scans.keys()) scans = get_scans()
for name in scan_names: for scan in scan_names:
assert name in scans.keys() if hasattr(scan, "meets_requirements") and scan.meets_requirements():
assert scan.__name__ in scans.keys()
modules = [ else:
"pipeline.recon.amass", assert scan not in scans.keys()
"pipeline.recon.masscan",
"pipeline.recon.nmap",
"pipeline.recon.nmap",
"pipeline.recon.web",
"pipeline.recon.web",
"pipeline.recon.web",
"pipeline.recon.web",
"pipeline.recon.web",
"pipeline.recon.web",
"pipeline.recon.wrappers",
"pipeline.recon.wrappers",
]
for module in scans.values():
assert module[0] in modules
@pytest.mark.parametrize( @pytest.mark.parametrize(

View File

@@ -40,6 +40,9 @@ class TestParseMasscanOutput:
) )
self.scan.input = lambda: luigi.LocalTarget(masscan_results) self.scan.input = lambda: luigi.LocalTarget(masscan_results)
def teardown_method(self):
shutil.rmtree(self.tmp_path)
def test_scan_creates_results_dir(self): def test_scan_creates_results_dir(self):
assert self.scan.results_subfolder == self.tmp_path / "masscan-results" assert self.scan.results_subfolder == self.tmp_path / "masscan-results"

View File

@@ -428,12 +428,17 @@ class TestReconShell:
"webbrowser.open", autospec=True "webbrowser.open", autospec=True
) as mocked_web, patch("selectors.DefaultSelector.register", autospec=True) as mocked_selector, patch( ) as mocked_web, patch("selectors.DefaultSelector.register", autospec=True) as mocked_selector, patch(
"cmd2.Cmd.select" "cmd2.Cmd.select"
) as mocked_select: ) as mocked_select, patch(
"pipeline.recon-pipeline.get_scans"
) as mocked_scans:
mocked_select.return_value = "Resume" mocked_select.return_value = "Resume"
mocked_popen.return_value = process_mock mocked_popen.return_value = process_mock
test_input += f" --results-dir {tmp_path / 'mostuff'}" test_input += f" --results-dir {tmp_path / 'mostuff'}"
mocked_scans.return_value = {"FullScan": ["pipeline.recon.wrappers"]}
if db_mgr is None: if db_mgr is None:
self.shell.do_scan(test_input) self.shell.do_scan(test_input)
assert expected in capsys.readouterr().out assert expected in capsys.readouterr().out

View File

@@ -4,8 +4,8 @@ import tempfile
from pathlib import Path from pathlib import Path
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock
from pipeline.tools import tools
from pipeline.recon.web import WebanalyzeScan, GatherWebTargets from pipeline.recon.web import WebanalyzeScan, GatherWebTargets
from pipeline.tools import tools
webanalyze_results = Path(__file__).parent.parent / "data" / "recon-results" / "webanalyze-results" webanalyze_results = Path(__file__).parent.parent / "data" / "recon-results" / "webanalyze-results"