Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
cd331b4
File named hostcheck.py is added to check whether target is buggy or…
Aarush289 Oct 24, 2025
57b196c
Update app.py
Aarush289 Oct 24, 2025
8d4c91e
Update hostcheck.py , removed unnecessary logs
Aarush289 Oct 24, 2025
6200bb5
Updated hostcheck.py to use allow_single_label and return the lower-c…
Aarush289 Oct 24, 2025
a620463
Docstring added
Aarush289 Oct 24, 2025
cf7ec54
app.py updated to remove noise in exception handling
Aarush289 Oct 24, 2025
a530bce
Multi-threading issue is resolved
Aarush289 Oct 24, 2025
3507550
chore: test signed commit (SSH)
Aarush289 Oct 24, 2025
2208ca4
chore: test signed commit (SSH) #2
Aarush289 Oct 24, 2025
c0050dc
chore: test signed commit (SSH) #3
Aarush289 Oct 24, 2025
fb47398
Merge branch 'master' into feature/my-change
Aarush289 Oct 25, 2025
320a3d1
Removed unnecessary print statements
Aarush289 Oct 25, 2025
89c51de
Indentation done
Aarush289 Oct 25, 2025
4213af5
trim dot before checking the length
Aarush289 Oct 25, 2025
4219dc7
Update hostcheck.py
Aarush289 Oct 25, 2025
b5f8897
Logging exceptions for better debugging
Aarush289 Oct 25, 2025
7f13363
Hostcheck.py OS-independent; add validate_before_scan
Aarush289 Oct 27, 2025
0e41dea
Hostchecker is made OS independent and validate_before_scan is adde…
Aarush289 Oct 27, 2025
89dd251
Update hostcheck.py to make it OS independent
Aarush289 Oct 27, 2025
0a54acd
Indentation done
Aarush289 Oct 27, 2025
d2b205a
removed the duplicate key
Aarush289 Oct 27, 2025
57cf0b6
unused parameter removed
Aarush289 Oct 27, 2025
734bd63
"Fix import order (ruff E402), isort formatting; run pre-commit"
Aarush289 Oct 27, 2025
9892513
Per-pass timeout added
Aarush289 Oct 27, 2025
8f81e75
Deadline removed
Aarush289 Oct 27, 2025
90eede2
Indentation done
Aarush289 Oct 27, 2025
de29d5d
Suggested changes are done
Aarush289 Oct 29, 2025
a635718
Validating IPs using the pre-defined functions in ip.py to improve p…
Aarush289 Oct 30, 2025
78ea0e3
Parser and probes.yaml added
Aarush289 Dec 9, 2025
c8ed916
Merge branch 'master' into feature/my-change
Aarush289 Dec 15, 2025
82c17c7
Delete nettacker/core/parser.py
Aarush289 Dec 16, 2025
069e3b7
Delete nettacker/modules/scan/probes.yaml
Aarush289 Dec 16, 2025
520a350
Pre commit done
Aarush289 Dec 16, 2025
f0a6cc0
Unnecessary files deleted
Aarush289 Jan 7, 2026
653d99a
Merge branch 'master' into feature/my-change
Aarush289 Jan 7, 2026
bd38ad4
Merge branch 'master' into feature/my-change
Aarush289 Jul 13, 2026
fb7be10
pre-commit
Aarush289 Jul 13, 2026
637cbc5
update gitignore
Aarush289 Jul 13, 2026
e04d1d0
Merge branch 'master' into feature/my-change
securestep9 Jul 27, 2026
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
1 change: 1 addition & 0 deletions nettacker/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ class DefaultSettings(ConfigBase):
show_all_profiles = False
show_help_menu = False
show_version = False
validate_before_scan = False
skip_service_discovery = False
socks_proxy = None
targets = None
Expand Down
62 changes: 60 additions & 2 deletions nettacker/core/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import shutil
import socket
import sys
from concurrent.futures import ThreadPoolExecutor, as_completed
from threading import Thread

import multiprocess
Expand All @@ -12,7 +13,8 @@
from nettacker.config import Config, version_info
from nettacker.core.arg_parser import ArgParser
from nettacker.core.die import die_failure
from nettacker.core.graph import create_compare_report, create_report
from nettacker.core.graph import create_report, create_compare_report
from nettacker.core.hostcheck import resolve_quick
from nettacker.core.ip import (
generate_ip_range,
get_ip_range,
Expand Down Expand Up @@ -218,7 +220,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 @@ -287,7 +288,64 @@ def scan_target(

return os.EX_OK

def filter_valid_targets(self, targets, timeout_per_target=2.0, max_threads=None, dedupe=True):
"""
Parallel validation of targets via resolve_quick(target, timeout_sec).
Returns a list of canonical targets (order preserved, invalids removed).
Comment on lines +291 to +294

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add coverage for the target-validation feature

This introduces hostname parsing, two-stage DNS resolution, timeout handling, deduplication, and scan-pipeline filtering without any corresponding tests, despite the repository explicitly requiring tests for new features. Add focused tests for successful, invalid, and timed-out resolutions plus scan_target_group filtering so regressions such as the ineffective timeout above are caught.

AGENTS.md reference: AGENTS.md:L27-L31

Useful? React with 👍 / 👎.

"""
# 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__}")
Comment on lines +297 to +300

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Chain the re-raised exception.

Static analysis flags this: re-raising TypeError inside an except TypeError block without from err/from None obscures the original traceback context.

🐛 Proposed fix
         try:
             targets = list(targets)
-        except TypeError:
-            raise TypeError(f"`targets` must be iterable, got {type(targets).__name__}")
+        except TypeError as err:
+            raise TypeError(
+                f"`targets` must be iterable, got {type(targets).__name__}"
+            ) from err
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
targets = list(targets)
except TypeError:
raise TypeError(f"`targets` must be iterable, got {type(targets).__name__}")
try:
targets = list(targets)
except TypeError as err:
raise TypeError(
f"`targets` must be iterable, got {type(targets).__name__}"
) from err
🧰 Tools
🪛 Ruff (0.15.21)

[warning] 301-301: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@nettacker/core/app.py` around lines 298 - 301, Update the exception handler
around the targets conversion to bind the caught TypeError and explicitly chain
the replacement TypeError with “from err” (or intentionally suppress context
with “from None”), preserving the existing message and validation behavior.

Source: Linters/SAST tools


if not targets:
return []

if max_threads is None:
max_threads = min(len(targets), 10) # cap threads

# Preserve order
validated_target = [None] * len(targets) # Invalid targets will be replaced by "None"

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_threads) 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 (OSError, socket.gaierror) as exc:
log.error(f"Invalid target (resolver error): {exc!s}")
continue

if canon:
validated_target[idx] = canon
Comment on lines +324 to +325

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve the original target across discovery scans

When -C is used with a hostname whose spelling changes during canonicalization, such as Example.COM or a trailing-dot name, this child process scans and stores discovery events under the lowercased/cleaned target while the parent retains the original target. The subsequent find_events(target, "port_scan", scan_id) lookup in expand_targets is an exact, case-sensitive target match, so default service discovery finds no event and removes an otherwise live host before the requested modules run; either propagate canonical targets to the parent before grouping or retain the original spelling for event keys.

Useful? React with 👍 / 👎.

else:
log.info(f"Invalid target -> dropping: {orig_target}")

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

if dedupe:
seen, unique = set(), []
for _c in filtered:
if _c not in seen:
seen.add(_c)
unique.append(_c)
return unique
return filtered

Comment thread
coderabbitai[bot] marked this conversation as resolved.
def scan_target_group(self, targets, scan_id, process_number):
if not self.arguments.socks_proxy and self.arguments.validate_before_scan:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Normalize the API value before testing the flag

For /new/scan requests that explicitly submit validate_before_scan=false, Flask supplies the nonempty string "false"; api/engine.py only normalizes skip_service_discovery before constructing the SimpleNamespace, so this condition treats that value as true and unexpectedly enables target filtering. Normalize this new form field using the endpoint's existing boolean convention before evaluating it.

Useful? React with 👍 / 👎.

targets = self.filter_valid_targets(
targets,
timeout_per_target=2.0,
max_threads=self.arguments.parallel_module_scan or None,
dedupe=True,
Comment on lines +345 to +347

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Cap validation workers independently of module concurrency

When users set a large -M/--parallel-module-scan value for module throughput, this passes that unbounded value directly to the validation executor, bypassing the ten-worker cap used only when max_threads is None. Each validation worker then creates another one-thread executor in resolve_quick, and this occurs in every target-group process, so a large target list and -M value can create thousands of threads and exhaust memory or thread limits before scanning starts; apply a dedicated bounded validation-worker limit.

Useful? React with 👍 / 👎.

)
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
8 changes: 8 additions & 0 deletions nettacker/core/arg_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,14 @@ def add_arguments(self):
dest="skip_service_discovery",
help=_("skip_service_discovery"),
)
method_options.add_argument(
"-C",
"--validate-before-scan",
action="store_true",
default=Config.settings.validate_before_scan,
dest="validate_before_scan",
help=_("validate_before_scan"),
)
method_options.add_argument(
"-t",
"--thread-per-host",
Expand Down
92 changes: 92 additions & 0 deletions nettacker/core/hostcheck.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# nettacker/core/hostcheck.py
from __future__ import annotations

import concurrent.futures
import re
import socket

from nettacker import logger
from nettacker.core.ip import is_single_ipv4, is_single_ipv6

log = logger.get_logger()

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


def valid_hostname(host: str, allow_single_label: bool = True) -> bool:
"""
Validate hostname syntax per RFC 1123.
Args:
host: Hostname to validate.
allow_single_label: If True, accept single-label names (e.g., "localhost").

Returns:
True if the hostname is syntactically valid.
"""
if host.endswith(
"."
): # From RFC 1123 ,the number of characters can be 250 at max (without dots) and 253 with dots
host = host[:-1]
if len(host) > 253:
return False
parts = host.split(".")
if len(parts) < 2 and not allow_single_label:
return False
return all(_LABEL.match(p) for p in parts)


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 _clean_host(s: str) -> str:
# remove surrounding quotes and whitespaces
s = s.strip().strip('"').strip("'")
s = s.strip() # again, after quote strip
# drop trailing commas that often sneak in from CSV-like inputs
if s.endswith(","):
s = s[:-1].rstrip()
# collapse accidental spaces inside
return s


def resolve_quick(
host: str, timeout_sec: float = 2.0, allow_single_label: bool = True
) -> tuple[bool, str | None]:
"""
Perform fast DNS resolution with timeout.
Args:
host: Hostname or IP literal to resolve.
timeout_sec: Maximum time to wait for resolution.
allow_single_label: If True, allow single-label hostnames (e.g., "intranet").

Returns:
(True, host_name) on success, (False, None) on failure/timeout.
"""
host = _clean_host(host)
if is_single_ipv4(host) or is_single_ipv6(host): # IP literal, no resolution needed
return True, host
Comment on lines +68 to +69

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Verify reachability before accepting IP literals

When -C is used with an offline or unroutable IP literal, this early return marks it valid without performing any network operation, so the target is still passed to every requested module. DNS success for hostnames likewise establishes only name resolution, not connectivity, which conflicts with the new CLI help promising that unreachable targets are skipped; either add an actual bounded reachability probe or describe this option strictly as hostname-syntax/DNS validation.

Useful? React with 👍 / 👎.


if host.endswith("."):
host = host[:-1]

if not valid_hostname(host, allow_single_label=allow_single_label):
return False, None

def _call(use_ai_addrconfig: bool):
return _gai_once(host, use_ai_addrconfig, None)

for use_ai in (True, False):
try:
# Run getaddrinfo in a thread so we can enforce timeout
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as ex:
fut = ex.submit(_call, use_ai)
fut.result(timeout=timeout_sec) # raises on timeout or error
Comment on lines +83 to +85

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Enforce the resolver timeout without waiting for shutdown

When getaddrinfo blocks longer than timeout_sec (for example, with a stalled DNS resolver), fut.result() raises TimeoutError, but exiting this executor context invokes shutdown(wait=True) and blocks until the lookup actually returns before trying the second lookup. A mocked 250 ms lookup with a 30 ms timeout therefore takes about 500 ms, and a permanently stuck resolver can stall every -C scan indefinitely; use an isolation mechanism that does not synchronously join the timed-out operation.

Useful? React with 👍 / 👎.

return True, host.lower()
except concurrent.futures.TimeoutError:
continue
except (OSError, socket.gaierror):
# DNS resolution failed for this candidate, try next
Comment thread
Aarush289 marked this conversation as resolved.
continue
Comment thread
Aarush289 marked this conversation as resolved.
return False, None
1 change: 1 addition & 0 deletions nettacker/locale/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ API_options: API options
API_port: API port number
Method: Method
skip_service_discovery: skip service discovery before scan and enforce all modules to scan anyway
validate_before_scan: Pre-validate targets/connectivity before scanning; unreachable targets are skipped.
no_live_service_found: no any live service found to scan.
icmp_need_root_access: to use icmp_scan module or --ping-before-scan you need to run the script as root!
available_graph: "build a graph of all activities and information, you must use HTML output. available graphs: {0}"
Expand Down
Loading