Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8d5f302
File named hostcheck.py is added to check whether target is buggy or…
Aarush289 Oct 24, 2025
aa994c5
Update app.py
Aarush289 Oct 24, 2025
ed7c81e
Update hostcheck.py , removed unnecessary logs
Aarush289 Oct 24, 2025
ccd870e
Updated hostcheck.py to use allow_single_label and return the lower-c…
Aarush289 Oct 24, 2025
b5da801
Docstring added
Aarush289 Oct 24, 2025
c1a9201
app.py updated to remove noise in exception handling
Aarush289 Oct 24, 2025
b66a598
Multi-threading issue is resolved
Aarush289 Oct 24, 2025
949c911
chore: test signed commit (SSH)
Aarush289 Oct 24, 2025
50374b9
chore: test signed commit (SSH) #2
Aarush289 Oct 24, 2025
2389890
chore: test signed commit (SSH) #3
Aarush289 Oct 24, 2025
859b850
Merge branch 'master' into feature/my-change
Aarush289 Oct 25, 2025
a83c17f
Removed unnecessary print statements
Aarush289 Oct 25, 2025
d852609
Indentation done
Aarush289 Oct 25, 2025
4de4cca
trim dot before checking the length
Aarush289 Oct 25, 2025
c472e71
Update hostcheck.py
Aarush289 Oct 25, 2025
fb43add
Logging exceptions for better debugging
Aarush289 Oct 25, 2025
8102395
Hostcheck.py OS-independent; add validate_before_scan
Aarush289 Oct 27, 2025
cd4e5ab
Hostchecker is made OS independent and validate_before_scan is adde…
Aarush289 Oct 27, 2025
c92c7f3
Update hostcheck.py to make it OS independent
Aarush289 Oct 27, 2025
8752881
Indentation done
Aarush289 Oct 27, 2025
bd762cd
removed the duplicate key
Aarush289 Oct 27, 2025
c23507e
unused parameter removed
Aarush289 Oct 27, 2025
7de608e
"Fix import order (ruff E402), isort formatting; run pre-commit"
Aarush289 Oct 27, 2025
87b773c
Per-pass timeout added
Aarush289 Oct 27, 2025
0ac3a96
Deadline removed
Aarush289 Oct 27, 2025
8565db6
Indentation done
Aarush289 Oct 27, 2025
ec97266
Suggested changes are done
Aarush289 Oct 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 63 additions & 3 deletions nettacker/core/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from threading import Thread

import multiprocess

from concurrent.futures import ThreadPoolExecutor, as_completed
from nettacker import logger
from nettacker.config import Config, version_info
from nettacker.core.arg_parser import ArgParser
Expand All @@ -23,6 +23,7 @@
is_ipv6_range,
is_ipv6_cidr,
)
from nettacker.core.hostcheck import resolve_quick, is_ip_literal, valid_hostname
from nettacker.core.messages import messages as _
from nettacker.core.module import Module
from nettacker.core.socks_proxy import set_socks_proxy
Expand Down Expand Up @@ -142,7 +143,7 @@ def expand_targets(self, scan_id):
):
targets += generate_ip_range(target)
# domains probably
else:
else:
targets.append(target)
self.arguments.targets = targets
self.arguments.url_base_path = base_path
Expand Down Expand Up @@ -220,7 +221,6 @@ def run(self):
if self.arguments.scan_compare_id is not None:
create_compare_report(self.arguments, scan_id)
log.info("ScanID: {0} ".format(scan_id) + _("done"))

return exit_code

def start_scan(self, scan_id):
Expand Down Expand Up @@ -289,7 +289,67 @@ def scan_target(

return os.EX_OK


def filter_valid_targets(self, targets, timeout_per_target=2.0, max_workers=None, dedupe=True):
"""
Parallel validation of targets via resolve_quick(target, timeout_sec).
Returns a list of canonical targets (order preserved, invalids removed).
"""
# Ensure it's a concrete list (len, indexing OK)
try:
targets = list(targets)
except TypeError:
raise TypeError(f"`targets` must be iterable, got {type(targets).__name__}")

if not targets:
return []

if max_workers is None:
max_workers = min(len(targets), 64) # cap threads

# Preserve order
canon_by_index = [None] * len(targets)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The names can be clearer


def _task(idx, t):
ok, canon = resolve_quick(t, timeout_sec=timeout_per_target)
return idx, t, (canon if ok and canon else None)

with ThreadPoolExecutor(max_workers=max_workers) as ex:
futures = [ex.submit(_task, i, t) for i, t in enumerate(targets)]
for fut in as_completed(futures):
try:
idx, orig_target, canon = fut.result()
except Exception as e:
# Treat as invalid on error; log with the best context we have
log.info(f"Invalid target (exception): {e!s}")
continue

if canon:
canon_by_index[idx] = canon
else:
log.info(f"Invalid target -> dropping: {orig_target}")

# Keep order, drop Nones
filtered = [c for c in canon_by_index if c is not None]

if dedupe:
seen, unique = set(), []
for c in filtered:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider better names, if iterator is not being used elsewhere then just use an "_"

if c not in seen:
seen.add(c)
unique.append(c)
return unique
return filtered

def scan_target_group(self, targets, scan_id, process_number):

if(not self.arguments.socks_proxy):
targets = self.filter_valid_targets(
targets,
timeout_per_target=2.0,
max_workers=self.arguments.parallel_module_scan or None,
dedupe=True,
)
active_threads = []
log.verbose_event_info(_("single_process_started").format(process_number))
total_number_of_modules = len(targets) * len(self.arguments.selected_modules)
Expand Down
122 changes: 122 additions & 0 deletions nettacker/core/hostcheck.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# nettacker/core/hostcheck.py
from __future__ import annotations
import re
import socket
import time
import concurrent.futures
import os
import sys


_LABEL = re.compile(r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)$")

def is_ip_literal(name: str) -> bool:
try:
socket.inet_pton(socket.AF_INET, name); return True
except OSError:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exceptions are slower than if-else statements. You're better off with precompiling an IPv4 regex (IPv6 would be too complex) at least and not relying on a try-except block as part of the main execution flow.

pass
try:
socket.inet_pton(socket.AF_INET6, name); return True
except OSError:
return False

def valid_hostname(host: str, allow_single_label: bool = False) -> bool:
if len(host) > 253:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do add a comment here to specify this comes from RFC1123 which specifies the number of characters to be 250 at max (without dots) and 253 with dots.

return False
if host.endswith("."):
host = host[:-1]
parts = host.split(".")
if len(parts) < 2 and not allow_single_label:
# log.warn("Its a name like google")
print("itegb")
return False
return all(_LABEL.match(p) for p in parts)

def _system_search_suffixes() -> list[str]:
# Only used when host has no dots; mirrors OS resolver search behavior (UNIX).
sufs: list[str] = []
try:
with open("/etc/resolv.conf") as f:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Aarush289 using /etc/resolv.conf here is not acceptable. This will not work on Windows and other non-Linux platforms and we have just spent lots of effort making Nettacker platform-independent

for line in f:
line = line.strip()
if not line or line.startswith("#"): continue
if line.startswith("search") or line.startswith("domain"):
sufs += [x for x in line.split()[1:] if x]
except Exception:
pass
seen = set(); out: list[str] = []
for s in sufs:
if s not in seen:
seen.add(s); out.append(s)
return out

# --- safer, more robust pieces to replace in hostcheck.py ---

def _gai_once(name: str, use_ai_addrconfig: bool, port):
flags = getattr(socket, "AI_ADDRCONFIG", 0) if use_ai_addrconfig else 0
return socket.getaddrinfo(
name, port, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, flags
)

def resolve_quick(
host: str,
timeout_sec: float = 2.0,
try_search_suffixes: bool = True,
allow_single_label: bool = True
) -> tuple[bool, str | None]:

candidates: list[str] = []
if "." in host:
# try both plain and absolute forms; whichever resolves first wins
if host.endswith("."):
candidates.extend([host, host[:-1]])
else:
candidates.extend([host, host + "."])
else:
# single label (e.g., "intranet")
if not allow_single_label:
return False, None
if try_search_suffixes:
for s in _system_search_suffixes():
candidates.extend([f"{host}.{s}", f"{host}.{s}."])
if not host.endswith("."):
candidates.append(host + ".") # bare absolute
candidates.append(host)

seen, uniq = set(), []
for c in candidates:
if c not in seen:
seen.add(c); uniq.append(c)
candidates = uniq
if not candidates:
return False, None

for pass_ix, (use_ai_addrconfig, port) in enumerate(((True, None), (False, None))):
deadline = time.monotonic() + timeout_sec
with concurrent.futures.ThreadPoolExecutor(max_workers=len(candidates)) as ex:
fut2cand = {ex.submit(_gai_once, c, use_ai_addrconfig, port): c for c in candidates}
pending = set(fut2cand)
while pending:
remaining = deadline - time.monotonic()
if remaining <= 0:
break
done, pending = concurrent.futures.wait(
pending, timeout=remaining,
return_when=concurrent.futures.FIRST_COMPLETED
)
for fut in done:
try:
res = fut.result()
if not res:
# treat as failure
raise RuntimeError("empty getaddrinfo result")
chosen = fut2cand[fut]
# cancel stragglers
for p in pending: p.cancel()
canon = chosen[:-1] if chosen.endswith(".") else chosen
return True, canon.lower()
except Exception as e:
continue
# cancel any survivors in this pass
for f in fut2cand: f.cancel()
return False, None
Loading