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
28 changes: 26 additions & 2 deletions src/edgewalker/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,14 @@ def run_guided_scan(
"-ao",
help="Allow scan to proceed with active configuration overrides.",
),
unprivileged: bool = typer.Option(
settings.unprivileged,
"--unprivileged",
help="Run without sudo using TCP connect scans (macOS/no-root).",
),
verbose: bool = typer.Option(
False, "--verbose", help="Print detailed nmap progress and discovered hosts/ports."
),
) -> None:
"""Run a guided security scan.

Expand Down Expand Up @@ -224,7 +232,15 @@ def run_guided_scan(
ensure_telemetry_choice()
controller = ScanController()
guided = GuidedScanner(controller)
asyncio.run(guided.automatic_mode(full_scan=full, target=target, full_creds=full_creds))
asyncio.run(
guided.automatic_mode(
full_scan=full,
target=target,
full_creds=full_creds,
unprivileged=unprivileged,
verbose=verbose,
)
)


@app.command()
Expand All @@ -249,8 +265,16 @@ def clear() -> None:


@app.command()
def tui() -> None:
def tui(
unprivileged: bool = typer.Option(
settings.unprivileged,
"--unprivileged",
help="Run without sudo using TCP connect scans (macOS/no-root).",
),
) -> None:
"""Launch the interactive Textual TUI."""
if unprivileged:
update_setting("unprivileged", True)
EdgeWalkerApp().run()


Expand Down
10 changes: 8 additions & 2 deletions src/edgewalker/cli/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ def __init__(self, scanner_service: Optional[ScannerService] = None) -> None:
self.scanner = scanner_service or ScannerService()

async def run_port_scan(
self, full: bool = False, target: str = None
self,
full: bool = False,
target: str = None,
unprivileged: bool = False,
verbose: bool = False,
) -> Optional[PortScanModel]:
"""Run port scan and display results asynchronously."""
scan_type = "FULL" if full else "QUICK"
Expand All @@ -50,7 +54,9 @@ async def run_port_scan(

try:
with utils.console.status(f"[bold green]Scanning {target}..."):
results = await self.scanner.perform_port_scan(target=target, full=full)
results = await self.scanner.perform_port_scan(
target=target, full=full, unprivileged=unprivileged, verbose=verbose
)
except Exception as e:
logger.error(f"Scan failed: {str(e)}")
logger.error("Make sure nmap is installed and you have sudo privileges.")
Expand Down
6 changes: 5 additions & 1 deletion src/edgewalker/cli/guided.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ async def automatic_mode(
full_scan: Optional[bool] = None,
target: Optional[str] = None,
full_creds: bool = False,
unprivileged: bool = False,
verbose: bool = False,
) -> None:
"""Run the automatic guided security assessment asynchronously."""
# Step 1: Choose scan type
Expand All @@ -49,7 +51,9 @@ async def automatic_mode(
# Step 3: Run port scan
utils.console.print()
logger.info(f"Starting {scan_type.lower()} port scan on {target}...")
port_results = await self.controller.run_port_scan(full=full_scan, target=target)
port_results = await self.controller.run_port_scan(
full=full_scan, target=target, unprivileged=unprivileged, verbose=verbose
)

if not port_results:
logger.error("Port scan failed. Returning to mode selection.")
Expand Down
32 changes: 29 additions & 3 deletions src/edgewalker/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,11 @@ def handle_demo_mode(cls, v: Path) -> Path:
description="Run in non-interactive mode (bypass prompts)",
)

unprivileged: bool = Field(
default=False,
description="Run without sudo using TCP connect scans (macOS/no-root).",
)

suppress_warnings: bool = Field(
default=False,
description="Suppress configuration and security warnings in the console",
Expand Down Expand Up @@ -489,6 +494,18 @@ def init_config() -> None:
for warning in settings.get_security_warnings():
logger.warning(warning)

# Check for root ownership of config file (common if run with sudo first)
if config_file.exists() and os.getuid() != 0:
try:
if config_file.stat().st_uid == 0:
logger.warning(
f"Config file '{config_file}' is owned by root. "
"Settings changes will not be saved. "
f"Run 'sudo chown {os.getlogin()} \"{config_file}\"' to fix."
)
except (OSError, AttributeError):
pass

if not config_file.exists():
save_settings(settings)
else:
Expand Down Expand Up @@ -517,9 +534,18 @@ def save_settings(settings_obj: Settings) -> None:
data["theme"] = settings_obj.theme

# Open with restricted permissions (0o600: read/write for owner only)
fd = os.open(config_file, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(fd, "w", encoding="utf-8") as f:
yaml.dump(data, f, sort_keys=False)
try:
fd = os.open(config_file, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(fd, "w", encoding="utf-8") as f:
yaml.dump(data, f, sort_keys=False)
except PermissionError:
logger.warning(
f"Permission denied when saving config to '{config_file}'. "
"If you previously ran with sudo, you may need to fix file ownership: "
f'sudo chown {os.getlogin()} "{config_file}"'
)
except Exception as e:
logger.error(f"Failed to save settings to {config_file}: {e}")


def update_setting(key: str, value: object) -> None:
Expand Down
18 changes: 15 additions & 3 deletions src/edgewalker/core/scanner_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,13 @@ def submit_scan_data(self, module: str, data: dict) -> None:
# No loop running, use sync version
self.telemetry.submit_scan_data_sync(module, data)

async def perform_port_scan(self, target: str, full: bool = False) -> PortScanModel:
async def perform_port_scan(
self,
target: str,
full: bool = False,
unprivileged: bool = False,
verbose: bool = False,
) -> PortScanModel:
"""Perform a port scan and return results as a model asynchronously."""
if self.demo_mode and self.demo_service:
return await self.demo_service.perform_port_scan(target, full)
Expand All @@ -94,11 +100,17 @@ async def perform_port_scan(self, target: str, full: bool = False) -> PortScanMo

if full:
results = await port_scan.full_scan(
target=target, verbose=False, progress_callback=self.progress_callback
target=target,
verbose=verbose,
progress_callback=self.progress_callback,
unprivileged=unprivileged,
)
else:
results = await port_scan.quick_scan(
target=target, verbose=False, progress_callback=self.progress_callback
target=target,
verbose=verbose,
progress_callback=self.progress_callback,
unprivileged=unprivileged,
)

if isinstance(results, dict):
Expand Down
Loading
Loading