Overseer Server - Python Documentation
This commit is contained in:
parent
6d4cc4a11b
commit
de195834df
@ -20,7 +20,8 @@ def create_app():
|
|||||||
|
|
||||||
@click.group(cls=FlaskGroup, create_app=create_app)
|
@click.group(cls=FlaskGroup, create_app=create_app)
|
||||||
def cli():
|
def cli():
|
||||||
"""Management script for the Wiki application."""
|
"""Management script for the application."""
|
||||||
|
|
||||||
|
|
||||||
|
# Import all flask views
|
||||||
import overseer.overseer # noqa: E501,F401,E402
|
import overseer.overseer # noqa: E501,F401,E402
|
||||||
|
@ -4,8 +4,10 @@ open_websockets = []
|
|||||||
def send_websocket_event(data):
|
def send_websocket_event(data):
|
||||||
"""Send an event to all registered websockets.
|
"""Send an event to all registered websockets.
|
||||||
|
|
||||||
Arguments:
|
Arguments
|
||||||
data -- Data to send over the websocket(s)
|
---------
|
||||||
|
data : obj
|
||||||
|
Data to send over the websocket(s)
|
||||||
"""
|
"""
|
||||||
for socket in open_websockets:
|
for socket in open_websockets:
|
||||||
socket.send(data)
|
socket.send(data)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
|
|
||||||
|
# Setup the API blueprint
|
||||||
api = Blueprint("v1", __name__, url_prefix="/api/v1")
|
api = Blueprint("v1", __name__, url_prefix="/api/v1")
|
||||||
from overseer.api.v1 import routes, events # noqa: F401,E402
|
from overseer.api.v1 import routes, events # noqa: F401,E402
|
||||||
|
@ -2,15 +2,18 @@ from overseer import app
|
|||||||
from overseer.api import open_websockets
|
from overseer.api import open_websockets
|
||||||
from flask_socketio import SocketIO
|
from flask_socketio import SocketIO
|
||||||
|
|
||||||
|
# Create and register new websocket for v1 API
|
||||||
socketio = SocketIO(app, path="/api/v1/socket.io")
|
socketio = SocketIO(app, path="/api/v1/socket.io")
|
||||||
open_websockets.append(socketio)
|
open_websockets.append(socketio)
|
||||||
|
|
||||||
|
|
||||||
@socketio.on("message")
|
@socketio.on("message")
|
||||||
def handle_message(data):
|
def handle_message(data):
|
||||||
|
"""Not Implemented - Used to response to client sent WebSocket messages"""
|
||||||
print("RAW DATA: %s" % data)
|
print("RAW DATA: %s" % data)
|
||||||
|
|
||||||
|
|
||||||
@socketio.on("json")
|
@socketio.on("json")
|
||||||
def handle_json(json):
|
def handle_json(json):
|
||||||
|
"""Not Implemented - Used to response to client sent WebSocket messages"""
|
||||||
print("JSON DATA: %s" % json)
|
print("JSON DATA: %s" % json)
|
||||||
|
@ -40,7 +40,7 @@ def get_scans_by_target(target):
|
|||||||
/api/v1/scans/1.1.1.1
|
/api/v1/scans/1.1.1.1
|
||||||
RESPONSE: { "data": [ <ARRAY_OF_SCAN_HISTORY> ] }
|
RESPONSE: { "data": [ <ARRAY_OF_SCAN_HISTORY> ] }
|
||||||
"""
|
"""
|
||||||
page = 1
|
page = 1 # TODO: Pagination
|
||||||
|
|
||||||
if overseer.scan_manager.is_ip(target):
|
if overseer.scan_manager.is_ip(target):
|
||||||
scan_results = overseer.database.get_scan_results_by_target(
|
scan_results = overseer.database.get_scan_results_by_target(
|
||||||
@ -56,7 +56,7 @@ def get_scans_by_target(target):
|
|||||||
|
|
||||||
@api.route("/search", methods=["GET"])
|
@api.route("/search", methods=["GET"])
|
||||||
def get_scans(search):
|
def get_scans(search):
|
||||||
"""
|
"""Not Implemented
|
||||||
GET:
|
GET:
|
||||||
REQUEST: /api/v1/search?query=1.1.1.1
|
REQUEST: /api/v1/search?query=1.1.1.1
|
||||||
/api/v1/search?query=192.168.0.0/24
|
/api/v1/search?query=192.168.0.0/24
|
||||||
@ -67,6 +67,7 @@ def get_scans(search):
|
|||||||
|
|
||||||
|
|
||||||
def __normalize_scan_results(scan_results, target):
|
def __normalize_scan_results(scan_results, target):
|
||||||
|
"""Returns a normalized list of objects for ScanHistory items"""
|
||||||
return list(
|
return list(
|
||||||
map(
|
map(
|
||||||
lambda x: {
|
lambda x: {
|
||||||
|
@ -2,11 +2,22 @@ import os
|
|||||||
|
|
||||||
|
|
||||||
def get_env(key, default=None, required=False):
|
def get_env(key, default=None, required=False):
|
||||||
|
"""Wrapper for gathering env vars."""
|
||||||
if required:
|
if required:
|
||||||
assert key in os.environ, "Missing Environment Variable: %s" % key
|
assert key in os.environ, "Missing Environment Variable: %s" % key
|
||||||
return os.environ.get(key, default)
|
return os.environ.get(key, default)
|
||||||
|
|
||||||
|
|
||||||
class EnvConfig:
|
class EnvConfig:
|
||||||
|
"""Wrap application configurations
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
DATABASE : str
|
||||||
|
The specied desired database (default: sqlite)
|
||||||
|
DATA_PATH : str
|
||||||
|
The path where to store any resources (default: ./)
|
||||||
|
"""
|
||||||
|
|
||||||
DATABASE = get_env("OVERSEER_DB", default="sqlite")
|
DATABASE = get_env("OVERSEER_DB", default="sqlite")
|
||||||
DATA_PATH = get_env("OVERSEER_DATA_PATH", default="./")
|
DATA_PATH = get_env("OVERSEER_DATA_PATH", default="./")
|
||||||
|
@ -8,23 +8,52 @@ from sqlalchemy.orm.exc import NoResultFound
|
|||||||
|
|
||||||
|
|
||||||
class DatabaseConnector:
|
class DatabaseConnector:
|
||||||
|
"""Used to manipulate the database
|
||||||
|
|
||||||
|
Methods
|
||||||
|
-------
|
||||||
|
create_scan_target(**kwargs)
|
||||||
|
Create a new ScanTarget database row with the provided hostname or
|
||||||
|
ip_addr
|
||||||
|
get_scan_target(**kwargs)
|
||||||
|
Get a ScanTarget by either its hostname or ip_addr
|
||||||
|
get_all_scan_targets(page=1)
|
||||||
|
Get all ScanTargets ordered by most recent, limited to 25 / page
|
||||||
|
create_scan_result(status, results=[], error=None, **kwargs)
|
||||||
|
Create a new ScanHistory for the provided hostname or ip_addr
|
||||||
|
update_scan_result(history_id, status, results=None, error=None)
|
||||||
|
Update the referenced ScanHistory ID with the provided values
|
||||||
|
get_scan_results_by_target(page=1, **kwargs)
|
||||||
|
Get all ScanHistory for the provided hostname or ip_add, limited to
|
||||||
|
25 / page
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, data_path, in_memory=False):
|
def __init__(self, data_path, in_memory=False):
|
||||||
|
"""
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
data_path : str
|
||||||
|
Directory to store the sqlite file
|
||||||
|
in_memory : bool, optional
|
||||||
|
Directive to store the DB in memory
|
||||||
|
"""
|
||||||
if in_memory:
|
if in_memory:
|
||||||
self.engine = create_engine(
|
self.__engine = create_engine(
|
||||||
"sqlite+pysqlite:///:memory:", echo=False, future=True
|
"sqlite+pysqlite:///:memory:", echo=False, future=True
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
db_path = path.join(data_path, "overseer.sqlite")
|
db_path = path.join(data_path, "overseer.sqlite")
|
||||||
self.engine = create_engine(
|
self.__engine = create_engine(
|
||||||
"sqlite+pysqlite:///%s" % db_path,
|
"sqlite+pysqlite:///%s" % db_path,
|
||||||
echo=False,
|
echo=False,
|
||||||
future=True,
|
future=True,
|
||||||
)
|
)
|
||||||
Base.metadata.create_all(self.engine)
|
Base.metadata.create_all(self.__engine)
|
||||||
self.__cleanup_stale_records()
|
self.__cleanup_stale_records()
|
||||||
|
|
||||||
def __cleanup_stale_records(self):
|
def __cleanup_stale_records(self):
|
||||||
session = Session(bind=self.engine)
|
"""Cleans up any stale ScanHistory records"""
|
||||||
|
session = Session(bind=self.__engine)
|
||||||
history_filter = ScanHistory.status == "IN_PROGRESS"
|
history_filter = ScanHistory.status == "IN_PROGRESS"
|
||||||
all_stale = session.query(ScanHistory).filter(history_filter).all()
|
all_stale = session.query(ScanHistory).filter(history_filter).all()
|
||||||
|
|
||||||
@ -36,6 +65,19 @@ class DatabaseConnector:
|
|||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
def create_scan_target(self, **kwargs):
|
def create_scan_target(self, **kwargs):
|
||||||
|
"""Create a new ScanTarget database row with the provided hostname or
|
||||||
|
ip_addr
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
**kwargs
|
||||||
|
Either hostname or ip_addr
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
ScanTarget
|
||||||
|
The created ScanTarget
|
||||||
|
"""
|
||||||
if len(kwargs.keys() & {"ip_addr", "hostname"}) != 1:
|
if len(kwargs.keys() & {"ip_addr", "hostname"}) != 1:
|
||||||
raise ValueError("Missing keyword argument: ip_addr or hostname")
|
raise ValueError("Missing keyword argument: ip_addr or hostname")
|
||||||
|
|
||||||
@ -47,13 +89,25 @@ class DatabaseConnector:
|
|||||||
hostname=kwargs["hostname"], updated_at=datetime.now()
|
hostname=kwargs["hostname"], updated_at=datetime.now()
|
||||||
)
|
)
|
||||||
|
|
||||||
session = Session(bind=self.engine, expire_on_commit=False)
|
session = Session(bind=self.__engine, expire_on_commit=False)
|
||||||
session.add(scan_target)
|
session.add(scan_target)
|
||||||
session.commit()
|
session.commit()
|
||||||
session.close()
|
session.close()
|
||||||
return scan_target
|
return scan_target
|
||||||
|
|
||||||
def get_scan_target(self, **kwargs):
|
def get_scan_target(self, **kwargs):
|
||||||
|
"""Get a ScanTarget by either its hostname or ip_addr
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
**kwargs
|
||||||
|
Either hostname or ip_addr
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
ScanTarget
|
||||||
|
The requested ScanTarget
|
||||||
|
"""
|
||||||
if len(kwargs.keys() & {"ip_addr", "hostname"}) != 1:
|
if len(kwargs.keys() & {"ip_addr", "hostname"}) != 1:
|
||||||
raise ValueError("Missing keyword argument: ip_addr or hostname")
|
raise ValueError("Missing keyword argument: ip_addr or hostname")
|
||||||
|
|
||||||
@ -63,13 +117,25 @@ class DatabaseConnector:
|
|||||||
elif "hostname" in kwargs:
|
elif "hostname" in kwargs:
|
||||||
target_filter = ScanTarget.hostname == kwargs["hostname"]
|
target_filter = ScanTarget.hostname == kwargs["hostname"]
|
||||||
|
|
||||||
session = Session(bind=self.engine)
|
session = Session(bind=self.__engine)
|
||||||
scan_target = session.query(ScanTarget).filter(target_filter).first()
|
scan_target = session.query(ScanTarget).filter(target_filter).first()
|
||||||
session.close()
|
session.close()
|
||||||
return scan_target
|
return scan_target
|
||||||
|
|
||||||
def get_all_scan_targets(self, page=1):
|
def get_all_scan_targets(self, page=1):
|
||||||
session = Session(bind=self.engine)
|
"""Get all ScanTargets ordered by most recent, limited to 25 / page
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
page : int, optional
|
||||||
|
The desired ScanTarget page
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
list
|
||||||
|
List of ScanTarget
|
||||||
|
"""
|
||||||
|
session = Session(bind=self.__engine)
|
||||||
scan_targets = (
|
scan_targets = (
|
||||||
session.query(ScanTarget)
|
session.query(ScanTarget)
|
||||||
.order_by(ScanTarget.updated_at.desc())
|
.order_by(ScanTarget.updated_at.desc())
|
||||||
@ -81,6 +147,24 @@ class DatabaseConnector:
|
|||||||
return scan_targets
|
return scan_targets
|
||||||
|
|
||||||
def create_scan_result(self, status, results=[], error=None, **kwargs):
|
def create_scan_result(self, status, results=[], error=None, **kwargs):
|
||||||
|
"""Create a new ScanHistory for the provided hostname or ip_addr
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
status : str
|
||||||
|
The status of the scan (IN_PROGRESS, FAILED, COMPLETE)
|
||||||
|
results : list
|
||||||
|
List of strings of open ports (E.g. ["53 UDP", "53 TCP"]
|
||||||
|
error : str, optional
|
||||||
|
Error message, if any
|
||||||
|
**kwargs
|
||||||
|
Either hostname or ip_addr
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
ScanHistory
|
||||||
|
The created ScanHistory
|
||||||
|
"""
|
||||||
scan_target = self.get_scan_target(**kwargs)
|
scan_target = self.get_scan_target(**kwargs)
|
||||||
if not scan_target:
|
if not scan_target:
|
||||||
scan_target = self.create_scan_target(**kwargs)
|
scan_target = self.create_scan_target(**kwargs)
|
||||||
@ -92,14 +176,32 @@ class DatabaseConnector:
|
|||||||
error=error,
|
error=error,
|
||||||
created_at=datetime.now(),
|
created_at=datetime.now(),
|
||||||
)
|
)
|
||||||
session = Session(bind=self.engine, expire_on_commit=False)
|
session = Session(bind=self.__engine, expire_on_commit=False)
|
||||||
session.add(scan_history)
|
session.add(scan_history)
|
||||||
session.commit()
|
session.commit()
|
||||||
session.close()
|
session.close()
|
||||||
return scan_history
|
return scan_history
|
||||||
|
|
||||||
def update_scan_result(self, history_id, status, results=None, error=None):
|
def update_scan_result(self, history_id, status, results=None, error=None):
|
||||||
session = Session(bind=self.engine, expire_on_commit=False)
|
"""Update the referenced ScanHistory ID with the provided values
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
history_id : int
|
||||||
|
The ScanHistory ID to update
|
||||||
|
status : str
|
||||||
|
The status of the scan (IN_PROGRESS, FAILED, COMPLETE)
|
||||||
|
results : list, optional
|
||||||
|
List of strings of open ports (E.g. ["53 UDP", "53 TCP"]
|
||||||
|
error : str, optional
|
||||||
|
Error message, if any
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
ScanHistory
|
||||||
|
The updated ScanHistory
|
||||||
|
"""
|
||||||
|
session = Session(bind=self.__engine, expire_on_commit=False)
|
||||||
scan_history = session.query(ScanHistory).get(history_id)
|
scan_history = session.query(ScanHistory).get(history_id)
|
||||||
|
|
||||||
if scan_history is None:
|
if scan_history is None:
|
||||||
@ -116,6 +218,19 @@ class DatabaseConnector:
|
|||||||
return scan_history
|
return scan_history
|
||||||
|
|
||||||
def get_scan_results_by_target(self, page=1, **kwargs):
|
def get_scan_results_by_target(self, page=1, **kwargs):
|
||||||
|
"""Get all ScanHistory for the provided hostname or ip_add, limited to
|
||||||
|
25 / page
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
page : int, optional
|
||||||
|
The desired ScanResult page
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
list
|
||||||
|
List of ScanHistory
|
||||||
|
"""
|
||||||
if len(kwargs.keys() & {"ip_addr", "hostname"}) != 1:
|
if len(kwargs.keys() & {"ip_addr", "hostname"}) != 1:
|
||||||
raise ValueError("Missing keyword argument: ip_addr or hostname")
|
raise ValueError("Missing keyword argument: ip_addr or hostname")
|
||||||
|
|
||||||
@ -125,7 +240,7 @@ class DatabaseConnector:
|
|||||||
elif "hostname" in kwargs:
|
elif "hostname" in kwargs:
|
||||||
history_filter = ScanTarget.hostname == kwargs["hostname"]
|
history_filter = ScanTarget.hostname == kwargs["hostname"]
|
||||||
|
|
||||||
session = Session(bind=self.engine)
|
session = Session(bind=self.__engine)
|
||||||
scan_histories = (
|
scan_histories = (
|
||||||
session.query(ScanHistory)
|
session.query(ScanHistory)
|
||||||
.join(ScanHistory.target)
|
.join(ScanHistory.target)
|
||||||
|
@ -5,6 +5,20 @@ Base = declarative_base()
|
|||||||
|
|
||||||
|
|
||||||
class ScanTarget(Base):
|
class ScanTarget(Base):
|
||||||
|
"""ScanTarget DB Model
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-------
|
||||||
|
id : Column(Integer)
|
||||||
|
The ID in the database
|
||||||
|
ip : Column(Integer), optional
|
||||||
|
The integer represented IP Address in the database
|
||||||
|
hostname : Column(String)
|
||||||
|
The hostname in the database
|
||||||
|
updated_at : Column(DateTime)
|
||||||
|
The DateTime when the ScanTarget was last updated
|
||||||
|
"""
|
||||||
|
|
||||||
__tablename__ = "scan_target"
|
__tablename__ = "scan_target"
|
||||||
|
|
||||||
# Unique ID
|
# Unique ID
|
||||||
@ -20,6 +34,7 @@ class ScanTarget(Base):
|
|||||||
updated_at = Column(DateTime())
|
updated_at = Column(DateTime())
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
"""The string representation of the class."""
|
||||||
return (
|
return (
|
||||||
f"ScanTarget(id={self.id!r}, ip={self.ip!r}, "
|
f"ScanTarget(id={self.id!r}, ip={self.ip!r}, "
|
||||||
f"hostname={self.hostname!r}, updated_at={self.updated_at!r})"
|
f"hostname={self.hostname!r}, updated_at={self.updated_at!r})"
|
||||||
@ -27,6 +42,26 @@ class ScanTarget(Base):
|
|||||||
|
|
||||||
|
|
||||||
class ScanHistory(Base):
|
class ScanHistory(Base):
|
||||||
|
"""ScanHistory DB Model
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-------
|
||||||
|
id : Column(Integer)
|
||||||
|
The ID in the database
|
||||||
|
target_id : Column(Integer)
|
||||||
|
The ScanTarget ID reference
|
||||||
|
results : Column(String)
|
||||||
|
The CSV delimited string representing all open ports.
|
||||||
|
created_at : Column(DateTime)
|
||||||
|
The DateTime when the ScanHistory was created
|
||||||
|
status : Column(String)
|
||||||
|
The status of the ScanHistory (IN_PROGRESS, COMPLETE, FAILED)
|
||||||
|
error : Column(String)
|
||||||
|
The error, if any, of the ScanHistory
|
||||||
|
target : relationship
|
||||||
|
The ScanTarget relationship reference
|
||||||
|
"""
|
||||||
|
|
||||||
__tablename__ = "scan_history"
|
__tablename__ = "scan_history"
|
||||||
|
|
||||||
# Unique ID
|
# Unique ID
|
||||||
@ -51,6 +86,7 @@ class ScanHistory(Base):
|
|||||||
target = relationship("ScanTarget", foreign_keys=[target_id])
|
target = relationship("ScanTarget", foreign_keys=[target_id])
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
"""The string representation of the class."""
|
||||||
return (
|
return (
|
||||||
f"ScanHistory(id={self.id!r}, target_id={self.target_id!r}, "
|
f"ScanHistory(id={self.id!r}, target_id={self.target_id!r}, "
|
||||||
f"results={self.results!r}, created_at={self.created_at!r}, "
|
f"results={self.results!r}, created_at={self.created_at!r}, "
|
||||||
|
@ -5,25 +5,19 @@ from overseer.api.v1 import api as api_v1
|
|||||||
|
|
||||||
@app.route("/", methods=["GET"])
|
@app.route("/", methods=["GET"])
|
||||||
def main_entry():
|
def main_entry():
|
||||||
"""
|
"""Initial Entrypoint to the SPA (i.e. 'index.html')"""
|
||||||
Initial Entrypoint to the SPA (i.e. 'index.html')
|
|
||||||
"""
|
|
||||||
return make_response(render_template("index.html"))
|
return make_response(render_template("index.html"))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/<path:path>", methods=["GET"])
|
@app.route("/<path:path>", methods=["GET"])
|
||||||
def catch_all(path):
|
def catch_all(path):
|
||||||
"""
|
"""Necessary due to client side SPA route handling"""
|
||||||
Necessary due to client side SPA route handling.
|
|
||||||
"""
|
|
||||||
return make_response(render_template("index.html"))
|
return make_response(render_template("index.html"))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/static/<path:path>")
|
@app.route("/static/<path:path>")
|
||||||
def static_resources(path):
|
def static_resources(path):
|
||||||
"""
|
"""Front end static resources"""
|
||||||
Front End Static Resources
|
|
||||||
"""
|
|
||||||
return send_from_directory("static", path)
|
return send_from_directory("static", path)
|
||||||
|
|
||||||
|
|
||||||
|
@ -9,19 +9,39 @@ import time
|
|||||||
|
|
||||||
|
|
||||||
class ScanManager:
|
class ScanManager:
|
||||||
def __init__(self):
|
"""Used to manage any ongoing scans
|
||||||
self.pending_shutdown = False
|
|
||||||
self.active_scans = []
|
|
||||||
self.broadcast_thread = Thread(target=self.__broadcast_thread)
|
|
||||||
self.broadcast_thread.start()
|
|
||||||
|
|
||||||
def __broadcast_thread(self):
|
Methods
|
||||||
while not self.pending_shutdown:
|
-------
|
||||||
|
shutdown()
|
||||||
|
Shutdown and cleanup the scan monitor thread
|
||||||
|
get_status()
|
||||||
|
Get a normalized list of dicts detailing outstanding scans
|
||||||
|
perform_scan(target)
|
||||||
|
Initiate a scan on target (Can be a hostname, or ip_addr)
|
||||||
|
is_ip(target)
|
||||||
|
Determines if a target is a hostname of ip_addr
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Create instance and start thread"""
|
||||||
|
self.__pending_shutdown = False
|
||||||
|
self.__active_scans = []
|
||||||
|
self.__monitor_thread = Thread(target=self.__scan_monitor)
|
||||||
|
self.__monitor_thread.start()
|
||||||
|
|
||||||
|
def __scan_monitor(self):
|
||||||
|
"""Monitors active and completed scans
|
||||||
|
|
||||||
|
Responsible for publishing status to the websockets, cleaning up
|
||||||
|
completed scans, and updating the database accordingly.
|
||||||
|
"""
|
||||||
|
while not self.__pending_shutdown:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
if len(self.active_scans) == 0:
|
if len(self.__active_scans) == 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for scan in self.active_scans:
|
for scan in self.__active_scans:
|
||||||
# WebSocket progress
|
# WebSocket progress
|
||||||
total_progress = (scan.tcp_progress + scan.udp_progress) / 2
|
total_progress = (scan.tcp_progress + scan.udp_progress) / 2
|
||||||
results = scan.get_results()
|
results = scan.get_results()
|
||||||
@ -61,13 +81,21 @@ class ScanManager:
|
|||||||
|
|
||||||
# Cleanup active scan
|
# Cleanup active scan
|
||||||
scan.join()
|
scan.join()
|
||||||
self.active_scans.remove(scan)
|
self.__active_scans.remove(scan)
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
self.pending_shutdown = True
|
"""Shutdown and cleanup the scan monitor thread"""
|
||||||
self.broadcast_thread.join()
|
self.__pending_shutdown = True
|
||||||
|
self.__monitor_thread.join()
|
||||||
|
|
||||||
def get_status(self):
|
def get_status(self):
|
||||||
|
"""Get a normalized list of dicts detailing outstanding scans
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
list
|
||||||
|
List of normalized details on current active scans
|
||||||
|
"""
|
||||||
return list(
|
return list(
|
||||||
map(
|
map(
|
||||||
lambda x: {
|
lambda x: {
|
||||||
@ -83,11 +111,18 @@ class ScanManager:
|
|||||||
(x.tcp_progress + x.udp_progress) / 2
|
(x.tcp_progress + x.udp_progress) / 2
|
||||||
), # noqa: E501
|
), # noqa: E501
|
||||||
},
|
},
|
||||||
self.active_scans,
|
self.__active_scans,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def perform_scan(self, target):
|
def perform_scan(self, target):
|
||||||
|
"""Initiate a scan on target (Can be a hostname, or ip_addr)
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
target : str
|
||||||
|
Either a hostname or IP address of the endpoint to scan
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
target = socket.gethostbyname(target)
|
target = socket.gethostbyname(target)
|
||||||
except socket.error:
|
except socket.error:
|
||||||
@ -104,10 +139,22 @@ class ScanManager:
|
|||||||
|
|
||||||
new_scan = Scanner(target, scan_history)
|
new_scan = Scanner(target, scan_history)
|
||||||
new_scan.start()
|
new_scan.start()
|
||||||
self.active_scans.append(new_scan)
|
self.__active_scans.append(new_scan)
|
||||||
return scan_history
|
return scan_history
|
||||||
|
|
||||||
def is_ip(self, target):
|
def is_ip(self, target):
|
||||||
|
"""Determines if a target is a hostname of ip_addr
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
target : str
|
||||||
|
Either a hostname or IP address of the endpoint to scan
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool
|
||||||
|
Whether the target is an IP or not
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
ipaddress.ip_address(target)
|
ipaddress.ip_address(target)
|
||||||
return True
|
return True
|
||||||
@ -116,12 +163,46 @@ class ScanManager:
|
|||||||
|
|
||||||
|
|
||||||
class Scanner(Thread):
|
class Scanner(Thread):
|
||||||
|
"""Subclass of Thread - used to perform a TCP and UDP scan
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
target : str
|
||||||
|
The hostname or ip_addr to scan
|
||||||
|
scan_history : ScanHistory
|
||||||
|
The ScanHistory DB model reference for this scan
|
||||||
|
tcp_progress : int
|
||||||
|
The current progress percentage of the threaded TCP scan
|
||||||
|
udp_progress : int
|
||||||
|
The current progress percentage of the threaded UDP scan
|
||||||
|
tcp_results : list
|
||||||
|
The current list if ints of open TCP ports
|
||||||
|
udp_results : list
|
||||||
|
The current list if ints of open UDP ports
|
||||||
|
|
||||||
|
Methods
|
||||||
|
-------
|
||||||
|
run()
|
||||||
|
Overridden run method from the Thread superclass. Starts the scan.
|
||||||
|
get_results()
|
||||||
|
Returns a normalized list of string of open ports
|
||||||
|
(E.g. ["53 UDP", "53 TCP"])
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, target, scan_history):
|
def __init__(self, target, scan_history):
|
||||||
|
"""
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
target : str
|
||||||
|
The hostname or ip_addr to scan
|
||||||
|
scan_history : ScanHistory
|
||||||
|
The ScanHistory DB model reference for this scan
|
||||||
|
"""
|
||||||
Thread.__init__(self)
|
Thread.__init__(self)
|
||||||
self.target = target
|
self.target = target
|
||||||
self.scan_history = scan_history
|
self.scan_history = scan_history
|
||||||
|
|
||||||
self.port_count = 1000
|
self.__port_count = 1000
|
||||||
|
|
||||||
self.tcp_progress = 0
|
self.tcp_progress = 0
|
||||||
self.udp_progress = 0
|
self.udp_progress = 0
|
||||||
@ -129,11 +210,19 @@ class Scanner(Thread):
|
|||||||
self.tcp_results = []
|
self.tcp_results = []
|
||||||
self.udp_results = []
|
self.udp_results = []
|
||||||
|
|
||||||
self.udp_payloads = {}
|
self.__udp_payloads = {}
|
||||||
self.__load_nmap_payloads()
|
self.__load_nmap_payloads()
|
||||||
|
|
||||||
def __load_nmap_payloads(self):
|
def __load_nmap_payloads(self):
|
||||||
"""Load and parse nmap UDP payloads"""
|
"""Load and parse nmap UDP payloads
|
||||||
|
|
||||||
|
Because of how UDP is designed, we have to test with specific payloads.
|
||||||
|
This parses nmaps protocol specific payloads [0] in preperation.
|
||||||
|
|
||||||
|
TODO: This should be cached. No need to parse it on every scan.
|
||||||
|
|
||||||
|
[0] https://nmap.org/book/nmap-payloads.html
|
||||||
|
"""
|
||||||
|
|
||||||
# Open file & remove comments
|
# Open file & remove comments
|
||||||
nmap_payloads = os.path.join(
|
nmap_payloads = os.path.join(
|
||||||
@ -165,25 +254,33 @@ class Scanner(Thread):
|
|||||||
start_port = int(port_range[0])
|
start_port = int(port_range[0])
|
||||||
end_port = int(port_range[1])
|
end_port = int(port_range[1])
|
||||||
for port_match in range(start_port, end_port + 1):
|
for port_match in range(start_port, end_port + 1):
|
||||||
if port_match not in self.udp_payloads:
|
if port_match not in self.__udp_payloads:
|
||||||
self.udp_payloads[port_match] = []
|
self.__udp_payloads[port_match] = []
|
||||||
self.udp_payloads[port_match].extend(match_payloads)
|
self.__udp_payloads[port_match].extend(match_payloads)
|
||||||
else:
|
else:
|
||||||
port_match = int(raw_port)
|
port_match = int(raw_port)
|
||||||
if port_match not in self.udp_payloads:
|
if port_match not in self.__udp_payloads:
|
||||||
self.udp_payloads[port_match] = []
|
self.__udp_payloads[port_match] = []
|
||||||
self.udp_payloads[port_match].extend(match_payloads)
|
self.__udp_payloads[port_match].extend(match_payloads)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
"""Overridden run method from the Thread superclass. Starts the scan"""
|
||||||
tcp_thread = Thread(target=self.__scan_tcp)
|
tcp_thread = Thread(target=self.__scan_tcp)
|
||||||
udp_thread = Thread(target=self.__scan_udp)
|
udp_thread = Thread(target=self.__scan_udp)
|
||||||
tcp_thread.start()
|
tcp_thread.start()
|
||||||
udp_thread.start()
|
udp_thread.start()
|
||||||
tcp_thread.join()
|
tcp_thread.join()
|
||||||
udp_thread.join()
|
udp_thread.join()
|
||||||
return {"TCP": self.tcp_results, "UDP": self.udp_results}
|
|
||||||
|
|
||||||
def get_results(self):
|
def get_results(self):
|
||||||
|
"""Returns a normalized list of string of open ports
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
list
|
||||||
|
List of open ports (E.g. ["53 UDP", "53 TCP"])
|
||||||
|
"""
|
||||||
|
|
||||||
results = list(map(lambda x: "%s UDP" % x, self.udp_results))
|
results = list(map(lambda x: "%s UDP" % x, self.udp_results))
|
||||||
results.extend(
|
results.extend(
|
||||||
list(map(lambda x: "%s TCP" % x, self.tcp_results))
|
list(map(lambda x: "%s TCP" % x, self.tcp_results))
|
||||||
@ -192,23 +289,24 @@ class Scanner(Thread):
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
def __scan_tcp(self):
|
def __scan_tcp(self):
|
||||||
for port in range(1, self.port_count):
|
"""Threaded TCP Scanner"""
|
||||||
|
for port in range(1, self.__port_count):
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
s.settimeout(0.1)
|
s.settimeout(0.1)
|
||||||
result = s.connect_ex((self.target, port))
|
result = s.connect_ex((self.target, port))
|
||||||
if result == 0:
|
if result == 0:
|
||||||
self.tcp_results.append(port)
|
self.tcp_results.append(port)
|
||||||
s.close()
|
s.close()
|
||||||
self.tcp_progress = round(port / self.port_count * 100)
|
self.tcp_progress = round(port / self.__port_count * 100)
|
||||||
|
|
||||||
def __scan_udp(self):
|
def __scan_udp(self):
|
||||||
for port in range(1, self.port_count):
|
"""Threaded UDP Scanner"""
|
||||||
# print("UDP port %s..." % port)
|
for port in range(1, self.__port_count):
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
s.settimeout(0.01)
|
s.settimeout(0.01)
|
||||||
payloads = ["\x00"]
|
payloads = ["\x00"]
|
||||||
if port in self.udp_payloads:
|
if port in self.__udp_payloads:
|
||||||
payloads = self.udp_payloads[port]
|
payloads.extend(self.__udp_payloads[port])
|
||||||
for payload in payloads:
|
for payload in payloads:
|
||||||
s.sendto(payload.encode("utf-8"), (self.target, port))
|
s.sendto(payload.encode("utf-8"), (self.target, port))
|
||||||
try:
|
try:
|
||||||
@ -220,16 +318,4 @@ class Scanner(Thread):
|
|||||||
except socket.timeout:
|
except socket.timeout:
|
||||||
pass
|
pass
|
||||||
s.close()
|
s.close()
|
||||||
self.udp_progress = round(port / self.port_count * 100)
|
self.udp_progress = round(port / self.__port_count * 100)
|
||||||
|
|
||||||
|
|
||||||
# FOR TESTING PURPOSES
|
|
||||||
def main():
|
|
||||||
sm = ScanManager()
|
|
||||||
sm.perform_scan("localhost")
|
|
||||||
# sm.perform_scan("10.0.20.254")
|
|
||||||
# sm.perform_scan("10.0.21.20")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
Loading…
Reference in New Issue
Block a user