Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
67 changes: 64 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ on:
pull_request:

jobs:
test:
unit-tests:
name: Unit And Contract Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -20,6 +21,66 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest
- name: Run tests
- name: Run unit and contract suite
run: pytest -q

browser-regressions:
name: Browser Regressions
runs-on: ubuntu-latest
env:
NMAPUI_RUN_BROWSER_REGRESSION: "1"
NMAPUI_TRUST_LOCAL_UI: "true"
NMAPUI_ALLOW_UNSAFE_WERKZEUG: "true"
NMAPUI_DEBUG: "false"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Install Playwright Chromium
run: python -m playwright install --with-deps chromium
- name: Run browser regression suite
run: pytest -q tests/test_browser_regressions.py
- name: Upload browser regression diagnostics
if: failure()
uses: actions/upload-artifact@v4
with:
name: browser-regression-diagnostics
path: |
logs
data
.pytest_cache
if-no-files-found: ignore

packaged-smoke:
name: Packaged App Smoke
runs-on: macos-latest
env:
NMAPUI_RUN_PACKAGED_SMOKE: "1"
NMAPUI_SKIP_OPEN: "1"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run packaged smoke suite
run: pytest -q tests/test_packaged_app_smoke.py
- name: Upload packaged smoke diagnostics
if: failure()
uses: actions/upload-artifact@v4
with:
name: packaged-smoke-diagnostics
path: |
NmapUI.app
logs
data
.pytest_cache
if-no-files-found: ignore
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ A web-based GUI for Nmap network scanning with real-time results, CVE detection,
./build.sh
```

`build.sh` will automatically run `install.sh` if dependencies haven't been set up yet. After a successful build, look for the network icon in your macOS menu bar. The app serves NmapUI at `http://127.0.0.1:9000`.
`build.sh` will automatically run `install.sh` if dependencies haven't been set up yet. After a successful build, look for the network icon in your macOS menu bar. The app serves NmapUI on a local loopback URL, defaulting to `http://127.0.0.1:9000` and falling back to the next available local port if needed.
The wrapper target is selected automatically from your host architecture (`arm64` or `x86_64`). Override it explicitly with `NMAPUI_SWIFT_TARGET` if needed.
The completed `NmapUIMenuBar.app` is installed into `/Applications` when writable, otherwise `~/Applications`, replacing any existing install. Override the destination explicitly with `NMAPUI_APPLICATIONS_DIR`.
The completed `NmapUI.app` is installed into `/Applications` when writable, otherwise `~/Applications`, replacing any existing install. Override the destination explicitly with `NMAPUI_APPLICATIONS_DIR`.

> **Prerequisites**: Xcode Command Line Tools (`xcode-select --install`) and [Homebrew](https://brew.sh) are needed by `install.sh` to pull in `nmap`, `arp-scan`, etc.

Expand Down Expand Up @@ -131,6 +131,10 @@ The app will:

Open your browser to `http://127.0.0.1:9000`

### Report Copies on Desktop

Enable **Settings → Report Exports → Save reports to Desktop** to copy report artifacts (PDF, HTML, XML, Nmap outputs) into `~/Desktop/nmapui-reports`, preserving the scan folder structure.

### Quick Start (Skip Checks)

To skip startup dependency checks:
Expand Down
36 changes: 28 additions & 8 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from nmapui.app_task_bindings import build_task_bindings
from nmapui.app_bindings import build_client_state_helpers, build_event_helpers
from nmapui.app_composition import (
build_execute_auto_monitor_rule_deps,
build_execute_auto_scan_deps,
build_startup_check_deps,
)
Expand Down Expand Up @@ -128,7 +129,8 @@
logger = logging.getLogger(__name__)

app = Flask(__name__)
allowed_origins = get_allowed_origins()
runtime_options = build_runtime_options(sys.argv)
allowed_origins = get_allowed_origins(port=runtime_options["port"])
socketio = SocketIO(app, cors_allowed_origins=allowed_origins)
CORS(app, resources={r"/api/*": {"origins": allowed_origins}})

Expand Down Expand Up @@ -242,6 +244,7 @@ def safe_emit(event, data=None):

runtime_bindings = build_runtime_bindings(
build_execute_auto_scan_deps=build_execute_auto_scan_deps,
build_execute_auto_monitor_rule_deps=build_execute_auto_monitor_rule_deps,
auto_scan_config=auto_scan_config,
get_current_customer=lambda: current_customer,
get_last_scan_target=lambda: last_scan_target,
Expand All @@ -257,6 +260,19 @@ def safe_emit(event, data=None):
startup_grace_seconds=AUTO_SCAN_STARTUP_GRACE_SECONDS,
current_assignment_loader=state_bindings["load_current_assignment"],
set_current_customer=lambda value: globals().__setitem__("current_customer", value),
settings_state=settings_state,
save_settings=lambda payload: save_settings_state(
settings_path=SETTINGS_FILE,
save_json_document=save_json_document,
settings_state=payload,
remote_sync_secret_path=REMOTE_SYNC_SECRET_FILE,
remote_sync_secret_key_path=REMOTE_SYNC_SECRET_KEY_FILE,
),
job_registry=job_registry,
emit_job_status=emit_job_status,
set_current_customer_state=set_current_customer_state,
set_last_scan_target_state=set_last_scan_target_state,
generate_report_task_provider=lambda: generate_report_task,
)
execute_auto_scan = runtime_bindings["execute_auto_scan"]
load_current_assignment = runtime_bindings["load_current_assignment"]
Expand Down Expand Up @@ -315,6 +331,13 @@ def safe_emit(event, data=None):
get_app_version=get_app_version,
get_default_interface_cached=lambda: DEFAULT_INTERFACE,
settings_state=settings_state,
save_settings=lambda payload: save_settings_state(
settings_path=SETTINGS_FILE,
save_json_document=save_json_document,
settings_state=payload,
remote_sync_secret_path=REMOTE_SYNC_SECRET_FILE,
remote_sync_secret_key_path=REMOTE_SYNC_SECRET_KEY_FILE,
),
startup_state=startup_state,
runtime_store=runtime_store,
get_auto_scan_thread=runtime_bindings["get_auto_scan_thread"],
Expand Down Expand Up @@ -349,13 +372,6 @@ def safe_emit(event, data=None):
start_scan_task=lambda sid, target: start_scan_task(sid, target),
generate_report_task=lambda sid, data: generate_report_task(sid, data),
generate_pdf_from_saved_task=lambda sid, data: generate_pdf_from_saved_task(sid, data),
save_settings=lambda payload: save_settings_state(
settings_path=SETTINGS_FILE,
save_json_document=save_json_document,
settings_state=payload,
remote_sync_secret_path=REMOTE_SYNC_SECRET_FILE,
remote_sync_secret_key_path=REMOTE_SYNC_SECRET_KEY_FILE,
),
validate_google_drive_settings=lambda *, folder_id: validate_google_drive_settings(
folder_id=folder_id,
credentials_path=GOOGLE_DRIVE_CREDENTIALS_FILE,
Expand Down Expand Up @@ -384,6 +400,9 @@ def safe_emit(event, data=None):
key_path=GOOGLE_DRIVE_TOKEN_KEY_FILE,
requests_module=requests,
),
get_customer_name=lambda customer_id: (
(customer_fingerprinter.get_customer_by_id(customer_id) or {}).get("name", "")
),
validate_remote_sync_settings=lambda *, endpoint, api_key: validate_remote_sync_settings(
endpoint=endpoint,
api_key=api_key
Expand Down Expand Up @@ -473,6 +492,7 @@ def startup_checks(quick=False):
def run_server(argv=None):
run_server_runtime(
argv=argv,
runtime_options=runtime_options if argv is None else build_runtime_options(argv),
build_runtime_options=build_runtime_options,
log_auth_posture=log_auth_posture,
startup_checks=startup_checks,
Expand Down
35 changes: 23 additions & 12 deletions build.sh
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
#!/bin/bash

# Build script for NmapUI Menu Bar Wrapper
# Build script for the NmapUI macOS wrapper
# Builds the Swift application bundle and opens it

# Clean up any existing instances
echo "Cleaning up any existing instances..."
pkill -f "NmapUIMenuBar" 2>/dev/null || true
pkill -f "NmapUI.app" 2>/dev/null || true
sleep 1 # Give processes time to terminate

# Set variables
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$SCRIPT_DIR"
PACKAGING_DIR="$ROOT_DIR/packaging/macos"
SRC="$PACKAGING_DIR/NmapUIMenuBarLauncher.swift"
BIN="$ROOT_DIR/NmapUIMenuBar"
APP_NAME="$ROOT_DIR/NmapUIMenuBar.app"
BUILD_DIR="$ROOT_DIR/.build"
BIN="$BUILD_DIR/NmapUI"
APP_NAME="$ROOT_DIR/NmapUI.app"
SYSTEM_APPLICATIONS_DIR="/Applications"
USER_APPLICATIONS_DIR="$HOME/Applications"
BUNDLE_VENV="$APP_NAME/Contents/Resources/.venv"
Expand Down Expand Up @@ -49,7 +50,7 @@ else
APP_INSTALL_DIR="$USER_APPLICATIONS_DIR"
fi

INSTALLED_APP_NAME="$APP_INSTALL_DIR/NmapUIMenuBar.app"
INSTALLED_APP_NAME="$APP_INSTALL_DIR/NmapUI.app"
INSTALLED_RUNTIME_DB="$INSTALLED_APP_NAME/Contents/Resources/data/runtime.sqlite3"
if [[ "${NMAPUI_MIGRATE_DB:-0}" == "1" ]]; then
if [[ -n "${NMAPUI_MIGRATE_DB_FROM:-}" ]]; then
Expand All @@ -64,13 +65,16 @@ if [[ ! -f "$SRC" ]]; then
exit 1
fi

# Ensure build output doesn't collide with the nmapui package on case-insensitive filesystems.
mkdir -p "$BUILD_DIR"

# Run install.sh if .venv doesn't exist yet
if [[ ! -d "$ROOT_DIR/.venv" && ! -d "$ROOT_DIR/venv" ]]; then
echo "No virtual environment found — running install.sh first..."
bash "$ROOT_DIR/install.sh" || { echo "install.sh failed"; exit 1; }
fi

echo "Building NmapUI Menu Bar Wrapper..."
echo "Building NmapUI macOS wrapper..."
echo "Source: $SRC"
echo "SDK: $SDK"
echo "Host architecture: $HOST_ARCH"
Expand All @@ -82,6 +86,13 @@ if [[ "${NMAPUI_MIGRATE_DB:-0}" == "1" ]]; then
echo "Database migration source: $MIGRATION_SOURCE_DB"
fi

# The Swift compiler outputs a binary at $BIN. If a file or directory exists there
# (from a previous build or manual artifact), it will cause compilation to fail.
if [[ -e "$BIN" ]]; then
echo "Removing conflicting build output at $BIN"
rm -rf "$BIN"
fi

# Compile the Swift binary using the requested format
swiftc \
-sdk "$SDK" \
Expand Down Expand Up @@ -197,11 +208,11 @@ cat > "$APP_NAME/Contents/Info.plist" << EOF
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>NmapUI Menu Bar</string>
<string>NmapUI</string>
<key>CFBundleDisplayName</key>
<string>NmapUI Menu Bar</string>
<string>NmapUI</string>
<key>CFBundleIdentifier</key>
<string>com.techmore.nmapuimenubar</string>
<string>com.techmore.nmapui</string>
<key>CFBundleVersion</key>
<string>$APP_VERSION</string>
<key>CFBundleShortVersionString</key>
Expand All @@ -223,7 +234,7 @@ EOF
echo "Application bundle created: $APP_NAME"

# Make the binary executable
chmod +x "$APP_NAME/Contents/MacOS/NmapUIMenuBar"
chmod +x "$APP_NAME/Contents/MacOS/NmapUI"

# Ad-hoc code sign to satisfy macOS Gatekeeper on locally built bundles.
# This prevents "cannot be opened because Apple cannot check it for malicious
Expand Down Expand Up @@ -269,7 +280,7 @@ else
open "$INSTALLED_APP_NAME"
fi

echo "Done! The NmapUI Menu Bar application is now running."
echo "Done! The NmapUI application is now running."
echo "Look for the network icon in your menu bar."
echo "Use the menu to open http://127.0.0.1:9000 or control the bundled app process."
echo "Use the menu to open the selected local NmapUI URL or control the bundled app process."
echo "Menu options: Open NmapUI, Start NmapUI, Stop NmapUI, Quit, Uninstall"
33 changes: 33 additions & 0 deletions nmapui/app_composition.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,37 @@ def build_execute_auto_scan_deps(
}


def build_execute_auto_monitor_rule_deps(
*,
rule,
logger,
network_key,
rate_limiter,
validate_target,
job_registry,
emit_job_status,
generate_report_task,
set_current_customer_state,
set_last_scan_target_state,
settings_state,
save_settings,
):
return {
"rule": rule,
"logger": logger,
"network_key": network_key,
"rate_limiter": rate_limiter,
"validate_target": validate_target,
"job_registry": job_registry,
"emit_job_status": emit_job_status,
"generate_report_task": generate_report_task,
"set_current_customer_state": set_current_customer_state,
"set_last_scan_target_state": set_last_scan_target_state,
"settings_state": settings_state,
"save_settings": save_settings,
}


def build_scan_task_deps(
*,
broadcaster,
Expand Down Expand Up @@ -368,6 +399,7 @@ def build_settings_routes_deps(
*,
settings_state,
save_settings,
get_customer_name,
validate_google_drive,
validate_remote_sync,
get_google_drive_auth_status,
Expand All @@ -378,6 +410,7 @@ def build_settings_routes_deps(
return {
"settings_state": settings_state,
"save_settings": save_settings,
"get_customer_name": get_customer_name,
"validate_google_drive": validate_google_drive,
"validate_remote_sync": validate_remote_sync,
"get_google_drive_auth_status": get_google_drive_auth_status,
Expand Down
2 changes: 2 additions & 0 deletions nmapui/app_handler_registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def register_app_handlers(
generate_report_task,
generate_pdf_from_saved_task,
save_settings,
get_customer_name,
validate_google_drive_settings,
validate_remote_sync_settings,
get_google_drive_auth_status,
Expand Down Expand Up @@ -181,6 +182,7 @@ def register_app_handlers(
settings_routes_deps=build_settings_routes_deps(
settings_state=settings_state,
save_settings=save_settings,
get_customer_name=get_customer_name,
validate_google_drive=validate_google_drive_settings,
validate_remote_sync=validate_remote_sync_settings,
get_google_drive_auth_status=get_google_drive_auth_status,
Expand Down
Loading
Loading