diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2097e04 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,91 @@ +# ============================================================================ +# .gitattributes +# ============================================================================ +# +# RULE 1 --LINE ENDINGS +# +# Every text file in this repository uses LF (\n) line endings. +# The single exception is docs/make.bat which requires CRLF for Windows. +# +# This file is the PRIMARY enforcement mechanism for line endings. +# The pre-commit hook and CI script provide defense-in-depth CR scanning. +# +# After adding or modifying this file, renormalize the repo: +# +# git add --renormalize . +# git status +# git commit -m "Normalize line endings" +# +# ============================================================================ + +# --------------------------------------------------------------------------- +# Default: every file is text, normalized to LF on commit AND checkout. +# +# We use "text eol=lf" (not "text=auto eol=lf") because this repo has a +# strict ASCII-only policy for source files. There is no ambiguity for Git +# to resolve --everything is either text or explicitly marked binary below. +# Unconditional normalization means stray CR bytes are always stripped. +# --------------------------------------------------------------------------- +* text eol=lf + +# --------------------------------------------------------------------------- +# Binary assets --disable text processing, diffing, and merge. +# The "binary" macro is shorthand for: -text -diff -merge +# +# Keep in sync with BINARY_EXTENSIONS in .githooks/pre-commit and +# .githooks/check-hygiene.sh. If new binary types are added, declare +# them here to prevent Git from mangling their content. +# --------------------------------------------------------------------------- +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.bmp binary +*.tiff binary +*.webp binary +*.pdf binary +*.woff binary +*.woff2 binary +*.ttf binary +*.eot binary +*.otf binary + +# --------------------------------------------------------------------------- +# Exception: docs/make.bat needs CRLF to run correctly on Windows. +# This overrides the global "* text eol=lf" for this one file. +# --------------------------------------------------------------------------- +docs/make.bat text eol=crlf + +# --------------------------------------------------------------------------- +# Explicit text declarations --defense-in-depth. +# +# The global "* text eol=lf" already covers these, but explicit per-extension +# rules protect against someone adding a more specific override later that +# might accidentally exclude core file types. +# --------------------------------------------------------------------------- +*.py text eol=lf +*.toml text eol=lf +*.txt text eol=lf +*.md text eol=lf +*.rst text eol=lf +*.html text eol=lf +*.xml text eol=lf +*.css text eol=lf +*.dat text eol=lf +*.cfg text eol=lf +*.ini text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.json text eol=lf +*.sh text eol=lf + +# --------------------------------------------------------------------------- +# Diff drivers --better hunk headers for common file types. +# These don't affect content, only how diffs are displayed. +# --------------------------------------------------------------------------- +*.py diff=python +*.html diff=html +*.css diff=css +*.md diff=markdown +*.rst diff=rst diff --git a/.githooks/check-hygiene.sh b/.githooks/check-hygiene.sh new file mode 100755 index 0000000..711c6d5 --- /dev/null +++ b/.githooks/check-hygiene.sh @@ -0,0 +1,224 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2310 # functions in if/! conditions are intentional +# ============================================================================ +# .githooks/check-hygiene.sh +# ============================================================================ +# +# Server-side enforcement of repository hygiene. Run in CI on every push +# and pull request so violations CANNOT be merged, even if a contributor +# bypasses the pre-commit hook with --no-verify. +# +# Checks all three invariants: +# RULE 1 -- LINE ENDINGS: No CR (\r) bytes in any text file. +# RULE 2 -- PERMISSIONS: Every file is mode 100644 (no executable bit). +# RULE 3 -- ENCODING: ASCII only (bytes 10, 32-126) outside exempt paths. +# +# Usage: +# bash .githooks/check-hygiene.sh # check ALL tracked files +# bash .githooks/check-hygiene.sh --diff # check only files changed vs base +# # (reads GITHUB_BASE_REF; falls back +# # to origin/main) +# +# GitHub Actions integration: +# Violations emit ::error annotations so they appear inline on the PR diff. +# +# Exit codes: +# 0 = all checks pass +# 1 = one or more violations +# +# ============================================================================ + +set -euo pipefail + +# --------------------------------------------------------------------------- +# CONFIGURATION -- keep in sync with .githooks/pre-commit +# --------------------------------------------------------------------------- + +# Files allowed to have the executable bit (mode 100755). +EXEC_ALLOWLIST=( + ".githooks/check-hygiene.sh" + ".githooks/pre-commit" +) + +# Paths exempt from the ASCII-only encoding check (Rule 3). +ASCII_EXEMPT_PATTERNS=( + "^docs/" + "^README\.md$" +) + +# Files allowed to contain CR (\r) bytes (CRLF line endings). +# Keep in sync with the eol=crlf declarations in .gitattributes. +CRLF_ALLOWLIST=( + "docs/make.bat" +) + +# Extensions treated as binary (skipped for encoding and line-ending checks). +# Keep in sync with the "binary" declarations in .gitattributes. +BINARY_EXTENSIONS="png|jpg|jpeg|gif|ico|bmp|tiff|webp|pdf|woff|woff2|ttf|eot|otf" + +# --------------------------------------------------------------------------- +# Determine which files to check +# --------------------------------------------------------------------------- + +if [[ "${1:-}" == "--diff" ]]; then + base="${GITHUB_BASE_REF:-main}" + + # Make sure the base ref is available (shallow clones in CI may not have it). + git fetch origin "${base}" --depth=1 2> /dev/null || true + + mapfile -t files < <( + git diff --name-only --diff-filter=ACMR "origin/${base}...HEAD" 2> /dev/null || true + ) + + echo "Mode: diff against origin/${base}" +else + mapfile -t files < <(git ls-files || true) + + echo "Mode: all tracked files" +fi + +echo "Files to check: ${#files[@]}" +echo "" + +if [[ ${#files[@]} -eq 0 ]]; then + echo "Nothing to check." + exit 0 +fi + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +fail=0 +perm_count=0 +encoding_count=0 +cr_count=0 + +is_exec_allowed() { + local file="${1}" + for allowed in "${EXEC_ALLOWLIST[@]+"${EXEC_ALLOWLIST[@]}"}"; do + [[ "${file}" == "${allowed}" ]] && return 0 + done + return 1 +} + +is_ascii_exempt() { + local file="${1}" + for pattern in "${ASCII_EXEMPT_PATTERNS[@]}"; do + if [[ "${file}" =~ ${pattern} ]]; then + return 0 + fi + done + return 1 +} + +is_binary() { + local file="${1}" + [[ "${file}" =~ \.(${BINARY_EXTENSIONS})$ ]] +} + +is_crlf_allowed() { + local file="${1}" + for allowed in "${CRLF_ALLOWLIST[@]+"${CRLF_ALLOWLIST[@]}"}"; do + [[ "${file}" == "${allowed}" ]] && return 0 + done + return 1 +} + +# Emit a GitHub Actions annotation if running in CI, otherwise plain text. +# Usage: annotate "error" "file.py" "Message" +annotate() { + local level="${1}" file="${2}" msg="${3}" + if [[ "${GITHUB_ACTIONS:-}" == "true" ]]; then + echo "::${level} file=${file}::${msg}" + else + echo "[${level^^}] ${file}: ${msg}" + fi +} + +# --------------------------------------------------------------------------- +# RULE 2 -- Permissions +# --------------------------------------------------------------------------- + +for file in "${files[@]}"; do + mode=$(git ls-files -s -- "${file}" 2> /dev/null | awk '{print $1}') + + if [[ "${mode}" == "100755" ]]; then + if ! is_exec_allowed "${file}"; then + annotate "error" "${file}" \ + "File has executable bit (mode 100755). Expected 100644. Fix: git update-index --chmod=-x \"${file}\"" + ((perm_count++)) + fail=1 + fi + fi +done + +# --------------------------------------------------------------------------- +# RULE 3 -- Encoding +# --------------------------------------------------------------------------- + +for file in "${files[@]}"; do + if is_ascii_exempt "${file}"; then continue; fi + if is_binary "${file}"; then continue; fi + + # Read from HEAD for full-repo mode, from the file for diff mode + # (the file on disk in CI is the checked-out version of the PR head). + bad_count=$(git show "HEAD:${file}" 2> /dev/null \ + | LC_ALL=C grep -Pc '[^\x0a\x20-\x7e]' 2> /dev/null || true) + + if [[ -n "${bad_count}" ]] && [[ "${bad_count}" -gt 0 ]]; then + # Grab the first offending line for diagnostic context. + first_bad=$(git show "HEAD:${file}" 2> /dev/null \ + | LC_ALL=C grep -Pn '[^\x0a\x20-\x7e]' 2> /dev/null \ + | head -1) + annotate "error" "${file}" \ + "File contains ${bad_count} line(s) with non-ASCII bytes. First: ${first_bad}" + ((encoding_count++)) + fail=1 + fi +done + +# --------------------------------------------------------------------------- +# RULE 1 -- Line endings (CR bytes) +# --------------------------------------------------------------------------- + +for file in "${files[@]}"; do + if is_binary "${file}"; then continue; fi + if is_crlf_allowed "${file}"; then continue; fi + + if git show "HEAD:${file}" 2> /dev/null | LC_ALL=C grep -Pq '\r' 2> /dev/null; then + annotate "error" "${file}" \ + "File contains CR (\\r) bytes. Line endings must be LF only." + ((cr_count++)) + fail=1 + fi +done + +# --------------------------------------------------------------------------- +# Report +# --------------------------------------------------------------------------- + +echo "" +echo "======================================" +echo " Repository hygiene report" +echo "======================================" +echo " Files checked: ${#files[@]}" +echo " Permission violations: ${perm_count}" +echo " Encoding violations: ${encoding_count}" +echo " Line-ending violations: ${cr_count}" +echo "======================================" + +if [[ "${fail}" -ne 0 ]]; then + echo "" + echo "FAILED -- fix the errors above." + echo "" + echo "Quick reference:" + [[ ${perm_count} -gt 0 ]] && echo " Permissions: git update-index --chmod=-x " + [[ ${encoding_count} -gt 0 ]] && echo " Encoding: replace non-ASCII bytes with ASCII equivalents" + [[ ${cr_count} -gt 0 ]] && printf ' Line endings: sed -i '\''s/\\r//g'\'' \n' + exit 1 +fi + +echo "" +echo "All checks passed." +exit 0 diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..3829e41 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,220 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2310 # functions in if/! conditions are intentional +# ============================================================================ +# .githooks/pre-commit +# ============================================================================ +# +# Local enforcement of three repository invariants on every commit. +# This hook reads STAGED content from the Git index, not the working tree, +# so it validates exactly what is about to be committed. +# +# RULE 1 -- LINE ENDINGS: No CR (\r) bytes in any text file. +# (.gitattributes is the primary mechanism; this is defense-in-depth.) +# +# RULE 2 -- PERMISSIONS: Every file is mode 100644. +# No file has the executable bit unless explicitly allowlisted. +# +# RULE 3 -- ENCODING: ASCII only (bytes 10, 32-126) in all text files +# outside the exempt paths listed below. +# Tabs (byte 9), NUL (byte 0), DEL (byte 127), and all bytes > 127 +# are rejected. +# +# RULE 4 -- PYTHON: All .py files in the repo must pass ruff format +# and ruff check (via uv run). Format is applied automatically +# and re-staged; lint violations block the commit. +# +# Install (run once per clone): +# +# git config core.hooksPath .githooks +# +# ============================================================================ + +set -euo pipefail + +# --------------------------------------------------------------------------- +# CONFIGURATION -- keep in sync with .githooks/check-hygiene.sh +# --------------------------------------------------------------------------- + +# Files allowed to have the executable bit (mode 100755). +# This list is intentionally empty. If you add a shell script that +# genuinely needs +x, add its repo-relative path here AND in the CI script. +EXEC_ALLOWLIST=( + ".githooks/check-hygiene.sh" + ".githooks/pre-commit" +) + +# Paths exempt from the ASCII-only encoding check (Rule 3). +# These may contain legitimate UTF-8 for Sphinx, badges, etc. +# Patterns are matched with bash =~ against repo-relative paths. +ASCII_EXEMPT_PATTERNS=( + "^docs/" + "^README\.md$" +) + +# Files allowed to contain CR (\r) bytes (CRLF line endings). +# Keep in sync with the eol=crlf declarations in .gitattributes. +CRLF_ALLOWLIST=( + "docs/make.bat" +) + +# Extensions treated as binary (skipped for encoding and line-ending checks). +# Keep in sync with the "binary" declarations in .gitattributes. +BINARY_EXTENSIONS="png|jpg|jpeg|gif|ico|bmp|tiff|webp|pdf|woff|woff2|ttf|eot|otf" + +# --------------------------------------------------------------------------- +# Internals +# --------------------------------------------------------------------------- + +RED='\033[0;31m' +YELLOW='\033[1;33m' +DIM='\033[2m' +NC='\033[0m' + +fail=0 +perm_count=0 +encoding_count=0 +cr_count=0 + +is_exec_allowed() { + local file="${1}" + for allowed in "${EXEC_ALLOWLIST[@]+"${EXEC_ALLOWLIST[@]}"}"; do + [[ "${file}" == "${allowed}" ]] && return 0 + done + return 1 +} + +is_ascii_exempt() { + local file="${1}" + for pattern in "${ASCII_EXEMPT_PATTERNS[@]}"; do + if [[ "${file}" =~ ${pattern} ]]; then + return 0 + fi + done + return 1 +} + +is_binary() { + local file="${1}" + [[ "${file}" =~ \.(${BINARY_EXTENSIONS})$ ]] +} + +is_crlf_allowed() { + local file="${1}" + for allowed in "${CRLF_ALLOWLIST[@]+"${CRLF_ALLOWLIST[@]}"}"; do + [[ "${file}" == "${allowed}" ]] && return 0 + done + return 1 +} + +# --------------------------------------------------------------------------- +# Collect staged files (Added, Copied, Modified, Renamed). +# NUL-delimited to handle filenames with spaces safely. +# --------------------------------------------------------------------------- + +staged_files=() +while IFS= read -r -d '' f; do + staged_files+=("${f}") +done < <(git diff --cached --diff-filter=ACMR --name-only -z 2> /dev/null || true) + +if [[ ${#staged_files[@]} -eq 0 ]]; then + exit 0 +fi + +# --------------------------------------------------------------------------- +# RULE 2 -- Permissions: no file should be mode 100755 +# --------------------------------------------------------------------------- + +for file in "${staged_files[@]}"; do + mode=$(git ls-files -s -- "${file}" 2> /dev/null | awk '{print $1}') + + if [[ "${mode}" == "100755" ]]; then + if ! is_exec_allowed "${file}"; then + echo -e "${RED}PERM${NC} ${file} ${DIM}(mode 100755, expected 100644)${NC}" + echo -e " ${DIM}fix: git update-index --chmod=-x \"${file}\"${NC}" + ((perm_count++)) + fail=1 + fi + fi +done + +# --------------------------------------------------------------------------- +# RULE 3 -- Encoding: ASCII only (bytes 10, 32-126) +# +# The grep pattern [^\x0a\x20-\x7e] matches any byte NOT in the allowed set: +# \x0a = LF (byte 10) +# \x20-\x7e = printable ASCII (bytes 32-126) +# +# This rejects: NUL (0), tabs (9), CR (13), DEL (127), and all > 127. +# --------------------------------------------------------------------------- + +for file in "${staged_files[@]}"; do + if is_ascii_exempt "${file}"; then continue; fi + if is_binary "${file}"; then continue; fi + + bad_lines=$(git show ":${file}" 2> /dev/null \ + | LC_ALL=C grep -Pn '[^\x0a\x20-\x7e]' 2> /dev/null || true) + + if [[ -n "${bad_lines}" ]]; then + count=$(echo "${bad_lines}" | wc -l) + echo -e "${RED}ASCII${NC} ${file} ${DIM}(${count} line(s) with non-ASCII bytes)${NC}" + # Show up to 3 offending lines with the bad bytes highlighted. + echo "${bad_lines}" | head -3 | sed 's/^/ /' + if ((count > 3)); then + echo -e " ${DIM}... and $((count - 3)) more${NC}" + fi + ((encoding_count++)) + fail=1 + fi +done + +# --------------------------------------------------------------------------- +# RULE 1 -- Line endings: no CR (\r) bytes in any text file. +# Defense-in-depth behind .gitattributes. +# --------------------------------------------------------------------------- + +for file in "${staged_files[@]}"; do + if is_binary "${file}"; then continue; fi + if is_crlf_allowed "${file}"; then continue; fi + + if git show ":${file}" 2> /dev/null | LC_ALL=C grep -Pq '\r' 2> /dev/null; then + echo -e "${RED}CRLF${NC} ${file} ${DIM}(contains \\r bytes)${NC}" + echo -e " ${DIM}fix: sed -i 's/\\r//g' \"${file}\" && git add \"${file}\"${NC}" + ((cr_count++)) + fail=1 + fi +done + +# --------------------------------------------------------------------------- +# RULE 4 -- Python: ruff format + ruff check (repo-wide, all .py files) +# --------------------------------------------------------------------------- + +# Auto-format the entire repo and re-stage any files that changed. +uv run --group dev ruff format --quiet . +mapfile -t reformatted < <(git diff --name-only -- '*.py' || true) +if [[ ${#reformatted[@]} -gt 0 ]]; then + git add "${reformatted[@]}" +fi + +# Lint check (read-only, repo-wide). Violations block the commit. +if ! uv run --group dev ruff check --quiet .; then + echo -e "${RED}RUFF${NC} ruff check found lint violations" + echo -e " ${DIM}fix: uv run --group dev ruff check --fix .${NC}" + fail=1 +fi + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- + +if [[ "${fail}" -ne 0 ]]; then + echo "" + echo -e "${YELLOW}Commit blocked.${NC}" + [[ ${perm_count} -gt 0 ]] && echo " Permissions: ${perm_count} file(s)" + [[ ${encoding_count} -gt 0 ]] && echo " Encoding: ${encoding_count} file(s)" + [[ ${cr_count} -gt 0 ]] && echo " Line endings: ${cr_count} file(s)" + echo "" + echo -e "${DIM}Bypass (not recommended): git commit --no-verify${NC}" + exit 1 +fi + +exit 0 diff --git a/.github/workflows/hygiene.yml b/.github/workflows/hygiene.yml new file mode 100644 index 0000000..ef751af --- /dev/null +++ b/.github/workflows/hygiene.yml @@ -0,0 +1,30 @@ +# ============================================================================ +# .github/workflows/hygiene.yml +# ============================================================================ +# +# Runs .githooks/check-hygiene.sh on every push and pull request. +# Blocks merge if any file violates the three invariants: +# 1. Line endings (LF only, no CR bytes) +# 2. Permissions (mode 100644, no executable bit) +# 3. Encoding (ASCII only outside docs/ and README.md) +# +# ============================================================================ + +name: Repository Hygiene + +on: + push: + pull_request: + +jobs: + hygiene: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + # fetch-depth 0 is needed for --diff mode to compare against base + fetch-depth: 0 + + - name: Run hygiene checks + run: bash .githooks/check-hygiene.sh --diff diff --git a/dementor/__init__.py b/dementor/__init__.py old mode 100755 new mode 100644 diff --git a/dementor/assets/Dementor.toml b/dementor/assets/Dementor.toml old mode 100755 new mode 100644 index 190a309..e67c587 --- a/dementor/assets/Dementor.toml +++ b/dementor/assets/Dementor.toml @@ -16,8 +16,8 @@ # # The three NTLM settings (Challenge, DisableExtendedSessionSecurity, # DisableNTLMv2) additionally fall back through two more levels: -# 3. [NTLM] section — shared default for all NTLM-enabled protocols -# 4. [Globals] section — last resort +# 3. [NTLM] section -- shared default for all NTLM-enabled protocols +# 4. [Globals] section -- last resort # # All other settings stop at step 2. # @@ -294,7 +294,7 @@ SMB2Support = true ErrorCode = "STATUS_SMB_BAD_UID" # NTLM settings: Challenge, DisableExtendedSessionSecurity, DisableNTLMv2 -# Not set here → falls back to [NTLM]. Set here to override [NTLM] for all +# Not set here -> falls back to [NTLM]. Set here to override [NTLM] for all # SMB servers, or inside [[SMB.Server]] to override for a single server only. # Challenge = "1337LEET" @@ -308,7 +308,7 @@ Port = 139 [[SMB.Server]] Port = 445 -# Per-server overrides (highest priority — override [SMB] and [NTLM] for this port only): +# Per-server overrides (highest priority -- override [SMB] and [NTLM] for this port only): # FQDN = "other.corp.com" # ServerOS = "Windows Server 2022" # ErrorCode = "STATUS_ACCESS_DENIED" @@ -350,7 +350,7 @@ Downgrade = true RequireSTARTTLS = false # NTLM settings: Challenge, DisableExtendedSessionSecurity, DisableNTLMv2 -# Not set here → falls back to [NTLM]. Set here to override [NTLM] for all +# Not set here -> falls back to [NTLM]. Set here to override [NTLM] for all # SMTP servers, or inside [[SMTP.Server]] to override for a single server only. # Challenge = "1337LEET" @@ -379,11 +379,11 @@ Port = 25 # SMB, HTTP, SMTP, LDAP, RPC, MSSQL, POP3, and IMAP. # # Resolution order for these three settings (highest priority first): -# 1. [[Protocol.Server]] entry — per-server override (list-based protocols only: +# 1. [[Protocol.Server]] entry -- per-server override (list-based protocols only: # SMB, HTTP, SMTP, LDAP, POP3, IMAP) -# 2. [Protocol] section — per-protocol override (e.g. [SMB], [HTTP]) -# 3. [NTLM] section — this section; the shared default -# 4. [Globals] section — broadest fallback +# 2. [Protocol] section -- per-protocol override (e.g. [SMB], [HTTP]) +# 3. [NTLM] section -- this section; the shared default +# 4. [Globals] section -- broadest fallback # # If a protocol section does not define Challenge, DisableExtendedSessionSecurity, # or DisableNTLMv2, it inherits the values set here. This lets you configure one @@ -399,8 +399,8 @@ Port = 25 # Accepted formats: # "hex:1122334455667788" explicit hex (preferred, unambiguous) # "ascii:1337LEET" explicit ASCII (preferred) -# "1122334455667788" 16 hex characters — auto-detected as hex -# "1337LEET" 8 ASCII characters — auto-detected as ASCII +# "1122334455667788" 16 hex characters -- auto-detected as hex +# "1337LEET" 8 ASCII characters -- auto-detected as ASCII # omitted / not set cryptographically random 8 bytes per run # # A fixed Challenge combined with DisableExtendedSessionSecurity = true makes @@ -423,9 +423,9 @@ DisableExtendedSessionSecurity = false # When true, TargetInfoFields are omitted from the CHALLENGE_MESSAGE. # Without TargetInfoFields clients cannot construct the NTLMv2 Blob -# (MS-NLMP §3.3.2), which has the following effect by client security level: -# Level 0–2 (older Windows, manually downgraded): fall back to NTLMv1. -# Level 3+ (all modern Windows defaults): refuse to authenticate — zero +# (MS-NLMP S3.3.2), which has the following effect by client security level: +# Level 0-2 (older Windows, manually downgraded): fall back to NTLMv1. +# Level 3+ (all modern Windows defaults): refuse to authenticate -- zero # hashes captured from these clients. # # Leave false unless specifically targeting legacy NTLMv1-only environments. @@ -497,7 +497,7 @@ TLS = false # ErrorCode = "unwillingToPerform" # NTLM settings: Challenge, DisableExtendedSessionSecurity, DisableNTLMv2 -# Not set here → falls back to [NTLM]. Set here to override [NTLM] for all +# Not set here -> falls back to [NTLM]. Set here to override [NTLM] for all # LDAP servers, or inside [[LDAP.Server]] to override for a single server only. # Challenge = "1337LEET" @@ -599,7 +599,7 @@ AuthSchemes = ["Basic", "Negotiate", "NTLM", "Bearer"] WebDAV = true # NTLM settings: Challenge, DisableExtendedSessionSecurity, DisableNTLMv2 -# Not set here → falls back to [NTLM]. Set here to override [NTLM] for all +# Not set here -> falls back to [NTLM]. Set here to override [NTLM] for all # HTTP servers, or inside [[HTTP.Server]] to override for a single server only. # Challenge = "1337LEET" @@ -651,7 +651,7 @@ Port = 80 [RPC] # NTLM settings: Challenge, DisableExtendedSessionSecurity, DisableNTLMv2 -# Not set here → falls back to [NTLM]. RPC uses a single server instance so +# Not set here -> falls back to [NTLM]. RPC uses a single server instance so # there is no per-server item level; only [RPC] or [NTLM] apply. # Challenge = "1337LEET" @@ -699,7 +699,7 @@ TargetPort = 49000 [MSSQL] # NTLM settings: Challenge, DisableExtendedSessionSecurity, DisableNTLMv2 -# Not set here → falls back to [NTLM]. MSSQL uses a single server instance so +# Not set here -> falls back to [NTLM]. MSSQL uses a single server instance so # there is no per-server item level; only [MSSQL] or [NTLM] apply. # Challenge = "1337LEET" @@ -762,7 +762,7 @@ InstanceName = "MSSQLServer" [POP3] # NTLM settings: Challenge, DisableExtendedSessionSecurity, DisableNTLMv2 -# Not set here → falls back to [NTLM]. Set here to override [NTLM] for all +# Not set here -> falls back to [NTLM]. Set here to override [NTLM] for all # POP3 servers, or inside [[POP3.Server]] to override for a single server only. # Challenge = "1337LEET" @@ -807,7 +807,7 @@ Port = 110 [IMAP] # NTLM settings: Challenge, DisableExtendedSessionSecurity, DisableNTLMv2 -# Not set here → falls back to [NTLM]. Set here to override [NTLM] for all +# Not set here -> falls back to [NTLM]. Set here to override [NTLM] for all # IMAP servers, or inside [[IMAP.Server]] to override for a single server only. # Challenge = "1337LEET" diff --git a/dementor/config/toml.py b/dementor/config/toml.py index bb34703..bfe6c75 100644 --- a/dementor/config/toml.py +++ b/dementor/config/toml.py @@ -27,7 +27,7 @@ _T = TypeVar("_T", bound="TomlConfig") # --------------------------------------------------------------------------- # -# Helper sentinel used to differentiate “no default supplied” from “None”. +# Helper sentinel used to differentiate "no default supplied" from "None". # --------------------------------------------------------------------------- # _LOCAL = object() @@ -47,7 +47,7 @@ class Attribute(NamedTuple): section. :type qname: str :param default_val: Default value to fall back to when the key is missing. - ``_LOCAL`` (a private sentinel) means “no default - the key is required”. + ``_LOCAL`` (a private sentinel) means "no default - the key is required". :type default_val: Any | None, optional :param section_local: If ``True`` the key is looked for only in the section defined by the concrete subclass (``self._section_``). If ``False`` the @@ -231,7 +231,7 @@ def _set_field( # ----------------------------------------------------------------- # value = config.get(qname, default_val) if value is _LOCAL: - # ``_LOCAL`` means “required but not supplied”. + # ``_LOCAL`` means "required but not supplied". raise ValueError( f"Expected '{qname}' in config or section({section}) for " + f"{self.__class__.__name__}!" diff --git a/dementor/config/util.py b/dementor/config/util.py index 919d975..b6a5b8d 100644 --- a/dementor/config/util.py +++ b/dementor/config/util.py @@ -88,11 +88,11 @@ class BytesValue: Supports the following input formats (str case): - - ``"hex:1122334455667788"`` — explicit hex prefix - - ``"ascii:1337LEET"`` — explicit ASCII prefix - - ``"1122334455667788"`` — auto-detect hex (when length matches ``2 * self.length``) - - ``"1337LEET"`` — auto-detect (try hex first, then encode) - - ``None`` — generate ``self.length`` cryptographically random bytes + - ``"hex:1122334455667788"`` -- explicit hex prefix + - ``"ascii:1337LEET"`` -- explicit ASCII prefix + - ``"1122334455667788"`` -- auto-detect hex (when length matches ``2 * self.length``) + - ``"1337LEET"`` -- auto-detect (try hex first, then encode) + - ``None`` -- generate ``self.length`` cryptographically random bytes When ``length`` is set, the result is validated to be exactly that many bytes. """ @@ -154,7 +154,7 @@ def _parse_str(self, value: str) -> bytes: if len(candidate) == self.length: return candidate except ValueError: - pass # not valid hex — fall through + pass # not valid hex -- fall through # Fallback: when length is known, the auto-detect hex path above # already handled the 2*length case; encode directly so that strings diff --git a/dementor/db/connector.py b/dementor/db/connector.py index acc1ed2..cbe8d47 100644 --- a/dementor/db/connector.py +++ b/dementor/db/connector.py @@ -165,11 +165,11 @@ def init_engine(session: SessionConfig) -> Engine | None: return create_engine(raw_path, **common) # MySQL / MariaDB / PostgreSQL: QueuePool. - # pool_pre_ping – detect dead connections before checkout. - # pool_use_lifo – reuse most-recent connection so idle ones expire + # pool_pre_ping - detect dead connections before checkout. + # pool_use_lifo - reuse most-recent connection so idle ones expire # naturally via server-side wait_timeout. - # pool_recycle – hard ceiling: close connections older than 1 hour. - # pool_timeout=5 – fail fast on exhaustion (PoolTimeoutError caught + # pool_recycle - hard ceiling: close connections older than 1 hour. + # pool_timeout=5 - fail fast on exhaustion (PoolTimeoutError caught # in model.py); hash file is the primary capture path. return create_engine( raw_path, diff --git a/dementor/filters.py b/dementor/filters.py old mode 100755 new mode 100644 diff --git a/dementor/loader.py b/dementor/loader.py old mode 100755 new mode 100644 index 8133ddb..8832d1c --- a/dementor/loader.py +++ b/dementor/loader.py @@ -306,7 +306,7 @@ def resolve_protocols( for path in protocol_paths: if not os.path.exists(path): - # Missing entries are ignored – they may be optional. + # Missing entries are ignored - they may be optional. continue if os.path.isfile(path): diff --git a/dementor/paths.py b/dementor/paths.py old mode 100755 new mode 100644 diff --git a/dementor/protocols/__init__.py b/dementor/protocols/__init__.py old mode 100755 new mode 100644 diff --git a/dementor/protocols/ftp.py b/dementor/protocols/ftp.py old mode 100755 new mode 100644 index 5880554..1085681 --- a/dementor/protocols/ftp.py +++ b/dementor/protocols/ftp.py @@ -110,7 +110,7 @@ class FTPHandler(BaseProtoHandler): Minimal FTP request handler. The handler sends the initial ``220`` greeting, then processes a very - small login sequence (``USER`` → ``PASS``). All other commands result + small login sequence (``USER`` -> ``PASS``). All other commands result in a ``501`` reply. :class:`ProtocolLogger` is used to attach FTP-specific metadata to log @@ -166,7 +166,7 @@ def handle_data(self, data: bytes | None, transport: socket) -> None: parts[1].decode(errors="replace").strip() if len(parts) > 1 else "" ) if not username: - self.reply(501) # Empty username → syntax error + self.reply(501) # Empty username -> syntax error continue self.reply(331) # Password required diff --git a/dementor/protocols/kerberos.py b/dementor/protocols/kerberos.py old mode 100755 new mode 100644 diff --git a/dementor/protocols/ldap.py b/dementor/protocols/ldap.py old mode 100755 new mode 100644 diff --git a/dementor/protocols/llmnr.py b/dementor/protocols/llmnr.py old mode 100755 new mode 100644 diff --git a/dementor/protocols/mdns.py b/dementor/protocols/mdns.py old mode 100755 new mode 100644 diff --git a/dementor/protocols/mysql.py b/dementor/protocols/mysql.py index 322d830..265d96f 100644 --- a/dementor/protocols/mysql.py +++ b/dementor/protocols/mysql.py @@ -344,7 +344,7 @@ class HandshakeResponse: # filler to the size of the handhshake response packet. All 0s. filler: f[bytes, Bytes(23)] = b"\0" * 23 - # login user name + # login user name username: cstr_t # opaque authentication response data generated by Authentication Method diff --git a/dementor/protocols/netbios.py b/dementor/protocols/netbios.py old mode 100755 new mode 100644 diff --git a/dementor/protocols/ntlm.py b/dementor/protocols/ntlm.py old mode 100755 new mode 100644 index 3bc0c73..ba96fd0 --- a/dementor/protocols/ntlm.py +++ b/dementor/protocols/ntlm.py @@ -81,18 +81,18 @@ # =========================================================================== # NTLMv1 NtChallengeResponse and LmChallengeResponse are always exactly -# 24 bytes (DESL output per §6). NTLMv2 NtChallengeResponse is always -# > 24 bytes (NTProofStr(16) + variable Blob per §2.2.2.8). +# 24 bytes (DESL output per S6). NTLMv2 NtChallengeResponse is always +# > 24 bytes (NTProofStr(16) + variable Blob per S2.2.2.8). # Sole discriminator between v1 and v2; the ESS flag does NOT imply v2. NTLMV1_RESPONSE_LEN: int = 24 -# ServerChallenge nonce length (§2.2.1.2). +# ServerChallenge nonce length (S2.2.1.2). NTLM_CHALLENGE_LEN: int = 8 -# NTProofStr length in an NTLMv2 NtChallengeResponse (§3.3.2). +# NTProofStr length in an NTLMv2 NtChallengeResponse (S3.3.2). NTLM_NTPROOFSTR_LEN: int = 16 -# TargetName payload offset in CHALLENGE_MESSAGE: fixed header is 56 bytes (§2.2.1.2). +# TargetName payload offset in CHALLENGE_MESSAGE: fixed header is 56 bytes (S2.2.1.2). NTLM_CHALLENGE_MSG_DOMAIN_OFFSET: int = 56 # 16 zero bytes used as the ESS padding suffix in LmChallengeResponse and @@ -125,19 +125,19 @@ NTLM_TRANSPORT_NTLMSSP: str = "ntlmssp" # =========================================================================== -# Hash Types (MS-NLMP §3.3) +# Hash Types (MS-NLMP S3.3) # =========================================================================== # Classification is based on NT response length and LM response content. # # Type NT len LM len / content HC mode MS-NLMP ref -# ─────────── ──────── ─────────────────── ──────── ───────────────────── -# NetNTLMv1 24 any / non-dummy 5500 §3.3.1 plain DES -# NetNTLMv1-ESS 24 24 / LM[8:]==Z(16) 5500* §3.3.1 + ESS -# NetNTLMv2 > 24 n/a 5600 §3.3.2 HMAC-MD5 blob -# NetLMv2 > 24† 24 / non-null 5600† §3.3.2 LMv2 companion +# ----------- -------- ------------------- -------- --------------------- +# NetNTLMv1 24 any / non-dummy 5500 S3.3.1 plain DES +# NetNTLMv1-ESS 24 24 / LM[8:]==Z(16) 5500* S3.3.1 + ESS +# NetNTLMv2 > 24 n/a 5600 S3.3.2 HMAC-MD5 blob +# NetLMv2 > 24* 24 / non-null 5600* S3.3.2 LMv2 companion # # * Mode 5500 auto-detects ESS via LM[8:24]==Z(16); always emit raw ServerChallenge. -# † NetLMv2 is always paired with NetNTLMv2; both use -m 5600. +# * NetLMv2 is always paired with NetNTLMv2; both use -m 5600. # # Hashcat formats (module_05500.c and module_05600.c): # NetNTLMv1 user::domain:LM(48 hex):NT(48 hex):ServerChallenge(16 hex) @@ -145,17 +145,17 @@ # NetNTLMv2 user::domain:ServerChallenge(16 hex):NTProofStr(32 hex):Blob(var hex) # NetLMv2 user::domain:ServerChallenge(16 hex):LMProof(32 hex):CChal(16 hex) # -# ESS detection (§3.3.1): LmChallengeResponse = ClientChallenge(8) || Z(16). +# ESS detection (S3.3.1): LmChallengeResponse = ClientChallenge(8) || Z(16). # len==24 and LM[8:]==Z(16) is the sole reliable signal; the ESS negotiate # flag is supplementary only. For NTLM_TRANSPORT_RAW there are no flags, # so only the byte structure is checked. # # ============================================================================= -# Responder → Dementor label mapping +# Responder -> Dementor label mapping # ============================================================================= # # Responder label Dementor label Reason -# ─────────────── ─────────────────── ──────────────────────────────────────── +# --------------- ------------------- ---------------------------------------- # NTLMv1-SSP NetNTLMv1 or Responder collapses both; ESS changes the # NetNTLMv1-ESS effective challenge and must be distinct. # NTLMv2-SSP NetNTLMv2 Responder threshold: len > 60; spec minimum @@ -192,7 +192,7 @@ def NTLM_AUTH_classify( if nt_len > NTLMV1_RESPONSE_LEN: return NTLM_V2 - # ESS: per §3.3.1 ComputeResponse, LmChallengeResponse = ClientChallenge(8) || Z(16). + # ESS: per S3.3.1 ComputeResponse, LmChallengeResponse = ClientChallenge(8) || Z(16). # This mandates exactly 24 bytes; the byte structure is the sole reliable signal. # The NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY flag is cross-checked only. try: @@ -227,7 +227,7 @@ def NTLM_AUTH_classify( # Challenge parsing is handled by BytesValue(NTLM_CHALLENGE_LEN) from -# dementor.config.util — supports hex:/ascii: prefixes, auto-detect, +# dementor.config.util -- supports hex:/ascii: prefixes, auto-detect, # and length validation in a single reusable helper. _parse_challenge = BytesValue(NTLM_CHALLENGE_LEN) @@ -328,7 +328,7 @@ def apply_config(session: SessionConfig) -> None: if session.ntlm_disable_ntlmv2: dm_logger.warning( - "NTLM DisableNTLMv2 is enabled — Level 3+ clients (all modern Windows) " + "NTLM DisableNTLMv2 is enabled -- Level 3+ clients (all modern Windows) " + "will FAIL authentication and NO hashes will be captured. " + "This only helps against pre-Vista / manually-configured Level 0-2 clients. " + "Use with caution." @@ -336,12 +336,12 @@ def apply_config(session: SessionConfig) -> None: # =========================================================================== -# Wire Encoding Helpers [MS-NLMP §2.2 and §2.2.2.5] +# Wire Encoding Helpers [MS-NLMP S2.2 and S2.2.2.5] # # NEGOTIATE_MESSAGE fields: always OEM (Unicode not yet negotiated). # CHALLENGE_MESSAGE / AUTHENTICATE_MESSAGE: governed by NegotiateFlags: -# NTLMSSP_NEGOTIATE_UNICODE (0x01) → UTF-16LE (no BOM) -# NTLM_NEGOTIATE_OEM (0x02) → cp437 baseline +# NTLMSSP_NEGOTIATE_UNICODE (0x01) -> UTF-16LE (no BOM) +# NTLM_NEGOTIATE_OEM (0x02) -> cp437 baseline # =========================================================================== @@ -394,7 +394,7 @@ def NTLM_AUTH_encode_string(string: str | None, negotiate_flags: int) -> bytes: # =========================================================================== -# Dummy LM Response Filtering [MS-NLMP §3.3.1] +# Dummy LM Response Filtering [MS-NLMP S3.3.1] # # When no LM hash is available (password > 14 chars or NoLMHash policy), # the client fills LmChallengeResponse with DESL() of a known dummy input: @@ -405,7 +405,7 @@ def NTLM_AUTH_encode_string(string: str | None, negotiate_flags: int) -> bytes: def _compute_dummy_lm_responses(server_challenge: bytes) -> set[bytes]: - """Compute the two known dummy LmChallengeResponse values (per §3.3.1). + """Compute the two known dummy LmChallengeResponse values (per S3.3.1). :param bytes server_challenge: 8-byte ServerChallenge from the CHALLENGE_MESSAGE :return: Two 24-byte DESL() outputs for the null and empty-string LM hashes. @@ -558,7 +558,7 @@ def NTLM_AUTH_to_hashcat_formats( comparisons appear in the branches below. - Dummy LM responses (DESL of null or empty-string LM hash) are discarded. - Level 2 duplication (LM == NT) omits the LM slot. - - Per §3.3.2 rule 7: when MsvAvTimestamp is present, clients set + - Per S3.3.2 rule 7: when MsvAvTimestamp is present, clients set LmChallengeResponse to Z(24); this null LMv2 is detected and skipped. """ if len(server_challenge) != NTLM_CHALLENGE_LEN: @@ -622,7 +622,7 @@ def NTLM_AUTH_to_hashcat_formats( server_challenge_hex: str = server_challenge.hex() - # NetNTLMv2: NtChallengeResponse = NTProofStr(16) + Blob(var) per §2.2.2.8 + # NetNTLMv2: NtChallengeResponse = NTProofStr(16) + Blob(var) per S2.2.2.8 # hashcat -m 5600: User::Domain:ServerChallenge:NTProofStr:Blob if hash_type == NTLM_V2: try: @@ -643,7 +643,7 @@ def NTLM_AUTH_to_hashcat_formats( return captures # NetLMv2 companion: HMAC-MD5(ResponseKeyLM, Server||Client)[0:16] || CChal(8) - # Per §3.3.2 rule 7: if MsvAvTimestamp was in the challenge, clients send Z(24). + # Per S3.3.2 rule 7: if MsvAvTimestamp was in the challenge, clients send Z(24). # hashcat -m 5600: User::Domain:ServerChallenge:LMProof:ClientChallenge try: if len(lm_response) == NTLMV1_RESPONSE_LEN: @@ -682,7 +682,7 @@ def NTLM_AUTH_to_hashcat_formats( return captures - # NetNTLMv1-ESS: per §3.3.1, ESS uses MD5(Server||Client)[0:8] as the challenge. + # NetNTLMv1-ESS: per S3.3.1, ESS uses MD5(Server||Client)[0:8] as the challenge. # Hashcat -m 5500 derives the mixed challenge internally; emit raw ServerChallenge. # LM field: ClientChallenge(8) || Z(16) = 24 bytes. if hash_type == NTLM_V1_ESS: @@ -711,20 +711,20 @@ def NTLM_AUTH_to_hashcat_formats( # LM slot is optional (0 or 48 hex chars); including a real LM response # enables the DES third-key optimisation. Two cases skip the LM slot: # 1. Level 2 duplication: client copies NT into LM (wrong one-way function). - # 2. Dummy LM: DESL() with null/empty-string hash — no crackable material. + # 2. Dummy LM: DESL() with null/empty-string hash -- no crackable material. try: nt_response_hex = nt_response.hex() lm_slot_hex: str = "" if len(lm_response) == NTLMV1_RESPONSE_LEN: if lm_response == nt_response: - # Case 1: duplication — LM is a copy of NT, skip it + # Case 1: duplication -- LM is a copy of NT, skip it dm_logger.debug( "LmChallengeResponse == NtChallengeResponse " + "(Level 2 duplication); omitting LM slot" ) elif lm_response in _compute_dummy_lm_responses(server_challenge): - # Case 2: dummy DESL output — no crackable credential material + # Case 2: dummy DESL output -- no crackable credential material dm_logger.debug( "LmChallengeResponse matches dummy LM hash; omitting LM slot" ) @@ -760,7 +760,7 @@ def NTLM_new_timestamp() -> int: :return: Current UTC time in 100-nanosecond intervals since Windows epoch (1601-01-01) :rtype: int """ - # calendar.timegm() → UTC seconds since 1970; scaled to 100ns ticks since 1601. + # calendar.timegm() -> UTC seconds since 1970; scaled to 100ns ticks since 1601. return ( NTLM_FILETIME_EPOCH_OFFSET + calendar.timegm(time.gmtime()) * NTLM_FILETIME_TICKS_PER_SECOND @@ -792,7 +792,7 @@ def NTLM_split_fqdn(fqdn: str) -> tuple[str, str]: def NTLM_AUTH_is_anonymous(token: ntlm.NTLMAuthChallengeResponse) -> bool: """Return True if the AUTHENTICATE_MESSAGE is an anonymous (null session) auth. - Per §3.2.5.1.2 server-side logic, null session is structural: + Per S3.2.5.1.2 server-side logic, null session is structural: UserName empty, NtChallengeResponse empty, and LmChallengeResponse empty or Z(1). For capture-first operation, do not trust the anonymous flag alone, and do not fail-closed on parsing exceptions. @@ -854,7 +854,7 @@ def NTLM_AUTH_CreateChallenge( """Build a CHALLENGE_MESSAGE from the client's NEGOTIATE_MESSAGE flags. :param ntlm.NTLMAuthNegotiate|dict token: Parsed NEGOTIATE_MESSAGE (must have a "flags" key) - :param str name: Server NetBIOS computer name — the flat hostname label, e.g. + :param str name: Server NetBIOS computer name -- the flat hostname label, e.g. "DEMENTOR" or "SERVER1". Must not contain a dot; callers should obtain this from NTLM_split_fqdn :param str domain: Server DNS domain name or "WORKGROUP", e.g. "corp.example.com". @@ -949,7 +949,7 @@ def NTLM_AUTH_CreateChallenge( dm_logger.debug("LM_KEY flag echoed into CHALLENGE_MESSAGE") # -- VERSION negotiation ------------------------------------------------- - # Per §2.2.1.2 and §3.2.5.1.1, Version should be populated only when + # Per S2.2.1.2 and S3.2.5.1.1, Version should be populated only when # NTLMSSP_NEGOTIATE_VERSION is negotiated; otherwise it must be all-zero. if client_flags & ntlm.NTLMSSP_NEGOTIATE_VERSION: response_flags |= ntlm.NTLMSSP_NEGOTIATE_VERSION @@ -959,7 +959,7 @@ def NTLM_AUTH_CreateChallenge( response_flags &= ~ntlm.NTLMSSP_NEGOTIATE_LM_KEY # -- Assemble the CHALLENGE_MESSAGE ------------------------------------ - # TargetName (§2.2.1.2): the server's authentication realm. A domain- + # TargetName (S2.2.1.2): the server's authentication realm. A domain- # joined server returns the NetBIOS domain name (flat, first DNS label, # uppercase); a workgroup server returns the NetBIOS computer name. # We always use the domain: NTLM_split_fqdn guarantees `domain` is @@ -979,13 +979,13 @@ def NTLM_AUTH_CreateChallenge( challenge_message["Version"] = NTLM_VERSION_PLACEHOLDER challenge_message["VersionLen"] = NTLM_VERSION_LEN - # TargetInfoFields (§2.2.1.2) sits immediately after TargetName in the + # TargetInfoFields (S2.2.1.2) sits immediately after TargetName in the # wire payload; its buffer offset is computed from TargetName's length. target_info_offset: int = NTLM_CHALLENGE_MSG_DOMAIN_OFFSET + len(target_name_bytes) if disable_ntlmv2: # Omitting TargetInfoFields prevents the client from constructing - # an NTLMv2 Blob (§3.3.2), forcing NTLMv1-capable clients to fall + # an NTLMv2 Blob (S3.3.2), forcing NTLMv1-capable clients to fall # back to NTLMv1. Level 3+ clients will refuse to authenticate. challenge_message["TargetInfoFields_len"] = 0 challenge_message["TargetInfoFields_max_len"] = 0 @@ -993,8 +993,8 @@ def NTLM_AUTH_CreateChallenge( challenge_message["TargetInfoFields_offset"] = target_info_offset dm_logger.debug("TargetInfoFields omitted (disable_ntlmv2=True)") else: - # TargetInfo is a sequence of AV_PAIR structures (§2.2.2.1). - # Full AvId space — disposition for each entry: + # TargetInfo is a sequence of AV_PAIR structures (S2.2.2.1). + # Full AvId space -- disposition for each entry: # # AvId Constant Sent Notes # 0x0000 MsvAvEOL auto List terminator; ntlm.AV_PAIRS appends it. @@ -1004,14 +1004,14 @@ def NTLM_AUTH_CreateChallenge( # 0x0004 MsvAvDnsDomainName YES DNS domain FQDN. # 0x0005 MsvAvDnsTreeName COND Forest FQDN; omitted when not domain-joined. # 0x0006 MsvAvFlags NO Constrained-auth flag (0x1); not applicable - # here — Dementor does not enforce constrained - # delegation. 0x2/0x4 bits are client→server. + # here -- Dementor does not enforce constrained + # delegation. 0x2/0x4 bits are client->server. # 0x0007 MsvAvTimestamp NO Intentionally omitted; see note below. - # 0x0008 MsvAvSingleHost N/A Client→server only (AUTHENTICATE_MESSAGE). - # 0x0009 MsvAvTargetName N/A Client→server only (AUTHENTICATE_MESSAGE). - # 0x000A MsvAvChannelBindings N/A Client→server only (AUTHENTICATE_MESSAGE). + # 0x0008 MsvAvSingleHost N/A Client->server only (AUTHENTICATE_MESSAGE). + # 0x0009 MsvAvTargetName N/A Client->server only (AUTHENTICATE_MESSAGE). + # 0x000A MsvAvChannelBindings N/A Client->server only (AUTHENTICATE_MESSAGE). # - # §2.2.2.1: 0x0001 and 0x0002 MUST be present. MsvAvEOL is + # S2.2.2.1: 0x0001 and 0x0002 MUST be present. MsvAvEOL is # appended automatically by ntlm.AV_PAIRS. AV_PAIRs may appear in # any order per spec; ascending AvId matches real Windows behaviour. @@ -1040,7 +1040,7 @@ def NTLM_AUTH_CreateChallenge( # 3. Encoding ------------------------------------------------------- # NTLM_AUTH_encode_string selects UTF-16LE or OEM based on the - # negotiated UNICODE flag in response_flags. Per §2.2.2.1 (note), + # negotiated UNICODE flag in response_flags. Per S2.2.2.1 (note), # TargetInfo AV_PAIR values MUST be Unicode regardless of the # negotiated encoding; all modern clients negotiate UNICODE, so this # is consistent in practice. @@ -1074,7 +1074,7 @@ def NTLM_AUTH_CreateChallenge( ) # MsvAvTimestamp (0x0007) is intentionally NOT included. - # Per §3.3.2 rule 7: when the server sends MsvAvTimestamp, the + # Per S3.3.2 rule 7: when the server sends MsvAvTimestamp, the # client MUST NOT send an LmChallengeResponse (sets it to Z(24)). # Omitting it ensures clients still send a real LMv2 alongside the # NetNTLMv2 response, maximising the number of captured hash types. diff --git a/dementor/protocols/quic.py b/dementor/protocols/quic.py old mode 100755 new mode 100644 diff --git a/dementor/protocols/smtp.py b/dementor/protocols/smtp.py old mode 100755 new mode 100644 index 6202021..80a6794 --- a/dementor/protocols/smtp.py +++ b/dementor/protocols/smtp.py @@ -275,7 +275,7 @@ async def chapture_ntlm_auth(self, server: SMTPServerBase, blob=None) -> Any: self.logger, ) if self.server_config.smtp_downgrade: - # Perform a simple donẃngrade attack by sending failed authentication + # Perform a simple downgrade attack by sending failed authentication # - Some clients may choose to use fall back to other login mechanisms # provided by the server self.logger.display( diff --git a/dementor/protocols/spnego.py b/dementor/protocols/spnego.py old mode 100755 new mode 100644 diff --git a/dementor/protocols/ssdp.py b/dementor/protocols/ssdp.py index 349b76f..b6bba68 100644 --- a/dementor/protocols/ssdp.py +++ b/dementor/protocols/ssdp.py @@ -124,12 +124,12 @@ class UDN: def __init__(self, udn: str) -> None: self.tokens = udn.split(":") - # Shall begin with “uuid:” followed by a UUID suffix specified by a UPnP vendor. + # Shall begin with "uuid:" followed by a UUID suffix specified by a UPnP vendor. @property def udn_uuid(self) -> str: return self.tokens[1] - # [Table 1-1 — Root device discovery messages] + # [Table 1-1 -- Root device discovery messages] # uuid:device-UUID::upnp:rootdevice # or # uuid:device-UUID @@ -319,7 +319,7 @@ def handle_search(self, transport): header_buffer = [ SSDP_OK_H.decode(), # CACHE-CONTROL - # Required. Field value shall have the max-age directive (“max-age=”) followed by + # Required. Field value shall have the max-age directive ("max-age=") followed by # an integer that specifies the number of seconds the advertisement is valid. f"CACHE-CONTROL: max-age={self.ssdp_config.ssdp_max_age}", # EXT diff --git a/dementor/servers.py b/dementor/servers.py old mode 100755 new mode 100644 diff --git a/dementor/tui/commands/config.py b/dementor/tui/commands/config.py old mode 100755 new mode 100644 index 165fa26..6d8ebca --- a/dementor/tui/commands/config.py +++ b/dementor/tui/commands/config.py @@ -195,7 +195,7 @@ def _resolve_key_path( invalid = False if not key: - # Empty key is considered invalid – keep defaults + # Empty key is considered invalid - keep defaults invalid = True else: # Helper for case-insensitive dict lookup returning the actual key @@ -226,7 +226,7 @@ def ci_lookup(d: dict[str, Any], lookup: str) -> str | None: invalid = True break if i == len(parts) - 1: - # Final element – return the list and index + # Final element - return the list and index result_container = current result_key = idx break @@ -248,7 +248,7 @@ def ci_lookup(d: dict[str, Any], lookup: str) -> str | None: result_container = current result_key = actual_key break - # Intermediate segment – move deeper + # Intermediate segment - move deeper actual_key = ci_lookup(current, part) if actual_key is None: invalid = True diff --git a/dementor/tui/commands/database.py b/dementor/tui/commands/database.py index b25c7b8..b978294 100644 --- a/dementor/tui/commands/database.py +++ b/dementor/tui/commands/database.py @@ -310,7 +310,7 @@ def get_completions(self, word: str, document: Document) -> list[str]: except Exception: tokens = document.text_before_cursor.split() - # No sub-command yet – suggest the four possible actions. + # No sub-command yet - suggest the four possible actions. subcommands = ["creds", "hosts", "clean", "export"] if len(tokens) <= 1: return [sc for sc in subcommands if sc.startswith(word)] diff --git a/dementor/tui/commands/env.py b/dementor/tui/commands/env.py old mode 100755 new mode 100644 diff --git a/dementor/tui/commands/ipconfig.py b/dementor/tui/commands/ipconfig.py old mode 100755 new mode 100644 diff --git a/dementor/tui/completer.py b/dementor/tui/completer.py old mode 100755 new mode 100644 index 705a8a2..cc6a232 --- a/dementor/tui/completer.py +++ b/dementor/tui/completer.py @@ -92,7 +92,7 @@ def get_completions(self, document: Document, complete_event: CompleteEvent): except Exception: tokens = text_before.split() - # No tokens yet → suggest command names. + # No tokens yet -> suggest command names. if not tokens: for name in self._iter_command_names(): if name.startswith(word): diff --git a/docs/source/compat.rst b/docs/source/compat.rst index be43fd4..7db0fa2 100644 --- a/docs/source/compat.rst +++ b/docs/source/compat.rst @@ -727,7 +727,7 @@ in development. The legend for each symbol is as follows: -

[1]: Responder combines NetNTLMv1 and NetNTLMv1-ESS under a single "NTLMv1-SSP" label. This is not incorrect — hashcat -m 5500 handles both — but Dementor distinguishes them for more granular reporting. Applies to all NTLM-capable protocols (SMB, HTTP, MSSQL, LDAP, DCE/RPC).

+

[1]: Responder combines NetNTLMv1 and NetNTLMv1-ESS under a single "NTLMv1-SSP" label. This is not incorrect -- hashcat -m 5500 handles both -- but Dementor distinguishes them for more granular reporting. Applies to all NTLM-capable protocols (SMB, HTTP, MSSQL, LDAP, DCE/RPC).

NTLM Spcifics

diff --git a/docs/source/config/database.rst b/docs/source/config/database.rst index 5e47ebd..1147f53 100644 --- a/docs/source/config/database.rst +++ b/docs/source/config/database.rst @@ -80,7 +80,7 @@ Options .. note:: The database driver (e.g. ``pymysql``, ``psycopg2``) must be installed - separately — it is not bundled with Dementor. + separately -- it is not bundled with Dementor. .. py:attribute:: Path @@ -96,12 +96,12 @@ Options * **Relative paths** are resolved from the workspace directory (:attr:`Dementor.Workspace`). * **Absolute paths** are used as-is. - * ``:memory:`` creates an in-memory database — fast, but all data is lost + * ``:memory:`` creates an in-memory database -- fast, but all data is lost when Dementor exits. The TUI can still query credentials while running. .. code-block:: toml - # Default — file in the workspace directory + # Default -- file in the workspace directory Path = "Dementor.db" # Subfolder (created automatically if it doesn't exist) @@ -110,7 +110,7 @@ Options # Absolute path Path = "/opt/dementor/creds.db" - # In-memory — fast, but data is lost on exit + # In-memory -- fast, but data is lost on exit Path = ":memory:" .. tip:: @@ -127,9 +127,9 @@ Options Controls whether duplicate credentials are stored in the database. - * ``true`` *(default)* — Every captured hash is stored, even if the same + * ``true`` *(default)* -- Every captured hash is stored, even if the same credential was already seen in this session. - * ``false`` — Only the first capture of each unique credential is stored. + * ``false`` -- Only the first capture of each unique credential is stored. Subsequent duplicates are silently skipped. A credential is considered a duplicate when all four of these fields match @@ -143,7 +143,7 @@ Options .. note:: The hash is always written to the log file stream regardless of this - setting, so no captured data is ever lost — only the database storage + setting, so no captured data is ever lost -- only the database storage is affected. .. tip:: @@ -166,7 +166,7 @@ ignored if still present in your configuration file. .. versionremoved:: 1.0.0.dev22 - **Removed.** The SQL dialect is now determined automatically — from + **Removed.** The SQL dialect is now determined automatically -- from :attr:`Url` when set, or defaults to ``sqlite`` when using :attr:`Path`. .. py:attribute:: Driver @@ -177,7 +177,7 @@ ignored if still present in your configuration file. .. versionremoved:: 1.0.0.dev22 - **Removed.** The SQL driver is now determined automatically — from + **Removed.** The SQL driver is now determined automatically -- from :attr:`Url` when set, or defaults to ``pysqlite`` when using :attr:`Path`. .. py:attribute:: Directory diff --git a/docs/source/config/dcerpc.rst b/docs/source/config/dcerpc.rst index 773dad7..7b39ff9 100644 --- a/docs/source/config/dcerpc.rst +++ b/docs/source/config/dcerpc.rst @@ -55,7 +55,7 @@ Section ``[RPC]`` Return values: - - ``0``: Success — continue listening for additional packets. + - ``0``: Success -- continue listening for additional packets. - Any other value: An error is sent in a *FAULT* response and the connection is closed. .. py:class:: RPCEndpointHandlerClass() @@ -163,7 +163,7 @@ Section ``[EPM]`` .. attention:: - The random port is selected **once at startup** — not per client. + The random port is selected **once at startup** -- not per client. Default Configuration --------------------- diff --git a/docs/source/config/globals.rst b/docs/source/config/globals.rst index 0523aa7..d9046e8 100644 --- a/docs/source/config/globals.rst +++ b/docs/source/config/globals.rst @@ -6,7 +6,7 @@ Globals ======= The ``[Globals]`` section allows defining settings that are applied across all -protocols — given the protocol supports global overrides. Some protocol-specific +protocols -- given the protocol supports global overrides. Some protocol-specific options may be limited to local scope. When available, options that support this section include a reference to it in their documentation. This section covers common configuration values, that can be shared across multiple services. @@ -72,7 +72,7 @@ Advanced filtering can be done using dictionary-based filter objects: Allows loading filter expressions from an external file instead of specifying them inline. .. hint:: - Filter objects may include custom metadata — referred to as *extras* — which are passed to the final + Filter objects may include custom metadata -- referred to as *extras* -- which are passed to the final :class:`~dementor.filters.FilterObj`. While currently unused, these extras may enable specialized handling for specific targets in future versions. diff --git a/docs/source/config/http.rst b/docs/source/config/http.rst index d8e8081..6df0245 100644 --- a/docs/source/config/http.rst +++ b/docs/source/config/http.rst @@ -37,7 +37,7 @@ Section ``[HTTP]`` defined within each individual** ``[[HTTP.Server]]`` **section.** The attributes described below may also be specified in the global ``[HTTP]`` section, where they will serve - as default values for all individual server entries — unless explicitly overridden. + as default values for all individual server entries -- unless explicitly overridden. .. py:attribute:: Server.ServerType diff --git a/docs/source/config/imap.rst b/docs/source/config/imap.rst index d586e3b..333b757 100644 --- a/docs/source/config/imap.rst +++ b/docs/source/config/imap.rst @@ -29,7 +29,7 @@ Section ``[IMAP]`` This value must be specified within a ``[[IMAP.Server]]`` section. The attributes described below may also be specified in the global ``[IMAP]`` section, where they act - as defaults for all individual server entries — unless explicitly overridden. + as defaults for all individual server entries -- unless explicitly overridden. .. py:attribute:: Server.Capabilities :type: str @@ -65,9 +65,9 @@ Section ``[IMAP]`` Lists the authentication mechanisms supported by the server. Currently implemented options: - - ``LOGIN`` — Base64-encoded challenge-based login. - - ``PLAIN`` — Sends credentials in cleartext. - - ``NTLM`` — Implements NTLM authentication per `[MS-SMTPNTLM] `_. + - ``LOGIN`` -- Base64-encoded challenge-based login. + - ``PLAIN`` -- Sends credentials in cleartext. + - ``NTLM`` -- Implements NTLM authentication per `[MS-SMTPNTLM] `_. To enforce NTLM-only authentication, remove ``LOGIN`` and ``PLAIN``. For downgrade attacks, refer to :attr:`SMTP.Server.Downgrade`. diff --git a/docs/source/config/index.rst b/docs/source/config/index.rst index 4eac5a0..bcccb82 100644 --- a/docs/source/config/index.rst +++ b/docs/source/config/index.rst @@ -4,7 +4,7 @@ Configuration ============= -Configuration can be tedious—but it's also one of the most important aspects, and +Configuration can be tedious--but it's also one of the most important aspects, and good documentation makes all the difference. The sections listed below provide detailed explanations for each configuration area. diff --git a/docs/source/config/ldap.rst b/docs/source/config/ldap.rst index 4066550..bcc6793 100644 --- a/docs/source/config/ldap.rst +++ b/docs/source/config/ldap.rst @@ -37,7 +37,7 @@ Section ``[LDAP]`` The attributes described below may also be specified in the global ``[LDAP]`` section, where they will serve - as default values for all individual server entries — unless explicitly overridden. + as default values for all individual server entries -- unless explicitly overridden. .. py:attribute:: Server.Capabilities diff --git a/docs/source/config/ntlm.rst b/docs/source/config/ntlm.rst index 77fd1f8..97635b4 100644 --- a/docs/source/config/ntlm.rst +++ b/docs/source/config/ntlm.rst @@ -21,10 +21,10 @@ Section ``[NTLM]`` The value must represent exactly ``8`` bytes and can be given in any of the following formats: - - ``"hex:1122334455667788"`` — explicit hex (recommended) - - ``"ascii:1337LEET"`` — explicit ASCII (recommended) - - ``"1122334455667788"`` — 16 hex characters (auto-detected as hex) - - ``"1337LEET"`` — 8 ASCII characters (auto-detected as ASCII) + - ``"hex:1122334455667788"`` -- explicit hex (recommended) + - ``"ascii:1337LEET"`` -- explicit ASCII (recommended) + - ``"1122334455667788"`` -- 16 hex characters (auto-detected as hex) + - ``"1337LEET"`` -- 8 ASCII characters (auto-detected as ASCII) If this option is omitted, a cryptographically random challenge is generated once at startup and reused for all connections. @@ -86,13 +86,13 @@ Section ``[NTLM]`` **Effect on captured hashes:** - - ``false`` (default) — ESS is negotiated when the client requests it. + - ``false`` (default) -- ESS is negotiated when the client requests it. NTLMv1 clients produce **NetNTLMv1-ESS** hashes (hashcat ``-m 5500``). ESS uses ``MD5(ServerChallenge ‖ ClientChallenge)[0:8]`` as the effective challenge; hashcat derives this internally from the emitted ``ClientChallenge`` field. - - ``true`` — ESS is suppressed. NTLMv1 clients produce plain **NetNTLMv1** + - ``true`` -- ESS is suppressed. NTLMv1 clients produce plain **NetNTLMv1** hashes. A fixed :attr:`Challenge` combined with rainbow tables can crack these without GPU resources. @@ -113,11 +113,11 @@ Section ``[NTLM]`` **Effect on captured hashes:** - - ``false`` (default) — ``TargetInfoFields`` is populated. Clients can + - ``false`` (default) -- ``TargetInfoFields`` is populated. Clients can construct an NTLMv2 response and produce **NetNTLMv2** and **NetLMv2** hashes (hashcat ``-m 5600``). - - ``true`` — ``TargetInfoFields`` is empty. Without it, clients cannot + - ``true`` -- ``TargetInfoFields`` is empty. Without it, clients cannot build the NTLMv2 blob per ``[MS-NLMP §3.3.2]``. LmCompatibilityLevel 0-2 clients fall back to NTLMv1. Level 3+ clients (all modern Windows) will **fail authentication** and @@ -208,15 +208,15 @@ CHALLENGE_MESSAGE Construction The ``CHALLENGE_MESSAGE`` is built directly from the client's ``NEGOTIATE_MESSAGE`` flags: -- **Flag mirroring** — ``SIGN``, ``SEAL``, ``ALWAYS_SIGN``, ``KEY_EXCH``, +- **Flag mirroring** -- ``SIGN``, ``SEAL``, ``ALWAYS_SIGN``, ``KEY_EXCH``, ``56``, ``128``, ``UNICODE``, and ``OEM`` are echoed when requested. Failing to echo ``SIGN`` causes strict clients to abort before sending the ``AUTHENTICATE_MESSAGE``, losing the capture. -- **ESS** — echoed only when the client requests it and +- **ESS** -- echoed only when the client requests it and :attr:`DisableExtendedSessionSecurity` is ``false``. When both ESS and ``LM_KEY`` are requested, only ESS is returned (§2.2.2.5 flag P mutual exclusivity). -- **Version** — a placeholder ``\\x00 * 8`` is used. The VERSION structure +- **Version** -- a placeholder ``\\x00 * 8`` is used. The VERSION structure content is not verified by clients per §2.2.2.10. AV_PAIRS (``TargetInfoFields``) @@ -255,7 +255,7 @@ AvId and gives concrete values for two typical ``FQDN`` settings: - ``corp.example.com`` * - ``0x0005`` - ``MsvAvDnsTreeName`` - - *(omitted — no domain suffix)* + - *(omitted -- no domain suffix)* - ``corp.example.com`` A bare hostname such as ``"DEMENTOR"`` contains no dot, so Dementor treats @@ -275,14 +275,14 @@ LM Response Filtering For **NetNTLMv1** captures, the LM slot in the hashcat line is omitted when any of the following conditions hold: -- **Identical response** — ``LmChallengeResponse == NtChallengeResponse``. +- **Identical response** -- ``LmChallengeResponse == NtChallengeResponse``. Using the LM copy with the NT one-way function during cracking would yield incorrect results. -- **Long-password placeholder** — ``LmChallengeResponse == DESL(Z(16))``. +- **Long-password placeholder** -- ``LmChallengeResponse == DESL(Z(16))``. Clients send this deterministic value when the password exceeds 14 characters or the ``NoLMHash`` registry policy is enforced. It carries no crackable material. -- **Empty-password placeholder** — ``LmChallengeResponse == DESL(LMOWFv1(""))``. +- **Empty-password placeholder** -- ``LmChallengeResponse == DESL(LMOWFv1(""))``. The LM derivative of an empty password; equally uncrackable. For **NetNTLMv2**, the NetLMv2 companion hash is captured alongside the NetNTLMv2 @@ -315,10 +315,10 @@ Default Configuration [NTLM] # 8-byte ServerChallenge nonce. Accepted formats: - # "hex:1122334455667788" — explicit hex (recommended) - # "ascii:1337LEET" — explicit ASCII (recommended) - # "1122334455667788" — 16 hex chars, auto-detected - # "1337LEET" — 8 ASCII chars, auto-detected + # "hex:1122334455667788" -- explicit hex (recommended) + # "ascii:1337LEET" -- explicit ASCII (recommended) + # "1122334455667788" -- 16 hex chars, auto-detected + # "1337LEET" -- 8 ASCII chars, auto-detected # Omit entirely for a cryptographically random value per run (recommended). Challenge = "1337LEET" @@ -360,7 +360,7 @@ to the hash type Dementor captures and the relevant hashcat mode. - ``-m 5500`` * - 2 - NTLMv1 in both LM and NT slots - - NetNTLMv1 (LM slot filtered — see `LM Response Filtering`_) + - NetNTLMv1 (LM slot filtered -- see `LM Response Filtering`_) - ``-m 5500`` * - 3 - NTLMv2 + LMv2 diff --git a/docs/source/config/pop3.rst b/docs/source/config/pop3.rst index fb15baf..719046c 100644 --- a/docs/source/config/pop3.rst +++ b/docs/source/config/pop3.rst @@ -29,7 +29,7 @@ Section ``[POP3]`` This value must be specified within a ``[[POP3.Server]]`` section. The attributes described below may also be specified in the global ``[POP3]`` section, where they will serve - as default values for all individual server entries — unless explicitly overridden. + as default values for all individual server entries -- unless explicitly overridden. .. py:attribute:: Server.FQDN :type: str diff --git a/docs/source/config/smb.rst b/docs/source/config/smb.rst index 45d180e..df86480 100644 --- a/docs/source/config/smb.rst +++ b/docs/source/config/smb.rst @@ -236,7 +236,7 @@ Section ``[SMB]`` *Corresponds to* :attr:`NTLM.DisableNTLMv2` When ``True``, ``TargetInfoFields`` is omitted from the ``CHALLENGE_MESSAGE``. - Level 0–2 clients fall back to NTLMv1; level 3+ clients fail with no capture. + Level 0-2 clients fall back to NTLMv1; level 3+ clients fail with no capture. Protocol Behaviour @@ -247,9 +247,9 @@ Authentication Flow The SMB handler accepts NTLM tokens in two forms: -- **NTLM SSP** — the security buffer begins with ``NTLMSSP\0`` and is consumed +- **NTLM SSP** -- the security buffer begins with ``NTLMSSP\0`` and is consumed directly by the three-message NTLM handshake (``NEGOTIATE → CHALLENGE → AUTHENTICATE``). -- **GSSAPI / SPNEGO** — the buffer is wrapped in a ``negTokenInit`` (tag ``0x60``) or +- **GSSAPI / SPNEGO** -- the buffer is wrapped in a ``negTokenInit`` (tag ``0x60``) or ``negTokenTarg`` (tag ``0xA1``) envelope. Dementor unwraps the SPNEGO layer, performs the NTLM handshake internally, and returns appropriately wrapped ``negTokenTarg`` responses. @@ -280,11 +280,11 @@ SMB 3.1.1 Negotiate Contexts When the negotiated dialect is **SMB 3.1.1**, the ``SMB2_NEGOTIATE_RESPONSE`` includes the mandatory negotiate context list: -- **SMB2_PREAUTH_INTEGRITY_CAPABILITIES** — SHA-512 integrity algorithm with a +- **SMB2_PREAUTH_INTEGRITY_CAPABILITIES** -- SHA-512 integrity algorithm with a cryptographically random 32-byte salt. -- **SMB2_ENCRYPTION_CAPABILITIES** — echoes the cipher the client advertised +- **SMB2_ENCRYPTION_CAPABILITIES** -- echoes the cipher the client advertised (falls back to AES-128-GCM if the context is absent or unparseable). -- **SMB2_SIGNING_CAPABILITIES** — echoes the signing algorithm the client +- **SMB2_SIGNING_CAPABILITIES** -- echoes the signing algorithm the client advertised (falls back to AES-CMAC). Session Logoff diff --git a/docs/source/examples/kdc.rst b/docs/source/examples/kdc.rst index 1a6777f..c0a0431 100644 --- a/docs/source/examples/kdc.rst +++ b/docs/source/examples/kdc.rst @@ -30,13 +30,13 @@ exchanged securely without exposing passwords over the network. 1. The client requests a Ticket Granting Ticket (TGT) from the Authentication Server (AS) within the Key Distribution Center (KDC) by sending an `AS-REQ `_ message. This request may include a timestamp encrypted with the user's Kerberos key, a process known as `Preauthentication `_. -2. The AS verifies the timestamp (if present) and responds with an `AS-REP `_ message. This response contains two encrypted parts: a TGT encrypted with the KDC’s key and client-specific data encrypted with the client’s key. Key information, such as the session key, is embedded in both parts to ensure shared access between the client and the KDC. +2. The AS verifies the timestamp (if present) and responds with an `AS-REP `_ message. This response contains two encrypted parts: a TGT encrypted with the KDC's key and client-specific data encrypted with the client's key. Key information, such as the session key, is embedded in both parts to ensure shared access between the client and the KDC. 3. When the client attempts to access a service, it negotiates the authentication protocol using SPNEGO. If Kerberos is chosen, the client must obtain a Service Ticket (ST) for the target service. -4. The client sends a `TGS-REQ `_ message to the KDC requesting the ST. This message includes the TGT, the Service Principal Name ([SPN](#service-principal-name-spn)) of the target service, and additional encrypted data (such as the client’s username and timestamp) to verify authenticity. +4. The client sends a `TGS-REQ `_ message to the KDC requesting the ST. This message includes the TGT, the Service Principal Name ([SPN](#service-principal-name-spn)) of the target service, and additional encrypted data (such as the client's username and timestamp) to verify authenticity. -5. The KDC decrypts the TGT using its key, extracts the session key, and verifies the client's username. Upon validation, the KDC issues a `TGS-REP `_ message containing two encrypted sections: an ST encrypted with the service’s key and client-related data encrypted with the session key. Shared data, such as the service session key, is embedded in both sections to facilitate communication between the client and the service. +5. The KDC decrypts the TGT using its key, extracts the session key, and verifies the client's username. Upon validation, the KDC issues a `TGS-REP `_ message containing two encrypted sections: an ST encrypted with the service's key and client-related data encrypted with the session key. Shared data, such as the service session key, is embedded in both sections to facilitate communication between the client and the service. 6. The client forwards the ST to the service within an `AP-REQ `_ message, which is encapsulated in the application protocol. The service decrypts the ST, retrieves the session key, and accesses the Privilege Attribute Certificate (PAC), which contains security information about the client. diff --git a/docs/source/examples/smtp_downgrade.rst b/docs/source/examples/smtp_downgrade.rst index 94aa667..2de2c09 100644 --- a/docs/source/examples/smtp_downgrade.rst +++ b/docs/source/examples/smtp_downgrade.rst @@ -87,7 +87,7 @@ or simulate a failure after NTLM auth to force the client to downgrade. 535 5.7.3 Authentication unsuccessful - The default Windows SMTP client will retry using cleartext credentials — if they are present. + The default Windows SMTP client will retry using cleartext credentials -- if they are present. .. figure:: /_static/images/smtp-downgrade_wireshark.png :align: center diff --git a/docs/source/examples/x11_cookies.rst b/docs/source/examples/x11_cookies.rst index 18c6c61..9902bde 100644 --- a/docs/source/examples/x11_cookies.rst +++ b/docs/source/examples/x11_cookies.rst @@ -5,9 +5,9 @@ Stealing XAuth Cookies The X11 protocol allows graphical applications to display their user interface on a remote system. If access control is enabled (e.g., using ``xhost``), the user must supply valid authentication -credentials—known as *X authorization cookies*—before launching the application. +credentials--known as *X authorization cookies*--before launching the application. -Let’s consider the following scenario: A user wants to run a graphical application locally but have it +Let's consider the following scenario: A user wants to run a graphical application locally but have it display remotely on a server named ``UbuntuSrv``. Since the user doesn't know the IP address, they use the hostname instead: diff --git a/docs/source/tui.rst b/docs/source/tui.rst old mode 100755 new mode 100644 diff --git a/pyproject.toml b/pyproject.toml index 07c9246..a17a9f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,104 +96,104 @@ src = ["dementor", "tests"] select = ["ALL"] ignore = [ # --- A: flake8-builtins --- - "A001", # builtin-variable-shadowing — unavoidable in Sphinx conf.py (copyright) - "A002", # builtin-argument-shadowing — common in protocol code (type, id) + "A001", # builtin-variable-shadowing -- unavoidable in Sphinx conf.py (copyright) + "A002", # builtin-argument-shadowing -- common in protocol code (type, id) - # --- ANN: flake8-annotations — use ty for gradual typing --- + # --- ANN: flake8-annotations -- use ty for gradual typing --- "ANN", # --- ARG: flake8-unused-arguments --- - "ARG001", # unused-function-argument — CLI/callback params required by framework - "ARG002", # unused-method-argument — interface/callback stubs + "ARG001", # unused-function-argument -- CLI/callback params required by framework + "ARG002", # unused-method-argument -- interface/callback stubs # --- ASYNC: flake8-async --- - "ASYNC240", # blocking-path-method-in-async — startup-time checks, not hot path + "ASYNC240", # blocking-path-method-in-async -- startup-time checks, not hot path # --- BLE: flake8-blind-except --- - "BLE001", # blind-except — legitimate in protocol handlers + "BLE001", # blind-except -- legitimate in protocol handlers # --- C90: mccabe --- - "C901", # complex-structure — protocol handlers are inherently complex + "C901", # complex-structure -- protocol handlers are inherently complex # --- COM: flake8-commas --- - "COM812", # missing-trailing-comma — conflicts with formatter + "COM812", # missing-trailing-comma -- conflicts with formatter # --- D: pydocstyle --- "D100", "D101", "D102", "D103", "D104", "D105", "D107", # missing docstrings "D203", # conflicts with D211 "D212", # conflicts with D213 (must pick one) "D213", # conflicts with D212 (must pick one) - "D401", # non-imperative-mood — too opinionated + "D401", # non-imperative-mood -- too opinionated # --- E: pycodestyle --- - "E501", # line-too-long — formatter handles what it can + "E501", # line-too-long -- formatter handles what it can # --- EM: flake8-errmsg --- - "EM101", # raw-string-in-exception — too pedantic - "EM102", # f-string-in-exception — too pedantic + "EM101", # raw-string-in-exception -- too pedantic + "EM102", # f-string-in-exception -- too pedantic # --- ERA: eradicate --- "ERA001", # REVISIT notes, protocol reference tables, placeholders # --- EXE: flake8-executable --- - "EXE002", # shebang-missing-executable-file — not relevant + "EXE002", # shebang-missing-executable-file -- not relevant # --- FBT: flake8-boolean-trap --- "FBT", # too strict for config-heavy protocol code # --- FIX: flake8-fixme --- - "FIX002", # line-contains-todo — TODOs are intentional + "FIX002", # line-contains-todo -- TODOs are intentional # --- G: flake8-logging-format --- - "G004", # logging-f-string — f-strings in logging are fine + "G004", # logging-f-string -- f-strings in logging are fine # --- I: isort --- - "I001", # unsorted-imports — custom import grouping + "I001", # unsorted-imports -- custom import grouping # --- INP: flake8-no-pep420 --- "INP001", # standalone script directories don't need __init__.py # --- ISC: flake8-implicit-str-concat --- - "ISC003", # explicit-string-concatenation — often more readable + "ISC003", # explicit-string-concatenation -- often more readable # --- N: pep8-naming --- "N801", "N802", "N803", "N806", # protocol code uses non-standard names "N811", "N812", "N815", "N817", # import/variable naming conventions - "N818", # error-suffix — control-flow exceptions are idiomatic + "N818", # error-suffix -- control-flow exceptions are idiomatic # --- PL: Pylint --- - "PLR0912", # too-many-branches — protocol handlers - "PLR0913", # too-many-arguments — protocol constructors - "PLR0915", # too-many-statements — protocol handlers - "PLR2004", # magic-value-comparison — protocol numeric constants + "PLR0912", # too-many-branches -- protocol handlers + "PLR0913", # too-many-arguments -- protocol constructors + "PLR0915", # too-many-statements -- protocol handlers + "PLR2004", # magic-value-comparison -- protocol numeric constants # --- PTH: flake8-use-pathlib --- - "PTH", # os.path → pathlib — low priority + "PTH", # os.path -> pathlib -- low priority # --- RET: flake8-return --- - "RET503", # implicit-return — idiomatic Python + "RET503", # implicit-return -- idiomatic Python # --- RUF: Ruff-specific rules --- - "RUF003", # ambiguous-unicode-character-comment — intentional - "RUF012", # mutable-class-default — false positives with struct-like patterns + "RUF003", # ambiguous-unicode-character-comment -- intentional + "RUF012", # mutable-class-default -- false positives with struct-like patterns # --- S: flake8-bandit --- - "S101", # assert — valid in non-production paths - "S104", # hardcoded-bind-all-interfaces — intentional for network tool - "S311", # suspicious-non-cryptographic-random — not used for crypto + "S101", # assert -- valid in non-production paths + "S104", # hardcoded-bind-all-interfaces -- intentional for network tool + "S311", # suspicious-non-cryptographic-random -- not used for crypto # --- SLF: flake8-self --- - "SLF001", # private-member-access — common in internal wiring + "SLF001", # private-member-access -- common in internal wiring # --- T20: flake8-print --- - "T201", # print — intentional in CLI/diagnostic code + "T201", # print -- intentional in CLI/diagnostic code # --- TD: flake8-todos --- - "TD002", # missing-todo-author — not needed for small team - "TD003", # missing-todo-link — not needed for small team + "TD002", # missing-todo-author -- not needed for small team + "TD003", # missing-todo-link -- not needed for small team # --- TRY: tryceratops --- - "TRY003", # raise-vanilla-args — too pedantic + "TRY003", # raise-vanilla-args -- too pedantic ] [tool.ruff.format] diff --git a/requirements.txt b/requirements.txt old mode 100755 new mode 100644 diff --git a/tests/.gitkeep b/tests/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_db.py b/tests/test_db.py index eb0f22b..b9b8dcf 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -155,7 +155,7 @@ def test_only_prefix_itself(self) -> None: # =================================================================== -# connector.py — DatabaseConfig +# connector.py -- DatabaseConfig # =================================================================== class TestDatabaseConfig: def test_default_fields_from_empty_config(self) -> None: @@ -195,7 +195,7 @@ def test_section_name(self) -> None: # =================================================================== -# connector.py — init_engine +# connector.py -- init_engine # =================================================================== class TestInitEngine: def _make_session( @@ -269,7 +269,7 @@ def test_url_without_driver(self) -> None: # =================================================================== -# connector.py — create_db +# connector.py -- create_db # =================================================================== class TestCreateDb: def test_returns_dementor_db(self) -> None: @@ -291,7 +291,7 @@ def test_raises_on_engine_failure(self) -> None: # =================================================================== -# model.py — DementorDB init / lifecycle +# model.py -- DementorDB init / lifecycle # =================================================================== class TestDementorDBInit: def test_creates_all_three_tables(self, engine, config) -> None: @@ -393,7 +393,7 @@ def test_session_works_after_release(self, db) -> None: # =================================================================== -# model.py — add_host +# model.py -- add_host # =================================================================== class TestAddHost: def test_creates_new_host(self, db) -> None: @@ -484,7 +484,7 @@ def test_different_ips_create_different_hosts(self, db) -> None: # =================================================================== -# model.py — add_host_extra +# model.py -- add_host_extra # =================================================================== class TestAddHostExtra: def test_creates_new_extra(self, db) -> None: @@ -555,7 +555,7 @@ def test_value_stored_as_string(self, db) -> None: # =================================================================== -# model.py — add_auth: basic storage +# model.py -- add_auth: basic storage # =================================================================== class TestAddAuth: def test_stores_all_fields(self, db, logger) -> None: @@ -732,7 +732,7 @@ def test_credential_fk_links_to_host(self, db, logger) -> None: # =================================================================== -# model.py — add_auth: protocol resolution +# model.py -- add_auth: protocol resolution # =================================================================== class TestAddAuthProtocol: def test_from_logger_extra(self, db) -> None: @@ -789,7 +789,7 @@ def test_no_protocol_no_logger_skips(self, db) -> None: # =================================================================== -# model.py — add_auth: duplicate detection +# model.py -- add_auth: duplicate detection # =================================================================== class TestDuplicateDetection: def test_dedup_skips_second(self, db, logger) -> None: @@ -979,7 +979,7 @@ def test_same_ip_different_port_still_deduped(self, db, logger) -> None: # =================================================================== -# model.py — add_auth: HOST_INFO extras +# model.py -- add_auth: HOST_INFO extras # =================================================================== class TestAddAuthExtras: def test_host_info_popped(self, db, logger) -> None: @@ -1023,7 +1023,7 @@ def test_empty_extras(self, db, logger) -> None: # =================================================================== -# model.py — add_auth: logging +# model.py -- add_auth: logging # =================================================================== class TestAddAuthLogging: def test_success_logs_captured(self, db, logger) -> None: @@ -1139,7 +1139,7 @@ def test_no_log_on_failed_write(self, db, logger) -> None: # =================================================================== -# model.py — _check_duplicate +# model.py -- _check_duplicate # =================================================================== class TestCheckDuplicate: def test_false_on_empty_db(self, db) -> None: @@ -1190,7 +1190,7 @@ def test_different_domain_returns_false(self, db, logger) -> None: # =================================================================== -# model.py — error handling +# model.py -- error handling # =================================================================== class TestErrorHandling: def test_handle_db_error_reraises_unknown(self, db) -> None: @@ -1219,7 +1219,7 @@ def test_commit_succeeds(self, db) -> None: # =================================================================== -# model.py — connection release +# model.py -- connection release # =================================================================== class TestConnectionRelease: def test_add_host_releases(self, db) -> None: @@ -1261,7 +1261,7 @@ def test_sequential_operations_work(self, db, logger) -> None: # =================================================================== -# model.py — thread safety +# model.py -- thread safety # =================================================================== class TestThreadSafety: def test_concurrent_add_host_same_ip(self, db) -> None: