Skip to content

Nmap Scanning Enhancements#668

Open
Antu7 wants to merge 2 commits intodevfrom
nmap_reporting_update
Open

Nmap Scanning Enhancements#668
Antu7 wants to merge 2 commits intodevfrom
nmap_reporting_update

Conversation

@Antu7
Copy link
Collaborator

@Antu7 Antu7 commented Mar 1, 2026

Overview

This PR significantly refactors the nmap_scan.py module to improve execution times, accurately parse a wider range of vulnerabilities, and overhauls the HTML security report for a more professional, modern look. Additionally, it lays the groundwork for in-depth cryptography testing by capturing and displaying crypto_data within the same UI.

Key Changes

1. Nmap Scanning Enhancements 🚀

  • Performance: Streamlined Nmap execution by adding -T4, --open, and --min-rate 1000 to quickly identify relevant services.

2. Improved Vulnerability Parsing 🛡️

  • Broadened Detection: Now actively catches security risks even if they lack a linked CVE. It scans script_output for indicators like "EXPLOIT", "VULNERABLE", or "vulnerable".
  • Intelligent Defaults: Assigns a default severity score of 5.0 to non-CVE vulnerabilities so that they accurately impact the risk profile.
  • Robust Detail Extraction: Uses improved RegEx to cleanly extract vulnerability descriptions and gracefully falls back to a limited-character dump if standard parsing fails.

3. Redesigned HTML Reporting Engine 📊

  • Modern UI / UX: Integrated the "Inter" font, updated styling using CSS variables, and restructured table outputs for better readability.
  • Dynamic Risk Score: Introduced an algorithm to calculate a Risk Score (0-100) based on vulnerability count and their total severity.
  • Status Hero Section: Added a large visual indicator showcasing either No Security Risks Found (Green/Clean) or an alert indicating the number of risks detected based on the scan's results.

4. Cryptography Integration 🔐

  • Imported ssl, socket, and urllib to handle cryptographic metrics.
  • The generate_html function has been updated to accept crypto_data as a parameter and neatly appends the cryptography snippets into the final report.

Impact

  • Faster execution times when dealing with large network perimeters.
  • No false-negatives regarding severe risks lacking a CVE identification.
  • Sharper presentation, allowing clients to assess their overall web posture much faster via the dedicated Risk Score and Status Hero components.

…ty parsing, a comprehensive risk scoring system, and a new dedicated report generation module.
@Antu7 Antu7 self-assigned this Mar 1, 2026
context = ssl.create_default_context()
try:
with socket.create_connection((domain, 443), timeout=2) as sock:
with context.wrap_socket(sock, server_hostname=domain) as ssock:

Check failure

Code scanning / CodeQL

Use of insecure SSL/TLS version High

Insecure SSL/TLS protocol version TLSv1 allowed by
call to ssl.create_default_context
.
Insecure SSL/TLS protocol version TLSv1_1 allowed by
call to ssl.create_default_context
.

Copilot Autofix

AI 3 days ago

In general, the problem is that ssl.create_default_context() is used without constraining the minimum TLS version, so TLS 1.0 and 1.1 may be negotiated. To fix this, configure the SSL context to disallow TLS 1.0 and 1.1, either via context.minimum_version = ssl.TLSVersion.TLSv1_2 (Python 3.7+) or, for older versions, by setting the appropriate OP_NO_TLSv1 and OP_NO_TLSv1_1 flags on context.options. This preserves existing functionality (connecting to servers, reading certificates, checking TLS version) while ensuring that only strong protocol versions are used for the main validated connection.

The single best fix here is to keep using ssl.create_default_context() but explicitly set context.minimum_version = ssl.TLSVersion.TLSv1_2 right after the context is created. For maximal compatibility with slightly older Python 3 versions that might not support minimum_version, we can defensively fall back to disabling TLSv1 and TLSv1_1 via context.options. These changes occur in Framework/Built_In_Automation/Security/nmap_scan.py in the get_cryptography_data function around line 927, immediately after context = ssl.create_default_context().

We do not need new imports because ssl is already imported at the top of the file. The fix is implemented by replacing the single line that creates the context with a small block that both creates and configures it, leaving all subsequent logic (socket creation, certificate retrieval, TLS version checking) unchanged.

Suggested changeset 1
Framework/Built_In_Automation/Security/nmap_scan.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/Framework/Built_In_Automation/Security/nmap_scan.py b/Framework/Built_In_Automation/Security/nmap_scan.py
--- a/Framework/Built_In_Automation/Security/nmap_scan.py
+++ b/Framework/Built_In_Automation/Security/nmap_scan.py
@@ -925,6 +925,15 @@
     print(f"[{domain}] Fetching SSL/TLS Certificate information...", flush=True)
     try:
         context = ssl.create_default_context()
+        # Restrict to strong TLS versions (TLS 1.2 and above)
+        if hasattr(context, "minimum_version") and hasattr(ssl, "TLSVersion"):
+            context.minimum_version = ssl.TLSVersion.TLSv1_2
+        else:
+            # Fallback for older Python versions: disable TLSv1 and TLSv1_1 explicitly
+            if hasattr(ssl, "OP_NO_TLSv1"):
+                context.options |= ssl.OP_NO_TLSv1
+            if hasattr(ssl, "OP_NO_TLSv1_1"):
+                context.options |= ssl.OP_NO_TLSv1_1
         try:
              with socket.create_connection((domain, 443), timeout=2) as sock:
                  with context.wrap_socket(sock, server_hostname=domain) as ssock:
EOF
@@ -925,6 +925,15 @@
print(f"[{domain}] Fetching SSL/TLS Certificate information...", flush=True)
try:
context = ssl.create_default_context()
# Restrict to strong TLS versions (TLS 1.2 and above)
if hasattr(context, "minimum_version") and hasattr(ssl, "TLSVersion"):
context.minimum_version = ssl.TLSVersion.TLSv1_2
else:
# Fallback for older Python versions: disable TLSv1 and TLSv1_1 explicitly
if hasattr(ssl, "OP_NO_TLSv1"):
context.options |= ssl.OP_NO_TLSv1
if hasattr(ssl, "OP_NO_TLSv1_1"):
context.options |= ssl.OP_NO_TLSv1_1
try:
with socket.create_connection((domain, 443), timeout=2) as sock:
with context.wrap_socket(sock, server_hostname=domain) as ssock:
Copilot is powered by AI and may make mistakes. Always verify output.
p = Path(file_path)

# Absolute path provided
if p.is_absolute() and p.is_file():

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 3 days ago

In general, to fix this issue we must ensure that any user‑supplied path is validated and constrained to a safe root directory before being used in filesystem access. This usually means combining the untrusted segment with a trusted base directory, normalizing the resulting path, and verifying that the resolved path is within the base.

For this function, the safest, least disruptive approach is:

  • Treat any file_path (absolute or relative) as a path segment that must reside under AUTO_LOG_DIR.
  • Combine AUTO_LOG_DIR with file_path and resolve the result with .resolve() to eliminate .. and symlinks.
  • Check that the resolved path is a file and that it is contained within AUTO_LOG_DIR using is_relative_to (Python 3.9+) or an equivalent prefix/ancestor check.
  • If the check fails, return a 400/404 JSON error instead of serving the file.
  • Keep the existing “run_dirs search” behavior for relative paths, but ensure the resulting candidate is also under AUTO_LOG_DIR before returning it (a defense‑in‑depth step).

Concretely, in server/reports.py:

  • Replace the “absolute path” branch (if p.is_absolute() and p.is_file(): return FileResponse(str(p))) so absolute user paths are no longer used directly.
  • Instead, construct candidate = (AUTO_LOG_DIR / file_path).resolve(), verify that candidate.is_file() and that AUTO_LOG_DIR is a parent of candidate. If so, return it; otherwise, continue to the existing search or return an error.
  • Optionally, add a small helper function (within the snippet) to perform the “is path under base dir” check using Path.is_relative_to when available, or base in path.parents for older Pythons.

This keeps existing functionality (serving security reports from within AutomationLog) while eliminating the risk of reading arbitrary paths.

Suggested changeset 1
server/reports.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/server/reports.py b/server/reports.py
--- a/server/reports.py
+++ b/server/reports.py
@@ -26,10 +26,17 @@
         if file_path:
             p = Path(file_path)
 
-            # Absolute path provided
-            if p.is_absolute() and p.is_file():
-                return FileResponse(str(p))
+            # Resolve the requested path under AUTO_LOG_DIR to avoid path traversal
+            explicit_candidate = (AUTO_LOG_DIR / p).resolve()
+            try:
+                is_under_auto_log = explicit_candidate.is_relative_to(AUTO_LOG_DIR)  # type: ignore[attr-defined]
+            except AttributeError:
+                # Python < 3.9 fallback: check by ancestor relationship
+                is_under_auto_log = AUTO_LOG_DIR == explicit_candidate or AUTO_LOG_DIR in explicit_candidate.parents
 
+            if explicit_candidate.is_file() and is_under_auto_log:
+                return FileResponse(str(explicit_candidate))
+
             # Relative path – search across every debug run folder (newest first)
             run_dirs = sorted(
                 AUTO_LOG_DIR.glob("debug_*/session_*/*"),
@@ -37,8 +43,13 @@
                 reverse=True,
             )
             for run_dir in run_dirs:
-                candidate = run_dir / file_path
-                if candidate.is_file():
+                candidate = (run_dir / p).resolve()
+                try:
+                    candidate_under_auto_log = candidate.is_relative_to(AUTO_LOG_DIR)  # type: ignore[attr-defined]
+                except AttributeError:
+                    candidate_under_auto_log = AUTO_LOG_DIR == candidate or AUTO_LOG_DIR in candidate.parents
+
+                if candidate.is_file() and candidate_under_auto_log:
                     return FileResponse(str(candidate))
 
         # ── Case 2: auto-find the newest HTML in any security_report folder ──
EOF
@@ -26,10 +26,17 @@
if file_path:
p = Path(file_path)

# Absolute path provided
if p.is_absolute() and p.is_file():
return FileResponse(str(p))
# Resolve the requested path under AUTO_LOG_DIR to avoid path traversal
explicit_candidate = (AUTO_LOG_DIR / p).resolve()
try:
is_under_auto_log = explicit_candidate.is_relative_to(AUTO_LOG_DIR) # type: ignore[attr-defined]
except AttributeError:
# Python < 3.9 fallback: check by ancestor relationship
is_under_auto_log = AUTO_LOG_DIR == explicit_candidate or AUTO_LOG_DIR in explicit_candidate.parents

if explicit_candidate.is_file() and is_under_auto_log:
return FileResponse(str(explicit_candidate))

# Relative path – search across every debug run folder (newest first)
run_dirs = sorted(
AUTO_LOG_DIR.glob("debug_*/session_*/*"),
@@ -37,8 +43,13 @@
reverse=True,
)
for run_dir in run_dirs:
candidate = run_dir / file_path
if candidate.is_file():
candidate = (run_dir / p).resolve()
try:
candidate_under_auto_log = candidate.is_relative_to(AUTO_LOG_DIR) # type: ignore[attr-defined]
except AttributeError:
candidate_under_auto_log = AUTO_LOG_DIR == candidate or AUTO_LOG_DIR in candidate.parents

if candidate.is_file() and candidate_under_auto_log:
return FileResponse(str(candidate))

# ── Case 2: auto-find the newest HTML in any security_report folder ──
Copilot is powered by AI and may make mistakes. Always verify output.

# Absolute path provided
if p.is_absolute() and p.is_file():
return FileResponse(str(p))

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 3 days ago

In general, the fix is to ensure that any path derived from user input is constrained to a safe root directory and is normalized before use. That means we should never directly serve an arbitrary absolute path provided by the client. Instead, we should only allow access to files within a specific directory tree (here, likely under AUTO_LOG_DIR), resolving and normalizing paths, and then verifying that the result is still within that root. For additional safety, we can also forbid absolute paths altogether for this endpoint.

For this specific function, the single best fix with minimal change in behavior is:

  • Treat file_path as a path relative to the automation log root (AUTO_LOG_DIR), not as an arbitrary absolute path.
  • Build an absolute candidate path as AUTO_LOG_DIR / file_path, normalize it with .resolve(), and verify that the resolved path is within AUTO_LOG_DIR using .relative_to(AUTO_LOG_DIR) (catching ValueError to reject attempts to escape via .. or absolute paths).
  • If the normalized path is a file, serve it; otherwise continue with the existing fallback logic (searching run dirs and finally auto‑finding the latest report).
  • Remove the branch that returns files for arbitrary user‑supplied absolute paths.

Concretely in server/reports.py:

  1. In get_security_report, replace the block that creates p = Path(file_path) and conditionally returns any absolute file (if p.is_absolute() and p.is_file():) plus the current relative search with a safe resolution step:
    • Construct raw_path = AUTO_LOG_DIR / file_path.
    • Call resolved = raw_path.resolve().
    • Use resolved.relative_to(AUTO_LOG_DIR) inside a try block to ensure the resolved path is under AUTO_LOG_DIR. If it isn’t, ignore it and move to the fallback logic.
    • If it is and resolved.is_file() is true, return FileResponse(str(resolved)).
  2. Optionally keep the existing run‑directory search as an additional, but now redundant, resolution method; however, it’s simpler and safer just to rely on the normalized AUTO_LOG_DIR check, since all run dirs are already under AUTO_LOG_DIR.

No new imports are needed: pathlib.Path is already imported and provides .resolve() and .relative_to().

Suggested changeset 1
server/reports.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/server/reports.py b/server/reports.py
--- a/server/reports.py
+++ b/server/reports.py
@@ -18,29 +18,27 @@
     """
     Security report downloader for the ZeuZ Node.
 
-    - file_path (optional): exact path of the report file (absolute OR relative to a run dir).
+    - file_path (optional): exact path of the report file (relative to the AutomationLog dir).
       If not given, the newest HTML file in the security_report folder is returned.
     """
     try:
         # ── Case 1: caller supplied an explicit file path ──────────────────────
         if file_path:
-            p = Path(file_path)
+            # Treat user input as a path relative to AUTO_LOG_DIR, and ensure it
+            # cannot escape this root via ".." or absolute paths.
+            raw_path = AUTO_LOG_DIR / file_path
+            resolved_path = raw_path.resolve()
 
-            # Absolute path provided
-            if p.is_absolute() and p.is_file():
-                return FileResponse(str(p))
+            try:
+                # This will raise ValueError if resolved_path is not under AUTO_LOG_DIR
+                resolved_path.relative_to(AUTO_LOG_DIR)
+            except ValueError:
+                # Disallow paths outside the AutomationLog directory
+                pass
+            else:
+                if resolved_path.is_file():
+                    return FileResponse(str(resolved_path))
 
-            # Relative path – search across every debug run folder (newest first)
-            run_dirs = sorted(
-                AUTO_LOG_DIR.glob("debug_*/session_*/*"),
-                key=lambda d: d.stat().st_mtime,
-                reverse=True,
-            )
-            for run_dir in run_dirs:
-                candidate = run_dir / file_path
-                if candidate.is_file():
-                    return FileResponse(str(candidate))
-
         # ── Case 2: auto-find the newest HTML in any security_report folder ──
         candidates = list(AUTO_LOG_DIR.glob("debug_*/session_*/*/security_report/*.html"))
 
EOF
@@ -18,29 +18,27 @@
"""
Security report downloader for the ZeuZ Node.

- file_path (optional): exact path of the report file (absolute OR relative to a run dir).
- file_path (optional): exact path of the report file (relative to the AutomationLog dir).
If not given, the newest HTML file in the security_report folder is returned.
"""
try:
# ── Case 1: caller supplied an explicit file path ──────────────────────
if file_path:
p = Path(file_path)
# Treat user input as a path relative to AUTO_LOG_DIR, and ensure it
# cannot escape this root via ".." or absolute paths.
raw_path = AUTO_LOG_DIR / file_path
resolved_path = raw_path.resolve()

# Absolute path provided
if p.is_absolute() and p.is_file():
return FileResponse(str(p))
try:
# This will raise ValueError if resolved_path is not under AUTO_LOG_DIR
resolved_path.relative_to(AUTO_LOG_DIR)
except ValueError:
# Disallow paths outside the AutomationLog directory
pass
else:
if resolved_path.is_file():
return FileResponse(str(resolved_path))

# Relative path – search across every debug run folder (newest first)
run_dirs = sorted(
AUTO_LOG_DIR.glob("debug_*/session_*/*"),
key=lambda d: d.stat().st_mtime,
reverse=True,
)
for run_dir in run_dirs:
candidate = run_dir / file_path
if candidate.is_file():
return FileResponse(str(candidate))

# ── Case 2: auto-find the newest HTML in any security_report folder ──
candidates = list(AUTO_LOG_DIR.glob("debug_*/session_*/*/security_report/*.html"))

Copilot is powered by AI and may make mistakes. Always verify output.
return FileResponse(str(latest_report))

except Exception as e:
return JSONResponse({"error": str(e)}, status_code=500)

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.

Copilot Autofix

AI 3 days ago

In general, to fix this kind of issue, the API should not return raw exception messages (or stack traces) to the client. Instead, it should return a generic, user-safe error message and log the detailed exception (including stack trace) on the server for diagnostics. This ensures developers still have the information needed for debugging while preventing information disclosure to external users.

For this specific route in server/reports.py, the best approach without changing existing functionality is:

  1. Keep the broad except Exception to preserve current error-handling behavior (the route still returns HTTP 500 when something unexpected happens).
  2. Inside the except block:
    • Log the exception details (including stack trace) on the server side.
    • Return a generic JSON error message that does not include str(e) or any sensitive details.
  3. To log the exception with a stack trace, use Python’s standard logging module and logger.exception(...), which automatically includes the traceback.
  4. This requires:
    • Importing logging at the top of server/reports.py.
    • Initializing a module-level logger via logging.getLogger(__name__).
    • Replacing JSONResponse({"error": str(e)}, status_code=500) with a logging call and a generic JSONResponse.

Concretely, in server/reports.py:

  • Add import logging near the other imports and define logger = logging.getLogger(__name__) after the imports.
  • Modify the except block of get_security_report (lines 56–57) to:
    • Call logger.exception("Unhandled error in get_security_report").
    • Return JSONResponse({"error": "An internal error occurred while fetching the security report."}, status_code=500) or similarly generic text.
Suggested changeset 1
server/reports.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/server/reports.py b/server/reports.py
--- a/server/reports.py
+++ b/server/reports.py
@@ -4,7 +4,10 @@
 from fastapi import APIRouter
 from fastapi.responses import FileResponse, JSONResponse
 from typing import Optional
+import logging
 
+logger = logging.getLogger(__name__)
+
 router = APIRouter(prefix="/debug/reports/security", tags=["security-reports"])
 
 # Resolve the Node's root directory relative to THIS file's location
@@ -54,4 +56,8 @@
         return FileResponse(str(latest_report))
 
     except Exception as e:
-        return JSONResponse({"error": str(e)}, status_code=500)
+        logger.exception("Unhandled error in get_security_report")
+        return JSONResponse(
+            {"error": "An internal error occurred while fetching the security report."},
+            status_code=500,
+        )
EOF
@@ -4,7 +4,10 @@
from fastapi import APIRouter
from fastapi.responses import FileResponse, JSONResponse
from typing import Optional
import logging

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/debug/reports/security", tags=["security-reports"])

# Resolve the Node's root directory relative to THIS file's location
@@ -54,4 +56,8 @@
return FileResponse(str(latest_report))

except Exception as e:
return JSONResponse({"error": str(e)}, status_code=500)
logger.exception("Unhandled error in get_security_report")
return JSONResponse(
{"error": "An internal error occurred while fetching the security report."},
status_code=500,
)
Copilot is powered by AI and may make mistakes. Always verify output.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant