Skip to content

Commit 989209d

Browse files
n0connectclaude
andcommitted
Themed UI, DRY consolidation, and a second correctness-hardening pass (1.3.0)
Adds a centralized theme.py driving level-colored console logging (with a new FOUND level replacing the old "[+]" convention), a themed block-style banner, boxed run summaries, a TqdmLoggingHandler so logs stop corrupting the live progress bar, and a SIP-ENUM pre-check that warns when a target looks like it's blanket-rejecting every request (e.g. modern PJSIP). Consolidates wordlist/ip-list reading, the bulk-confirmation prompt, and IP-octet validation into single shared implementations across nes.py/enum.py/das.py, caches .message template reads, and fixes a second round of real bugs found via a from-scratch full-source audit: a printResult crash on malformed SIP responses, a --tc=0 hang, an ENUM single-username off-by-one, an implicit logger.found() import-order dependency, DAS's -m/-s validation gaps and promiscuous-mode restoration on unexpected exceptions, and (for real this time) the tty.setraw()/setcbreak() y/n-prompt bug that an earlier amend of this same commit had silently reintroduced after it was already fixed and pushed. 122 tests, ruff clean. See CHANGELOG.md for the full breakdown. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent fe31d1a commit 989209d

19 files changed

Lines changed: 1012 additions & 240 deletions

CHANGELOG.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,60 @@
11
# Changelog
22

3+
## 1.3.0 — Themed UI, DRY consolidation, and a second correctness-hardening pass
4+
5+
No CLI behavior is removed and no existing flag changes meaning; a script that worked against `1.2.0` still works. This pass focused on three things: a real terminal UI layer (colors driven by log level instead of hardcoded per call site), removing every remaining case of the same logic implemented two different ways across `nes.py`/`enum.py`/`das.py`, and a from-scratch re-audit of the whole `src/` tree that found and fixed a further round of real bugs beyond what `1.2.0`'s pass covered.
6+
7+
### New capability
8+
9+
- **Themed console output.** `src/core/theme.py` is now the single source of every ANSI color/style used anywhere in the tool. Console log lines get an automatic `[ DEBUG ]`/`[ INFO ]`/`[ FOUND ]`/`[ WARN ]`/`[ ERROR ]` tag, colored by level via a new `logging_config.ColorFormatter` — replacing the old convention of hardcoding escape codes and ad-hoc `[!]`/`[+]` string prefixes inline at each call site. A new custom `FOUND` log level (between `INFO` and `WARNING`) replaces the old `[+]` marker for "live host / valid extension found" results. Color auto-disables when `NO_COLOR` is set or stdout isn't a real terminal.
10+
- **New block-style startup banner**, colored through `theme.py`'s constants instead of raw escape codes baked into the banner string.
11+
- **Boxed run summaries.** Each module's final result line is now rendered as a bordered panel (`theme.panel()`) instead of a single unstyled log line.
12+
- **`TqdmLoggingHandler`** (`logging_config.py`) routes console logs through `tqdm.write()` instead of writing directly to stdout, so log lines during a scan no longer corrupt the live progress bar's line.
13+
- **SIP-ENUM blanket-rejection pre-check.** Before enumerating, ENUM now sends one probe per target with a random, guaranteed-nonexistent username; if the target responds 401/403 to it, ENUM warns that the server may be blanket-rejecting everything (e.g. modern PJSIP's default behavior — see F6 below), so the operator knows to treat results with suspicion instead of trusting a silent false-positive.
14+
15+
### Architecture / DRY consolidation
16+
17+
Per-module logic that had drifted into 2–4 different implementations of the same operation was consolidated to one:
18+
19+
- `net_utils.read_lines()` / `net_utils.read_ip_list()` replace four divergent wordlist/`ip_list.txt`-reading implementations previously spread across `nes.py`, `enum.py`, and `das.py` (one of which silently skipped the `isalnum` filter the others applied).
20+
- `threadpool.confirm_bulk_run()` replaces near-identical copy-pasted "N packages will be generated, continue?" prompt-building code in `nes.py` and `enum.py`.
21+
- `net_utils._validate_dotted_quad()` replaces three separate copies of octet-range validation inside `check_ip_address()` (two of which were missing the `ValueError` guard the third had).
22+
- `net_utils.printResult()` now calls `decimal_to_octets()` instead of re-implementing the same int→dotted-IP conversion inline.
23+
- `nes.py`'s three copy-pasted target×user cross-product comprehensions were merged into one, computed once after `target_networks` is determined.
24+
- `net_utils.defineTargetType()` now calls `read_lines()` instead of manually re-implementing its split/filter logic (with a subtly different, unintended behavior — see Correctness fixes).
25+
- Dead `net_utils.readFile()` removed (its only caller was consolidated away).
26+
- New `net_utils.positive_int()` argparse validator for `--tc`/`--thread-count`.
27+
- `sip_packet._read_template()` caches `.message` template file reads (`functools.cache`) instead of re-opening the same file from disk on every `generate_packet()` call — the same class of redundant I/O already fixed for wordlists/`servers.txt` in `1.2.0` (F8/F16), just missed for the templates themselves. Matters most in DAS's flood loop, which calls this up to ~1e8 times at the default counter.
28+
29+
### Correctness fixes
30+
31+
- **The y/n confirmation prompt's `setraw`/`setcbreak` bug, actually fixed this time.** `1.2.0`'s changelog and the prior commit's message both claimed a switch from `tty.setraw()` to `tty.setcbreak()` to fix doubled/misaligned keypress output — but the code was never actually changed. `src/core/threadpool.py`'s `_read_single_char()` now genuinely calls `tty.setcbreak()`; verified live over a real controlling terminal (`setsid`/`TIOCSCTTY`) that a single `n` keypress renders correctly with no doubling.
32+
- **`net_utils.printResult()` no longer crashes on a malformed or incomplete SIP response.** `sip_packet.getResponse()` can return `{}` (unparseable status line), fall through to an implicit `None` (no headers past the status line), or store `None` for a header line with no `:`. `printResult()` used to index straight into `result["response"]["headers"]` and then `list(value)`, crashing with `KeyError`/`TypeError` on any of these — reachable from a single malformed reply during a live scan. Now guarded at every level.
33+
- **`--tc=0` (or negative) no longer hangs the tool forever.** `run_worker_pool()` starts `range(thread_count)` worker threads; a count below 1 meant no thread ever existed to drain the queue, so the run spun with no error message until manually killed. `--tc` now goes through the new `positive_int` validator.
34+
- **SIP-ENUM's single-username-wordlist off-by-one.** `if len(user_list) <= 1` rejected a wordlist containing exactly one valid entry as if it were empty, blocking the legitimate "enumerate exactly one extension" case. Now `if not user_list`.
35+
- **`logger.found()` no longer depends on import order.** The custom `FOUND` level is registered as an import-time side effect of `logging_config.py`; `net_utils.py` (which calls `logger.found()` in `printResult()`) now imports `logging_config` itself, so this works regardless of whether `cli.py` has already run first.
36+
- **`check_ip_address()`'s range and plain-IP branches now reject a non-numeric octet the same clean way the CIDR branch already did**, via the new shared `_validate_dotted_quad()` helper — previously they had no guard around `int(number)` (argparse's own exception handling happened to still produce a usable, if generic, error message, so this was never a raw crash, just an inconsistent one).
37+
- **`nes.py`'s file-vs-literal-value detection for `--from`/`--to` switched from a fragile `"txt" in value` substring check to `os.path.isfile(value)`.** The old check misclassified a literal username containing "txt" as a file, and a wordlist path without a literal `.txt` in it as a literal value.
38+
- **`net_utils.defineTargetType()`'s server-list matching now strips whitespace before filtering**, via the `read_lines()` consolidation above — the old manual re-implementation could silently drop a legitimate `servers.txt` entry with trailing whitespace or a stray `\r`.
39+
- **DAS: `-m` without `--il` now raises a clean error instead of a raw `TypeError`.** `args.manual_ip_list` defaults to `None`; `net_utils.read_ip_list(None)` used to call `open(None)` directly.
40+
- **DAS: `-s`/`--subnet` without a valid interface netmask now raises a clean error instead of crashing mid-flood.** A `client_netmask` of `None` used to reach `randomIPAddressFromNetwork()` and fail deep inside the send loop.
41+
- **DAS: promiscuous mode is now guaranteed to be turned back off**, via `try/finally`. It previously only ran on the `KeyboardInterrupt` path and after normal completion — any other exception (e.g. an empty `--il` list making `random.choice` raise `IndexError`) left the interface stuck in promiscuous mode.
42+
43+
### Performance fixes
44+
45+
- **`das.py`'s flood loop no longer recomputes a Scapy routing-table lookup (`IP(dst=...).src`) on every iteration.** `args.target_network` is invariant across the loop, and the value was discarded immediately whenever `-r`/`-s`/`-m` overrode it anyway — now computed once, up front.
46+
- **`run_worker_pool()` simplified back down to one completion-tracking mechanism.** An earlier iteration of this pass had it running `queue.join()` alongside a *second*, separate daemon thread polling `unfinished_tasks` every 50ms just to drive the progress bar — more machinery than needed, and the timeout-bounded teardown of that second thread had a small theoretical race with `pbar.close()`. Each worker now updates the (lock-guarded) progress bar itself right after finishing its own item; `queue.join()` alone decides when the function returns.
47+
48+
### Testing
49+
50+
~50 new pytest cases (`test_theme.py`, `test_logging_config.py`, `test_enum.py` are new files; `test_net_utils.py`, `test_threadpool.py`, `test_das.py`, `test_sip_packet.py` gained new cases), including regression tests for the `setcbreak` fix, the `printResult`/`check_ip_address`/`positive_int`/template-caching fixes above, and the blanket-rejection pre-check. 122 tests total; `ruff check src/ tests/ mr.sip.py` clean.
51+
52+
### Known observation, not fixed
53+
54+
SIP-ENUM's new blanket-rejection pre-check runs one synchronous probe per target *before* the bulk-confirmation prompt, unthreaded. For a single `--tn` target this is one extra packet; for a large `output/ip_list.txt` (e.g. a full `/24` from a prior SIP-NES run), it adds up to one sequential round-trip per host before the operator is even asked to confirm the run. Not incorrect, just a potential wait on large host lists - left as-is pending a decision on whether to thread it through `run_worker_pool()` or move it after confirmation.
55+
56+
## 1.2.0
57+
358
Technical summary of the modular refactor and correctness/security hardening pass on top of upstream Mr.SIP `v1.1.0`. Baseline for comparison is commit `bdd98ad` ("Update OffzoneMoscow2019badge.svg") — the last commit before this work started.
459

560
No CLI behavior is removed and no existing flag changes meaning. Two new flags were added (`--mtu`, `--version`). Everything else is a bug fix, a hardening change, or an internal restructure; a script that worked before still works, generally with more accurate output.

src/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.2.0"
1+
__version__ = "1.3.0"

src/cli.py

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,23 @@
66
from scapy.all import conf
77

88
from src import __version__
9-
from src.core import errors, logging_config, net_utils
9+
from src.core import errors, logging_config, net_utils, theme
1010
from src.modules import das, enum, nes
1111

12-
BANNER = """
13-
__ __ ____ ___ ____ ____ ___ ____ _ _
14-
| \\/ |_ __/ ___|_ _| _ \\ _ / ___|_ _| _ \\ | |__ __ _ ___ ___ __| |
15-
| |\\/| | '__\\___ \\| || |_) (_) \\___ \\| || |_) |____| '_ \\ / _` / __|/ _ \\/ _` |
16-
| | | | | _ ___) | || __/ _ ___) | || __/_____| |_) | (_| \\__ \\ __/ (_| |
17-
|_| |_|_|(_)____/___|_| (_) |____/___|_| |_.__/ \\__,_|___/\\___|\\__,_|
18-
19-
_ _ _ _ _ _ _ _ _
20-
/ \\ _ _ __| (_) |_ __ _ _ __ __| | / \\ | |_| |_ __ _ ___| | __
21-
/ _ \\| | | |/ _` | | __| / _` | '_ \\ / _` | / _ \\| __| __/ _` |/ __| |/ /
22-
/ ___ \\ |_| | (_| | | |_ | (_| | | | | (_| | / ___ \\ |_| || (_| | (__| <
23-
/_/ \\_\\__,_|\\__,_|_|\\__| \\__,_|_| |_|\\__,_| /_/ \\_\\__|\\__\\__,_|\\___|_|\\_\\
24-
25-
_____ _
26-
|_ _|__ ___ | |
27-
| |/ _ \\ / _ \\| |
28-
| | (_) | (_) | |
29-
|_|\\___/ \\___/|_|+ \033[1m\033[91m ~ By Melih Tas (SN)\n\033[0m
30-
""" + "Greetz ~ \033[1m\033[94m Caner \033[1m\033[93m Onur \033[1m\033[95m Neslisah \n\033[0m" \
31-
+ " Maintainer ~ \033[0;32mHakki Riza Kucuk\n\033[0m"
32-
33-
NES_HELP = "SIP-NES is a network scanner. It needs the IP range or IP subnet information as input. It sends SIP OPTIONS message to each IP addresses in the subnet/range and according to the responses, it provides the output of the potential SIP clients and servers on that subnet."
12+
BANNER = r"""
13+
███╗ ███╗██████╗ ███████╗██╗██████╗
14+
████╗ ████║██╔══██╗ ██╔════╝██║██╔══██╗
15+
██╔████╔██║██████╔╝ ███████╗██║██████╔╝
16+
██║╚██╔╝██║██╔══██╗ ╚════██║██║██╔═══╝
17+
██║ ╚═╝ ██║██║ ██║██╗███████║██║██║
18+
╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚══════╝╚═╝╚═╝
19+
""" + theme.BOLD + theme.ACCENT + " SIP Based Attack And Audit Tool\n" + theme.RESET \
20+
+ " " + theme.BOLD + theme.CREDIT_RED + "By Melih Tas (SN)\n" + theme.RESET \
21+
+ " Greetz ~ " + theme.BOLD + theme.CREDIT_BLUE + " Caner " + theme.BOLD + theme.CREDIT_YELLOW + " Onur " \
22+
+ theme.BOLD + theme.CREDIT_MAGENTA + " Neslisah \n" + theme.RESET \
23+
+ " Maintainer ~ " + theme.SUCCESS + "Hakki Riza Kucuk\n" + theme.RESET
24+
25+
NES_HELP = "SIP-NES is a network scanner. It needs the IP range or IP subnet information as input. It sends SIP OPTIONS message to each IP address in the subnet/range and according to the responses, it provides the output of the potential SIP clients and servers on that subnet."
3426
ENUM_HELP = "SIP-ENUM is an enumerator. It needs the output of SIP-NES and also pre-defined SIP usernames. It generates SIP REGISTER messages and sends them to all SIP components and tries to find the valid SIP users on the target network. You can write the output in a file."
3527
DAS_HELP = "SIP-DAS is a DoS/DDoS attack simulator. It comprises four components: powerful spoofed IP address generator, SIP message generator, message sender and response parser. It needs the outputs of SIP-NES and SIP-ENUM along with some pre-defined files."
3628

@@ -47,7 +39,7 @@
4739
ENUM_USAGE = """python3 mr.sip.py --enum --from=output/from.txt
4840
python3 mr.sip.py --enum --tn=<target_IP> --from=output/from.txt
4941
"""
50-
DAS_USAGE = """python3 mr.sip.py --das -mt=invite -c <package_count> --tn=<target_IP> -r
42+
DAS_USAGE = """python3 mr.sip.py --das --mt=invite -c <package_count> --tn=<target_IP> -r
5143
python3 mr.sip.py --das --mt=invite -c <package_count> --tn=<target_IP> -s
5244
python3 mr.sip.py --das --mt=invite -c <package_count> --tn=<target_IP> -m --il=output/ip_list.txt
5345
"""
@@ -76,7 +68,7 @@ def build_parser() -> ArgumentParser:
7668
group.add_argument("--ua", "--user-agent", dest="user_agent", default=str(net_utils.WORDLISTS_DIR / "userAgent.txt"), help="User Agent list file. Default is the bundled userAgent.txt.")
7769
group.add_argument("--il", "--manual-ip-list", dest="manual_ip_list", help="IP list file.")
7870
group.add_argument("--if", "--interface", dest="interface", help="Interface to work on.")
79-
group.add_argument("--tc", "--thread-count", dest="thread_count", type=int, default=10, help="Number of threads running.")
71+
group.add_argument("--tc", "--thread-count", dest="thread_count", type=net_utils.positive_int, default=10, help="Number of threads running (minimum 1). Default is 10.")
8072
group.add_argument("--mtu", dest="mtu", type=int, help="MTU size for packet fragmentation (Scapy mode only).")
8173

8274
group.add_argument("-v", "--verbose", action="store_true", dest="verbose", default=False, help="Enable verbose (DEBUG-level) logging.")
@@ -94,7 +86,7 @@ def main():
9486
args = build_parser().parse_args()
9587
logger = logging_config.setup_logging(args.verbose)
9688

97-
print(BANNER)
89+
print(BANNER if theme.supports_color() else theme.strip_ansi(BANNER))
9890
if args.interface is not None:
9991
conf.iface = args.interface
10092

src/core/errors.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
class MrSipError(Exception):
2-
"""Base class for all Mr.SIP errors. Caught once, centrally, in src.cli.main()."""
2+
"""Base class for all Mr.SIP errors.
3+
4+
Setup/validation failures - TemplateNotFoundError, InvalidInterfaceError,
5+
and plain MrSipError raised by net_utils.check_value_errors() - are meant
6+
to abort the whole run and are caught once, centrally, in src.cli.main().
7+
8+
PacketSendError is deliberately different: nes.py/enum.py/das.py each
9+
catch it locally, per work item, so one failed probe or dropped packet
10+
doesn't abort an entire scan/flood. It isn't expected to reach
11+
cli.main()'s handler in normal operation.
12+
"""
313

414

515
class PacketSendError(MrSipError):
6-
"""Raised when a SIP packet could not be sent or no response was received in time."""
16+
"""Raised when a SIP packet could not be sent or no response was received
17+
in time. Caught locally per work item by nes.py/enum.py/das.py, not by
18+
cli.main() - see MrSipError's docstring."""
719

820

921
class TemplateNotFoundError(MrSipError):

src/core/logging_config.py

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,54 @@
11
import logging
22
import os
3-
import re
43
import sys
54
from datetime import datetime
65

6+
from src.core import theme
7+
78
CONSOLE_FORMAT = "%(message)s"
89
FILE_FORMAT = "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
9-
_ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-9;]*m")
10+
11+
# Custom level for "a live target / valid extension was found" results.
12+
# Sits between INFO and WARNING so it's a real, filterable log level instead
13+
# of the old ad-hoc "[+]" string prefix buried inside .info() messages.
14+
FOUND = 25
15+
logging.addLevelName(FOUND, "FOUND")
16+
17+
18+
def _found(self, message, *args, **kwargs):
19+
if self.isEnabledFor(FOUND):
20+
self._log(FOUND, message, args, **kwargs)
21+
22+
23+
logging.Logger.found = _found
24+
25+
# levelno -> (short tag, color). Tags are padded to equal width in
26+
# ColorFormatter so console output stays aligned regardless of level.
27+
_LEVEL_STYLE = {
28+
logging.DEBUG: ("DEBUG", theme.MUTED),
29+
logging.INFO: ("INFO", theme.ACCENT),
30+
FOUND: ("FOUND", theme.SUCCESS),
31+
logging.WARNING: ("WARN", theme.WARNING),
32+
logging.ERROR: ("ERROR", theme.ERROR),
33+
logging.CRITICAL: ("CRIT", theme.CRITICAL),
34+
}
35+
36+
37+
class ColorFormatter(logging.Formatter):
38+
"""Prefixes every console line with a colored '[ LEVEL ]' tag.
39+
40+
Replaces the old convention of hardcoding ANSI color codes and ad-hoc
41+
"[!]"/"[+]" markers inline at each call site - callers now just log at
42+
the right level (.debug/.info/.found/.warning/.error) and this formatter
43+
is the single place that decides how that looks. Auto-disabled via
44+
theme.supports_color() (NO_COLOR, non-tty output).
45+
"""
46+
47+
def format(self, record):
48+
message = super().format(record)
49+
tag, color = _LEVEL_STYLE.get(record.levelno, (record.levelname, theme.ACCENT))
50+
prefix = theme.colorize(f"[ {tag:<5} ]", color)
51+
return "\n".join(f"{prefix} {line}" for line in message.split("\n"))
1052

1153

1254
class StripAnsiFormatter(logging.Formatter):
@@ -16,17 +58,30 @@ class StripAnsiFormatter(logging.Formatter):
1658

1759
def format(self, record):
1860
message = super().format(record)
19-
return _ANSI_ESCAPE_RE.sub("", message)
61+
return theme.strip_ansi(message)
62+
63+
64+
class TqdmLoggingHandler(logging.Handler):
65+
"""A logging handler that writes logs via tqdm.write to prevent progress bar corruption."""
66+
67+
def emit(self, record):
68+
try:
69+
msg = self.format(record)
70+
from tqdm import tqdm
71+
tqdm.write(msg, file=sys.stdout)
72+
self.flush()
73+
except Exception:
74+
self.handleError(record)
2075

2176

2277
def setup_logging(verbose: bool, log_dir: str = "logs") -> logging.Logger:
2378
logger = logging.getLogger()
2479
logger.setLevel(logging.DEBUG)
2580
logger.handlers.clear()
2681

27-
console_handler = logging.StreamHandler(sys.stdout)
82+
console_handler = TqdmLoggingHandler()
2883
console_handler.setLevel(logging.DEBUG if verbose else logging.INFO)
29-
console_handler.setFormatter(logging.Formatter(CONSOLE_FORMAT))
84+
console_handler.setFormatter(ColorFormatter(CONSOLE_FORMAT))
3085
logger.addHandler(console_handler)
3186

3287
os.makedirs(log_dir, exist_ok=True)

0 commit comments

Comments
 (0)