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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user