From 335e6c97559e52c7bf30fa3fd562a42158eb5bb4 Mon Sep 17 00:00:00 2001 From: Marshall Hallenbeck Date: Thu, 12 Oct 2023 15:41:13 -0400 Subject: [PATCH] ruff: manually fixed pydocstyle ruff alerts and add exclusions --- nxc/connection.py | 23 ++++++----- nxc/helpers/bloodhound.py | 3 +- nxc/helpers/misc.py | 10 ++--- nxc/helpers/powershell.py | 3 +- nxc/logger.py | 9 +++-- nxc/modules/handlekatz.py | 2 +- nxc/modules/keepass_discover.py | 2 +- nxc/modules/keepass_trigger.py | 2 +- nxc/modules/masky.py | 2 +- nxc/modules/nanodump.py | 2 +- nxc/modules/pi.py | 2 +- nxc/modules/procdump.py | 2 +- nxc/modules/spider_plus.py | 68 +++++++++++++-------------------- nxc/modules/winscp_dump.py | 2 +- nxc/servers/http.py | 2 +- pyproject.toml | 2 +- 16 files changed, 59 insertions(+), 77 deletions(-) diff --git a/nxc/connection.py b/nxc/connection.py index 662f5c74..5e477c6a 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -169,8 +169,8 @@ class connection(object): self.call_cmd_args() def call_cmd_args(self): - """ - Calls all the methods specified by the command line arguments + """Calls all the methods specified by the command line arguments + Iterates over the attributes of an object (self.args) For each attribute, it checks if the object (self) has an attribute with the same name and if that attribute is callable (i.e., a function) If both conditions are met and the attribute value is not False or None, @@ -191,8 +191,8 @@ class connection(object): getattr(self, attr)() def call_modules(self): - """ - This function calls the modules and performs various actions based on the module's attributes. + """Calls modules and performs various actions based on the module's attributes. + It iterates over the modules specified in the command line arguments. For each module, it loads the module and creates a context object, then calls functions based on the module's attributes. """ @@ -256,8 +256,8 @@ class connection(object): return False def query_db_creds(self): - """ - Queries the database for credentials to be used for authentication. + """Queries the database for credentials to be used for authentication. + Valid cred_id values are: - a single cred_id - a range specified with a dash (ex. 1-5) @@ -296,8 +296,8 @@ class connection(object): return domains, usernames, owned, secrets, cred_types, data def parse_credentials(self): - """ - Parse credentials from the command line or from a file specified. + r"""Parse credentials from the command line or from a file specified. + Usernames can be specified with a domain (domain\\username) or without (username). If the file contains domain\\username the domain specified will be overwritten by the one in the file. @@ -381,8 +381,8 @@ class connection(object): return domain, username, owned, secret, cred_type, [None] * len(secret) def try_credentials(self, domain, username, owned, secret, cred_type, data=None): - """ - Try to login using the specified credentials and protocol. + """Try to login using the specified credentials and protocol. + Possible login methods are: - plaintext (/kerberos) - NTLM-hash (/kerberos) @@ -415,8 +415,7 @@ class connection(object): return self.kerberos_login(domain, username, "", "", secret, self.kdcHost, False) def login(self): - """ - Try to login using the credentials specified in the command line or in the database. + """Try to login using the credentials specified in the command line or in the database. :return: True if the login was successful and "--continue-on-success" was not specified, False otherwise. """ diff --git a/nxc/helpers/bloodhound.py b/nxc/helpers/bloodhound.py index d8d717a5..d1da4e84 100644 --- a/nxc/helpers/bloodhound.py +++ b/nxc/helpers/bloodhound.py @@ -6,8 +6,7 @@ from neo4j.exceptions import AuthError, ServiceUnavailable def add_user_bh(user, domain, logger, config): - """ - Adds a user to the BloodHound graph database. + """Adds a user to the BloodHound graph database. Args: ---- diff --git a/nxc/helpers/misc.py b/nxc/helpers/misc.py index 7d956750..367e6b69 100755 --- a/nxc/helpers/misc.py +++ b/nxc/helpers/misc.py @@ -45,12 +45,10 @@ def called_from_cmd_args(): # Stolen from https://github.com/pydanny/whichcraft/ def which(cmd, mode=os.F_OK | os.X_OK, path=None): - """Given a command, mode, and a PATH string, return the path which - conforms to the given mode on the PATH, or None if there is no such - file. - `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result - of os.environ.get("PATH"), or can be overridden with a custom search - path. + """Find the path which conforms to the given mode on the PATH for a command. + + Given a command, mode, and a PATH string, return the path which conforms to the given mode on the PATH, or None if there is no such file. + `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result of os.environ.get("PATH"), or can be overridden with a custom search path. Note: This function was backported from the Python 3 source code. """ diff --git a/nxc/helpers/powershell.py b/nxc/helpers/powershell.py index aa13eefe..4f79e2d8 100644 --- a/nxc/helpers/powershell.py +++ b/nxc/helpers/powershell.py @@ -15,8 +15,7 @@ obfuscate_ps_scripts = False def get_ps_script(path): - """ - Generates a full path to a PowerShell script given a relative path. + """Generates a full path to a PowerShell script given a relative path. Parameters ---------- diff --git a/nxc/logger.py b/nxc/logger.py index 9db2f56e..3a01d173 100755 --- a/nxc/logger.py +++ b/nxc/logger.py @@ -37,8 +37,8 @@ class NXCAdapter(logging.LoggerAdapter): # logging.getLogger("impacket").disabled = True def format(self, msg, *args, **kwargs): - """ - Format msg for output if needed + """Format msg for output + This is used instead of process() since process() applies to _all_ messages, including debug calls """ if self.extra is None: @@ -125,9 +125,10 @@ class NXCAdapter(logging.LoggerAdapter): self.log_console_to_file(text, *args, **kwargs) def log_console_to_file(self, text, *args, **kwargs): - """ + """Log the console output to a file + If debug or info logging is not enabled, we still want display/success/fail logged to the file specified, - so we create a custom LogRecord and pass it to all the additional handlers (which will be all the file handlers + so we create a custom LogRecord and pass it to all the additional handlers (which will be all the file handlers) """ if self.logger.getEffectiveLevel() >= logging.INFO: # will be 0 if it's just the console output, so only do this if we actually have file loggers diff --git a/nxc/modules/handlekatz.py b/nxc/modules/handlekatz.py index 913ea60a..1a464758 100644 --- a/nxc/modules/handlekatz.py +++ b/nxc/modules/handlekatz.py @@ -21,7 +21,7 @@ class NXCModule: multiple_hosts = True def options(self, context, module_options): - """ + r""" TMP_DIR Path where process dump should be saved on target system (default: C:\\Windows\\Temp\\) HANDLEKATZ_PATH Path where handlekatz.exe is on your system (default: /tmp/) HANDLEKATZ_EXE_NAME Name of the handlekatz executable (default: handlekatz.exe) diff --git a/nxc/modules/keepass_discover.py b/nxc/modules/keepass_discover.py index d34214d4..c067e668 100644 --- a/nxc/modules/keepass_discover.py +++ b/nxc/modules/keepass_discover.py @@ -20,7 +20,7 @@ class NXCModule: self.search_path = "'C:\\Users\\','$env:PROGRAMFILES','env:ProgramFiles(x86)'" def options(self, context, module_options): - """ + r""" SEARCH_TYPE Specify what to search, between: PROCESS Look for running KeePass.exe process only FILES Look for KeePass-related files (KeePass.config.xml, .kdbx, KeePass.exe) only, may take some time diff --git a/nxc/modules/keepass_trigger.py b/nxc/modules/keepass_trigger.py index b307596a..a16adc9e 100644 --- a/nxc/modules/keepass_trigger.py +++ b/nxc/modules/keepass_trigger.py @@ -56,7 +56,7 @@ class NXCModule: self.restart_keepass_script_str = restart_keepass_script_file.read() def options(self, context, module_options): - """ + r""" ACTION (mandatory) Performs one of the following actions, specified by the user: ADD insert a new malicious trigger into KEEPASS_CONFIG_PATH's specified file CHECK check if a malicious trigger is currently set in KEEPASS_CONFIG_PATH's diff --git a/nxc/modules/masky.py b/nxc/modules/masky.py index 790546c4..9e539957 100644 --- a/nxc/modules/masky.py +++ b/nxc/modules/masky.py @@ -13,7 +13,7 @@ class NXCModule: multiple_hosts = True def options(self, context, module_options): - """ + r""" CA Certificate Authority Name (CA_SERVER\CA_NAME) TEMPLATE Template name allowing users to authenticate with (default: User) DC_IP IP Address of the domain controller diff --git a/nxc/modules/nanodump.py b/nxc/modules/nanodump.py index a22f55d8..d8f165cd 100644 --- a/nxc/modules/nanodump.py +++ b/nxc/modules/nanodump.py @@ -34,7 +34,7 @@ class NXCModule: self.module_options = module_options def options(self, context, module_options): - """ + r""" TMP_DIR Path where process dump should be saved on target system (default: C:\\Windows\\Temp\\) NANO_PATH Path where nano.exe is on your system (default: OS temp directory) NANO_EXE_NAME Name of the nano executable (default: nano.exe) diff --git a/nxc/modules/pi.py b/nxc/modules/pi.py index 44e0577f..0aaa303c 100644 --- a/nxc/modules/pi.py +++ b/nxc/modules/pi.py @@ -11,7 +11,7 @@ class NXCModule: multiple_hosts = True def options(self, context, module_options): - """ + r""" PID // Process ID for Target User, PID=pid EXEC // Command to exec, EXEC='command' Single quote is better to use diff --git a/nxc/modules/procdump.py b/nxc/modules/procdump.py index 1a46b78b..6e5b2fd8 100644 --- a/nxc/modules/procdump.py +++ b/nxc/modules/procdump.py @@ -20,7 +20,7 @@ class NXCModule: multiple_hosts = True def options(self, context, module_options): - """ + r""" TMP_DIR Path where process dump should be saved on target system (default: C:\\Windows\\Temp\\) PROCDUMP_PATH Path where procdump.exe is on your system (default: /tmp/), if changed embeded version will not be used PROCDUMP_EXE_NAME Name of the procdump executable (default: procdump.exe), if changed embeded version will not be used diff --git a/nxc/modules/spider_plus.py b/nxc/modules/spider_plus.py index 00d3da6f..e4f73d0b 100755 --- a/nxc/modules/spider_plus.py +++ b/nxc/modules/spider_plus.py @@ -15,10 +15,7 @@ CHUNK_SIZE = 4096 def human_size(nbytes): - """ - This function takes a number of bytes as input and converts it to a human-readable - size representation with appropriate units (e.g., KB, MB, GB, TB). - """ + """Takes a number of bytes as input and converts it to a human-readable size representation with appropriate units (e.g., KB, MB, GB, TB)""" suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] # Find the appropriate unit suffix and convert bytes to higher units @@ -35,17 +32,12 @@ def human_size(nbytes): def human_time(timestamp): - """This function takes a numerical timestamp (seconds since the epoch) and formats it - as a human-readable date and time in the format "YYYY-MM-DD HH:MM:SS". - """ + """Takes a numerical timestamp (seconds since the epoch) and formats it as a human-readable date and time in the format "YYYY-MM-DD HH:MM:SS""" return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp)) def make_dirs(path): - """ - This function attempts to create directories at the given path. It handles the - exception `os.errno.EEXIST` that may occur if the directories already exist. - """ + """Creates directories at the given path. It handles the exception `os.errno.EEXIST` that may occur if the directories already exist.""" try: os.makedirs(path) except OSError as e: @@ -55,8 +47,7 @@ def make_dirs(path): def get_list_from_option(opt): - """ - This function takes a comma-separated string and converts it to a list of lowercase strings. + """Takes a comma-separated string and converts it to a list of lowercase strings. It filters out empty strings from the input before converting. """ return list(map(lambda o: o.lower(), filter(bool, opt.split(",")))) @@ -106,9 +97,8 @@ class SMBSpiderPlus: make_dirs(self.output_folder) def reconnect(self): - """This function performs a series of reconnection attempts, up to `self.max_connection_attempts`, - with a 3-second delay between each attempt. It renegotiates the session by creating a new - connection object and logging in again. + """Performs a series of reconnection attempts, up to `self.max_connection_attempts`, with a 3-second delay between each attempt. + It renegotiates the session by creating a new connection object and logging in again. """ for i in range(1, self.max_connection_attempts + 1): self.logger.display(f"Reconnection attempt #{i}/{self.max_connection_attempts} to server.") @@ -122,7 +112,7 @@ class SMBSpiderPlus: return False def list_path(self, share, subfolder): - """This function returns a list of paths for a given share/folder.""" + """Returns a list of paths for a given share/folder.""" filelist = [] try: # Get file list for the current folder @@ -144,7 +134,7 @@ class SMBSpiderPlus: return filelist def get_remote_file(self, share, path): - """This function will check if a path is readable in a SMB share.""" + """Checks if a path is readable in a SMB share.""" try: remote_file = RemoteFile(self.smb.conn, path, share, access=FILE_READ_DATA) return remote_file @@ -155,10 +145,9 @@ class SMBSpiderPlus: return None def read_chunk(self, remote_file, chunk_size=CHUNK_SIZE): - """This function reads the next chunk of data from the provided remote file using - the specified chunk size. If a `SessionError` is encountered, - it retries up to 3 times by reconnecting the SMB connection. If the maximum number - of retries is exhausted or an unexpected exception occurs, it returns an empty chunk. + """Reads the next chunk of data from the provided remote file using the specified chunk size. + If a `SessionError` is encountered, it retries up to 3 times by reconnecting the SMB connection. + If the maximum number of retries is exhausted or an unexpected exception occurs, it returns an empty chunk. """ chunk = "" retry = 3 @@ -182,9 +171,9 @@ class SMBSpiderPlus: return chunk def get_file_save_path(self, remote_file): - """This function processes the remote file path to extract the filename and the folder - path where the file should be saved locally. It converts forward slashes (/) and backslashes (\) - in the remote file path to the appropriate path separator for the local file system. + r"""Processes the remote file path to extract the filename and the folder path where the file should be saved locally. + + It converts forward slashes (/) and backslashes (\) in the remote file path to the appropriate path separator for the local file system. The folder path and filename are then obtained separately. """ # Remove the backslash before the remote host part and replace slashes with the appropriate path separator @@ -199,9 +188,7 @@ class SMBSpiderPlus: return folder, filename def spider_shares(self): - """This function enumerates all available shares for the SMB connection, spiders - through the readable shares, and saves the metadata of the shares to a JSON file. - """ + """Enumerates all available shares for the SMB connection, spiders through the readable shares, and saves the metadata of the shares to a JSON file""" self.logger.info("Enumerating shares for spidering.") shares = self.smb.shares() @@ -251,9 +238,9 @@ class SMBSpiderPlus: return self.results def spider_folder(self, share_name, folder): - """This recursive function traverses through the contents of the specified share and folder. - It checks each entry (file or folder) against various filters, performs file metadata recording, - and downloads eligible files if the download flag is set. + """Traverses through the contents of the specified share and folder. + + It checks each entry (file or folder) against various filters, performs file metadata recording, and downloads eligible files if the download flag is set. """ self.logger.info(f'Spider share "{share_name}" in folder "{folder}".') @@ -284,9 +271,7 @@ class SMBSpiderPlus: self.parse_file(share_name, next_fullpath, result) def parse_file(self, share_name, file_path, file_info): - """This function checks file attributes against various filters, records file metadata, - and downloads eligible files if the download flag is set. - """ + """Checks file attributes against various filters, records file metadata, and downloads eligible files if the download flag is set""" # Record the file metadata file_size = file_info.get_filesize() file_creation_time = file_info.get_ctime_epoch() @@ -361,7 +346,8 @@ class SMBSpiderPlus: self.stats["num_get_fail"] += 1 def save_file(self, remote_file, share_name): - """This function reads the `remote_file` in chunks using the `read_chunk` method. + """Reads the `remote_file` in chunks using the `read_chunk` method. + Each chunk is then written to the local file until the entire file is saved. It handles cases where the file remains empty due to errors. """ @@ -392,9 +378,9 @@ class SMBSpiderPlus: self.logger.fail(f'Unable to download file "{remote_path}".') def dump_folder_metadata(self, results): - """This function takes the metadata results as input and writes them to a JSON file - in the `self.output_folder`. The results are formatted with indentation and - sorted keys before being written to the file. + """Takes the metadata results as input and writes them to a JSON file in the `self.output_folder`. + + The results are formatted with indentation and sorted keys before being written to the file. """ metadata_path = os.path.join(self.output_folder, f"{self.host}.json") try: @@ -405,7 +391,7 @@ class SMBSpiderPlus: self.logger.fail(f"Failed to save share metadata: {str(e)}") def print_stats(self): - """This function prints the statistics during processing.""" + """Prints the statistics during processing""" # Share statistics. shares = self.stats.get("shares", []) if shares: @@ -493,8 +479,8 @@ class SMBSpiderPlus: class NXCModule: - """ - Spider plus module + """Spider Plus Nodule + Module by @vincd Updated by @godylockz """ diff --git a/nxc/modules/winscp_dump.py b/nxc/modules/winscp_dump.py index 0cef8cd3..9780d2d8 100644 --- a/nxc/modules/winscp_dump.py +++ b/nxc/modules/winscp_dump.py @@ -27,7 +27,7 @@ class NXCModule: multiple_hosts = True def options(self, context, module_options): - """ + r""" PATH Specify the Path if you already found a WinSCP.ini file. (Example: PATH="C:\\Users\\USERNAME\\Documents\\WinSCP_Passwords\\WinSCP.ini") REQUIRES ADMIN PRIVILEGES: diff --git a/nxc/servers/http.py b/nxc/servers/http.py index cff87031..ab37de5e 100755 --- a/nxc/servers/http.py +++ b/nxc/servers/http.py @@ -45,7 +45,7 @@ class RequestHandler(BaseHTTPRequestHandler): self.server.module.on_response(self.server.context, self) def stop_tracking_host(self): - """This gets called when a module has finshed executing, removes the host from the connection tracker list""" + """Called when a module has finshed executing, removes the host from the connection tracker list""" try: self.server.hosts.remove(self.client_address[0]) if hasattr(self.server.module, "on_shutdown"): diff --git a/pyproject.toml b/pyproject.toml index 09a1166d..266e09a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,7 +81,7 @@ build-backend = "poetry.core.masonry.api" # McCabe complexity (`C901`) by default. # Other options: N (pep8-naming), D (pydocstyle) select = ["E", "F", "D"] -ignore = [ "E501", "F405", "F841", "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", "D203", "D204", "D212", "D213", "D400", "D415", "D419"] +ignore = [ "E501", "F405", "F841", "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", "D203", "D204", "D205", "D212", "D213", "D400", "D401", "D415", "D417", "D419"] # Allow autofix for all enabled rules (when `--fix`) is provided. fixable = ["ALL"]