diff --git a/api/locales/en.json b/api/locales/en.json index 49ea6e83a..ce11e71ef 100644 --- a/api/locales/en.json +++ b/api/locales/en.json @@ -172,7 +172,7 @@ "run_web_shot_caption": "Dashboard — optional tenant/technician fields and Start scan (no shell required).", "run_auto_h3": "For automation / scripting", "run_cli_h3": "From the CLI", - "run_cli_p1": "From the repo root, prefer uv run python main.py (or activate your venv and run python main.py). Full flag list: python main.py --help. Packaged installs: man 1 data-boar (command) and man 5 data-boar (config file format).", + "run_cli_p1": "Run data-boar from any terminal on your PATH. Full flag list: data-boar --help. Reference: man 1 data-boar (command) and man 5 data-boar (config file format).", "run_cli_oneshot": "One-shot audit (same targets as a dashboard scan):", "run_cli_api": "Start the API / dashBOARd (no scan until you use the browser or HTTP):", "run_cli_tls": "You must either terminate TLS at the process (--https-cert-file + --https-key-file, or the same under api: in config) or explicitly accept plaintext with --allow-insecure-http (or api.allow_insecure_http: true). The official Docker image passes --allow-insecure-http by default; mount certs and remove it for HTTPS.", diff --git a/api/locales/pt-BR.json b/api/locales/pt-BR.json index e4afbf1ce..a4f9b600d 100644 --- a/api/locales/pt-BR.json +++ b/api/locales/pt-BR.json @@ -172,7 +172,7 @@ "run_web_shot_caption": "Painel — campos opcionais de cliente/técnico e Iniciar varredura (sem shell).", "run_auto_h3": "Para automação / scripting", "run_cli_h3": "Pela CLI", - "run_cli_p1": "Na raiz do repositório, prefira uv run python main.py (ou ative o venv e execute python main.py). Lista completa de flags: python main.py --help. Instalações empacotadas: man 1 data-boar (comando) e man 5 data-boar (formato do arquivo de configuração).", + "run_cli_p1": "Execute data-boar em qualquer terminal no PATH. Lista completa de flags: data-boar --help. Referência: man 1 data-boar (comando) e man 5 data-boar (formato do arquivo de configuração).", "run_cli_oneshot": "Auditoria pontual (mesmos alvos que uma varredura pelo painel):", "run_cli_api": "Subir a API / dashBOARd (sem varredura até usar o navegador ou HTTP):", "run_cli_tls": "É preciso terminar TLS no processo (--https-cert-file + --https-key-file, ou o mesmo em api: na config) ou aceitar explicitamente texto claro com --allow-insecure-http (ou api.allow_insecure_http: true). A imagem Docker oficial passa --allow-insecure-http por padrão; monte certificados e remova para HTTPS.", diff --git a/api/routes.py b/api/routes.py index 8e34f20af..a6ca66347 100644 --- a/api/routes.py +++ b/api/routes.py @@ -1686,11 +1686,15 @@ def run_one_target(): @app.get("/{locale_slug}/help", response_class=HTMLResponse) async def help_page(request: Request, locale_slug: LocaleSlug): """Help and documentation page: quickstart, config example, links to README/USAGE docs.""" + from utils.cli_presentation import cli_help_context + tag = _locale_tag_from_slug(locale_slug.value) return templates.TemplateResponse( request=request, name="help.html", - context=_i18n_template_context(request, locale_slug.value, tag, {}), + context=_i18n_template_context( + request, locale_slug.value, tag, cli_help_context() + ), ) diff --git a/api/templates/help.html b/api/templates/help.html index c8afec218..3a8274733 100644 --- a/api/templates/help.html +++ b/api/templates/help.html @@ -34,24 +34,23 @@

{{ t('help.run_web_h3') }}

{{ t('help.run_auto_h3') }}

{{ t('help.run_cli_p1') }}

{{ t('help.run_cli_oneshot') }}

-
uv run python main.py --demo
-data-boar --demo
-uv run python main.py --config config.yaml
-uv run python main.py --config config.yaml --validate-config
-uv run python main.py --config config.yaml --diff <session_a> <session_b>
-uv run python main.py --config config.yaml --diff <session_a> <session_b> --fail-on-new-high
-uv run python main.py --config config.yaml --export-dsar <session_id>
-uv run python main.py --config config.yaml --export-dsar <session_id> --dsar-output dsar.json
-uv run python main.py --config config.yaml --tenant "Acme Corp" --technician "Alice Colleague-V"
+  
data-boar --demo
+data-boar --config config.yaml
+data-boar --config config.yaml --validate-config
+data-boar --config config.yaml --diff <session_a> <session_b>
+data-boar --config config.yaml --diff <session_a> <session_b> --fail-on-new-high
+data-boar --config config.yaml --export-dsar <session_id>
+data-boar --config config.yaml --export-dsar <session_id> --dsar-output dsar.json
+data-boar --config config.yaml --tenant "Acme Corp" --technician "Alice Colleague-V"
 # Same optional toggles as the dashboard checkboxes (this run only):
-uv run python main.py --config config.yaml --scan-compressed --content-type-check --scan-stego --jurisdiction-hint
+data-boar --config config.yaml --scan-compressed --content-type-check --scan-stego --jurisdiction-hint

{{ t('help.run_cli_api') }}

{{ t('help.run_cli_tls') }}

-
uv run python main.py --config config.yaml --web --https-cert-file server.crt --https-key-file server.key
+  
data-boar --config config.yaml --web --https-cert-file server.crt --https-key-file server.key
 # Lab / loopback only — explicit plaintext:
-uv run python main.py --config config.yaml --web --allow-insecure-http
+data-boar --config config.yaml --web --allow-insecure-http
 # Default bind is loopback (127.0.0.1). Listen on all interfaces only with network controls:
-uv run python main.py --config config.yaml --web --allow-insecure-http --host 0.0.0.0 --port 8088
+data-boar --config config.yaml --web --allow-insecure-http --host 0.0.0.0 --port 8088

{{ t('help.run_cli_bind') }}

{{ t('help.run_cli_danger') }}

{{ t('help.run_cli_audit') }}

@@ -118,7 +117,7 @@

{{ t('help.cfg_h2') }}

- name: "Documentos_LGPD" type: filesystem - path: /home/user/Documents/LGPD + path: {{ filesystem_lgpd_example }} recursive: true file_scan: diff --git a/docs/data_boar.1 b/docs/data_boar.1 index 3403c9991..307271ba5 100644 --- a/docs/data_boar.1 +++ b/docs/data_boar.1 @@ -49,7 +49,7 @@ is accepted as an alias; both names refer to the same application. The actual entry point is: .br .RS 4 -.B python main.py +.B data-boar [ .I OPTIONS ] @@ -563,19 +563,19 @@ The "Start scan" button triggers .TP Minimal scan with default config: .RS -.B python main.py \-\-config config.yaml +.B data-boar \-\-config config.yaml .RE . .TP Scan with tenant and technician metadata: .RS -.B python main.py \-\-config config.yaml \-\-tenant "Acme Corp" \-\-technician "Alice Colleague-V" +.B data-boar \-\-config config.yaml \-\-tenant "Acme Corp" \-\-technician "Alice Colleague-V" .RE . .TP Wipe all sessions, findings and generated reports (dangerous): .RS -.B python main.py \-\-config config.yaml \-\-reset\-data +.B data-boar \-\-config config.yaml \-\-reset\-data .RE . .SS Web API @@ -589,20 +589,18 @@ in the configuration) for plaintext HTTP. Zero\-config demo (no configuration file): .RS .B data-boar \-\-demo -.br -.B python main.py \-\-demo .RE . .TP Start the API on port 8088 (plaintext, explicit opt\-in): .RS -.B python main.py \-\-config config.yaml \-\-web \-\-allow\-insecure\-http \-\-port 8088 +.B data-boar \-\-config config.yaml \-\-web \-\-allow\-insecure\-http \-\-port 8088 .RE . .TP HTTPS (PEM cert + key; TLS >= 1.2): .RS -.B python main.py \-\-config config.yaml \-\-web \-\-https\-cert\-file server.crt \-\-https\-key\-file server.key +.B data-boar \-\-config config.yaml \-\-web \-\-https\-cert\-file server.crt \-\-https\-key\-file server.key .RE . .TP @@ -610,14 +608,14 @@ Use CONFIG_PATH and start the server: .RS .nf export CONFIG_PATH=/etc/lgpd\-audit/config.yaml -python main.py \-\-web \-\-allow\-insecure\-http \-\-port 8088 +data-boar \-\-web \-\-allow\-insecure\-http \-\-port 8088 .fi .RE . .TP Listen on all interfaces (e.g.\& LAN or container without Docker env); use only with network controls: .RS -.B python main.py \-\-config config.yaml \-\-web \-\-allow\-insecure\-http \-\-host 0.0.0.0 \-\-port 8088 +.B data-boar \-\-config config.yaml \-\-web \-\-allow\-insecure\-http \-\-host 0.0.0.0 \-\-port 8088 .RE . .SS curl \- API examples diff --git a/docs/data_boar.5 b/docs/data_boar.5 index 8c6bc13ac..acd500437 100644 --- a/docs/data_boar.5 +++ b/docs/data_boar.5 @@ -233,7 +233,7 @@ default resolution when unset is unless .B API_HOST is set or -.B "main.py \-\-web \-\-host" +.B "data-boar \-\-web \-\-host" overrides); optionally .I require_api_key @@ -480,7 +480,7 @@ Example main config (excerpt): \fBapi:\fR port: 8088 # host: 127.0.0.1 # optional bind address; see man 1 data-boar for CLI and API_HOST - # https_cert_file: /path/server.crt # optional: TLS for python main.py --web + # https_cert_file: /path/server.crt # optional: TLS for data-boar --web # https_key_file: /path/server.key # optional: must pair with https_cert_file # allow_insecure_http: false # explicit plaintext only when true (Docker image CMD often sets CLI flag instead) diff --git a/main.py b/main.py index 3e1e1e8e2..87ec9b69d 100644 --- a/main.py +++ b/main.py @@ -263,7 +263,11 @@ def _run_session_diff_cli( def main() -> None: + from utils.cli_presentation import build_cli_epilog, cli_prog_name + + _cli_prog = cli_prog_name() parser = argparse.ArgumentParser( + prog=_cli_prog, description=( "Data Boar — enterprise data discovery and risk governance engine. " "Loads YAML/JSON config, scans configured databases/filesystems/APIs/shares, " @@ -271,54 +275,7 @@ def main() -> None: "Run once from the CLI or start a REST API dashboard (LGPD/GDPR/CCPA-aware patterns; " "additional frameworks via config)." ), - epilog=( - "Configuration:\n" - " - Main config file (YAML or JSON) defines targets (databases, filesystems, APIs, shares),\n" - " detection options and report settings. Default is 'config.yaml' in the current directory.\n" - " - See docs/USAGE.md for a full schema and examples.\n" - "\n" - "CLI examples:\n" - " # One-shot audit with the default config.yaml\n" - " python main.py --config config.yaml\n" - "\n" - " # One-shot audit tagging tenant/customer and technician/operator\n" - ' python main.py --config config.yaml --tenant "ACME Corp" --technician "Alice"\n' - "\n" - " # One-shot with archive scan + content-type detection (this run only)\n" - " python main.py --config config.yaml --scan-compressed --content-type-check\n" - "\n" - " # Validate config only (loader checks; no scan or API startup)\n" - " python main.py --config config.yaml --validate-config\n" - "\n" - " # Compare two scan sessions (CI: add --fail-on-new-high)\n" - " python main.py --config config.yaml --diff \n" - "\n" - " # DSAR-oriented JSON export for one session (stdout or --dsar-output)\n" - " python main.py --config config.yaml --export-dsar \n" - "\n" - " # Wipe all collected data and generated reports (dangerous, see SECURITY.md)\n" - " python main.py --config config.yaml --reset-data\n" - "\n" - "Web/API examples:\n" - " # HTTPS: PEM cert + key (TLS >= 1.2)\n" - " python main.py --config config.yaml --web --https-cert-file server.crt --https-key-file server.key\n" - "\n" - " # Plaintext HTTP (explicit risk acceptance; required when not using TLS)\n" - " python main.py --config config.yaml --web --allow-insecure-http\n" - "\n" - " # Explicit port or bind (same flags as before, still need TLS or --allow-insecure-http)\n" - " python main.py --config config.yaml --web --allow-insecure-http --port 9090\n" - " python main.py --config config.yaml --web --allow-insecure-http --host 0.0.0.0\n" - "\n" - " # Zero-config demo (synthetic corpus, loopback dashboard — no config.yaml)\n" - " python main.py --demo\n" - " data-boar --demo\n" - "\n" - "Once a one-shot scan finishes, an Excel report and heatmap PNG are written under\n" - "the configured report.output_dir (default: current directory). When the API is\n" - "running, you can navigate to the documented endpoints (see README.md) to trigger\n" - "scans, list sessions and download the latest reports through the browser." - ), + epilog=build_cli_epilog(), formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( diff --git a/scripts/build_locale_catalogs.py b/scripts/build_locale_catalogs.py index d019d6eb0..f9c5253fa 100644 --- a/scripts/build_locale_catalogs.py +++ b/scripts/build_locale_catalogs.py @@ -201,9 +201,8 @@ def _en() -> dict: "run_auto_h3": "For automation / scripting", "run_cli_h3": "From the CLI", "run_cli_p1": ( - "From the repo root, prefer uv run python main.py (or activate your venv and run " - "python main.py). Full flag list: python main.py --help. Packaged installs: " - "man 1 data-boar (command) and man 5 data-boar (config file format)." + "Run data-boar from any terminal on your PATH. Full flag list: data-boar --help. " + "Reference: man 1 data-boar (command) and man 5 data-boar (config file format)." ), "run_cli_oneshot": "One-shot audit (same targets as a dashboard scan):", "run_cli_api": "Start the API / dashBOARd (no scan until you use the browser or HTTP):", @@ -578,9 +577,8 @@ def tr(d: dict) -> dict: base["help"]["run_auto_h3"] = "Para automação / scripting" base["help"]["run_cli_h3"] = "Pela CLI" base["help"]["run_cli_p1"] = ( - "Na raiz do repositório, prefira uv run python main.py (ou ative o venv e execute " - "python main.py). Lista completa de flags: python main.py --help. Instalações empacotadas: " - "man 1 data-boar (comando) e man 5 data-boar (formato do arquivo de configuração)." + "Execute data-boar em qualquer terminal no PATH. Lista completa de flags: data-boar --help. " + "Referência: man 1 data-boar (comando) e man 5 data-boar (formato do arquivo de configuração)." ) base["help"]["run_cli_oneshot"] = ( "Auditoria pontual (mesmos alvos que uma varredura pelo painel):" diff --git a/tests/operator_help_sync_manifest.py b/tests/operator_help_sync_manifest.py index 0fc3f6ad2..f2712ef47 100644 --- a/tests/operator_help_sync_manifest.py +++ b/tests/operator_help_sync_manifest.py @@ -5,8 +5,8 @@ operators should discover from `--help` or `/help`, add or adjust a row below and update the surfaces listed in `docs/OPERATOR_HELP_AUDIT.md`. -Use ``None`` for a surface that intentionally does not carry that cue (e.g. `uv run` -is documented on `/help` and README, not in argparse text). +Use ``None`` for a surface that intentionally does not carry that cue (e.g. dev-only +``uv run`` flows live in repo docs, not in dashBOARd ``/help`` or installed ``--help``). Canonical detail stays in docs/USAGE*.md, man pages, and the repo README — this file only pins a minimal substring set so tests fail if copy disappears. @@ -14,7 +14,50 @@ from __future__ import annotations +import re from dataclasses import dataclass +from pathlib import Path + +MAN_OPERATOR_PAGE_REL_PATHS: tuple[str, ...] = ( + "docs/data_boar.1", + "docs/data_boar.5", +) + +# Installed-user man pages: bold (.B) command examples must use data-boar, not dev entrypoints. +_MAN_B_LINE_DEV_INVOCATION = re.compile( + r"^\s*\.B\b.*(?:\buv run\b|python main\.py|\bmain\.py\s+\\-)", + re.IGNORECASE, +) + + +def collect_man_operator_dev_invocation_violations( + repo_root: Path | None = None, +) -> list[str]: + """Return violations if operator man troff sources show dev-only CLI invocations.""" + root = repo_root or Path(__file__).resolve().parents[1] + violations: list[str] = [] + for rel in MAN_OPERATOR_PAGE_REL_PATHS: + path = root / rel + if not path.is_file(): + violations.append(f"{rel}: missing man page source") + continue + for lineno, line in enumerate( + path.read_text(encoding="utf-8").splitlines(), start=1 + ): + stripped = line.strip() + if stripped.startswith(".BR python (1)"): + continue + if "uv run" in line: + violations.append(f"{rel}:{lineno}: uv run in operator man page") + if "python main.py" in line: + violations.append( + f"{rel}:{lineno}: python main.py in operator man page" + ) + if _MAN_B_LINE_DEV_INVOCATION.search(line): + violations.append( + f"{rel}:{lineno}: dev invocation in .B example line: {stripped!r}" + ) + return violations @dataclass(frozen=True) @@ -158,8 +201,6 @@ class OperatorHelpMarker: "--jurisdiction-hint", _MAN_JURISDICTION_HINT, ), - # Web /help recommends uv; full docs in README — not duplicated in argparse. - OperatorHelpMarker("uv_run", None, "uv run", None), OperatorHelpMarker("api_host_env", "API_HOST", "API_HOST", "API_HOST"), OperatorHelpMarker("bind_loopback", "127.0.0.1", "127.0.0.1", "127.0.0.1"), OperatorHelpMarker( diff --git a/tests/test_cli_presentation.py b/tests/test_cli_presentation.py new file mode 100644 index 000000000..9b05b38ef --- /dev/null +++ b/tests/test_cli_presentation.py @@ -0,0 +1,114 @@ +"""Tests for OS-aware CLI help presentation (#1130).""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +from fastapi.testclient import TestClient + +from utils.cli_presentation import ( + DASHBOARD_CLI_PROG, + build_cli_epilog, + cli_filesystem_lgpd_example, + cli_help_context, + cli_home_example, + cli_prog_name, +) + +_REPO_ROOT = Path(__file__).resolve().parents[1] + + +def test_cli_prog_name_dev_main_py() -> None: + assert cli_prog_name("main.py") == "python main.py" + assert cli_prog_name("/repo/main.py") == "python main.py" + + +def test_cli_prog_name_installed_entry_point() -> None: + assert cli_prog_name("data-boar") == "data-boar" + assert cli_prog_name("/home/user/.local/bin/data-boar") == "data-boar" + # Synthetic Windows pipx path ( is a PII-guard placeholder token). + win_path = r"C:\Users\\.local\bin\data-boar.exe" + assert cli_prog_name(win_path) == "data-boar" + + +def test_home_example_is_concrete_not_tilde() -> None: + home = cli_home_example() + assert home + assert "~" not in home + assert home == str(Path.home()) + + +def test_filesystem_lgpd_example_under_home() -> None: + path = cli_filesystem_lgpd_example() + assert "~" not in path + assert path.startswith(str(Path.home())) + + +def test_build_cli_epilog_uses_argparse_prog_placeholder() -> None: + epilog = build_cli_epilog() + assert "%(prog)s" in epilog + assert "python main.py" not in epilog + assert "data-boar" not in epilog + + +def test_cli_help_stdout_uses_python_main_when_run_from_repo() -> None: + result = subprocess.run( + [sys.executable, str(_REPO_ROOT / "main.py"), "--help"], + capture_output=True, + text=True, + cwd=str(_REPO_ROOT), + check=False, + ) + assert result.returncode == 0, result.stderr + assert "python main.py --config config.yaml --validate-config" in result.stdout + assert "uv run" not in result.stdout + + +def test_web_help_shows_data_boar_and_concrete_home_path(tmp_path: Path) -> None: + import api.routes as routes + + config_path = tmp_path / "config.yaml" + config_path.write_text( + f"""targets: [] +report: + output_dir: {tmp_path} +api: + port: 8088 +sqlite_path: {tmp_path}/audit.db +scan: + max_workers: 1 +""", + encoding="utf-8", + ) + orig_path = routes._config_path + orig_cfg = routes._config + orig_engine = routes._audit_engine + routes._config_path = str(config_path) + routes._config = None + routes._audit_engine = None + try: + client = TestClient(routes.app) + html = client.get("/help").text + assert "uv run python main.py" not in html + assert "python main.py --help" not in html + assert f"{DASHBOARD_CLI_PROG} --demo" in html + assert f"{DASHBOARD_CLI_PROG} --config config.yaml" in html + assert cli_filesystem_lgpd_example() in html + assert "~" not in html + finally: + routes._config_path = orig_path + routes._config = orig_cfg + routes._audit_engine = orig_engine + + +def test_cli_help_context_keys() -> None: + ctx = cli_help_context() + assert "cli_prog" not in ctx + assert "home_example" in ctx + assert "filesystem_lgpd_example" in ctx + + +def test_dashboard_cli_prog_constant() -> None: + assert DASHBOARD_CLI_PROG == "data-boar" diff --git a/tests/test_operator_help_sync.py b/tests/test_operator_help_sync.py index a12dd400a..ad2b23707 100644 --- a/tests/test_operator_help_sync.py +++ b/tests/test_operator_help_sync.py @@ -12,7 +12,10 @@ import pytest from fastapi.testclient import TestClient -from tests.operator_help_sync_manifest import OPERATOR_HELP_MARKERS +from tests.operator_help_sync_manifest import ( + OPERATOR_HELP_MARKERS, + collect_man_operator_dev_invocation_violations, +) _REPO_ROOT = Path(__file__).resolve().parents[1] _MAN1_PATH = _REPO_ROOT / "docs" / "data_boar.1" @@ -106,3 +109,13 @@ def test_operator_help_markers_in_man1(man1_text: str) -> None: f"(expected troff substring {marker.man1_troff_substring!r}); " "update man page and tests/operator_help_sync_manifest.py" ) + + +def test_man_pages_no_dev_cli_invocation() -> None: + """Operator man pages target installed ``data-boar`` (#1131 anti-drift).""" + violations = collect_man_operator_dev_invocation_violations(_REPO_ROOT) + assert not violations, ( + "Operator man pages must not show uv run / python main.py " + "(use data-boar in .B examples); dev flows stay in README/CONTRIBUTING:\n" + + "\n".join(violations) + ) diff --git a/utils/cli_presentation.py b/utils/cli_presentation.py new file mode 100644 index 000000000..8e68e4d4e --- /dev/null +++ b/utils/cli_presentation.py @@ -0,0 +1,115 @@ +"""Runtime CLI / help presentation for installed (pipx) vs dev entry points.""" + +from __future__ import annotations + +import os +import sys +from pathlib import Path + +# dashBOARd /help always shows the packaged command name (not dev ``python main.py``). +DASHBOARD_CLI_PROG = "data-boar" + + +def _basename_for_argv0(raw: str) -> str: + """Basename that tolerates Windows-style paths on POSIX runners.""" + if "\\" in raw: + from pathlib import PureWindowsPath + + return PureWindowsPath(raw).name + return os.path.basename(raw) + + +def cli_prog_name(argv0: str | None = None) -> str: + """ + Command prefix shown in argparse ``prog`` and ``--help``. + + Installed users see ``data-boar``; repo/dev runs see ``python main.py``. + """ + if argv0 is not None: + raw = argv0 + elif len(sys.argv) >= 2: + script = _basename_for_argv0(sys.argv[1]) + interpreter = _basename_for_argv0(sys.argv[0]).lower() + if script == "main.py" and ( + interpreter.startswith("python") or interpreter in {"py", "py.exe"} + ): + return "python main.py" + raw = sys.argv[0] + else: + raw = sys.argv[0] if sys.argv else "main.py" + + stem, _ext = os.path.splitext(_basename_for_argv0(raw)) + if stem.lower() == "data-boar": + return "data-boar" + if stem == "main": + return "python main.py" + return "python main.py" + + +def cli_home_example() -> str: + """Concrete home directory for help copy (not ``~``).""" + return str(Path.home()) + + +def cli_filesystem_lgpd_example() -> str: + """Concrete filesystem target path for help/config illustrations.""" + return str(Path.home() / "Documents" / "LGPD") + + +def cli_help_context(_argv0: str | None = None) -> dict[str, str]: + """Template context for dashBOARd ``/help`` (installed-user paths only).""" + return { + "home_example": cli_home_example(), + "filesystem_lgpd_example": cli_filesystem_lgpd_example(), + } + + +def build_cli_epilog() -> str: + """Argparse epilog; ``%(prog)s`` is substituted by argparse from ``prog=``.""" + return ( + "Configuration:\n" + " - Main config file (YAML or JSON) defines targets (databases, filesystems, APIs, shares),\n" + " detection options and report settings. Default is 'config.yaml' in the current directory.\n" + " - See docs/USAGE.md for a full schema and examples.\n" + "\n" + "CLI examples:\n" + " # One-shot audit with the default config.yaml\n" + " %(prog)s --config config.yaml\n" + "\n" + " # One-shot audit tagging tenant/customer and technician/operator\n" + ' %(prog)s --config config.yaml --tenant "ACME Corp" --technician "Alice"\n' + "\n" + " # One-shot with archive scan + content-type detection (this run only)\n" + " %(prog)s --config config.yaml --scan-compressed --content-type-check\n" + "\n" + " # Validate config only (loader checks; no scan or API startup)\n" + " %(prog)s --config config.yaml --validate-config\n" + "\n" + " # Compare two scan sessions (CI: add --fail-on-new-high)\n" + " %(prog)s --config config.yaml --diff \n" + "\n" + " # DSAR-oriented JSON export for one session (stdout or --dsar-output)\n" + " %(prog)s --config config.yaml --export-dsar \n" + "\n" + " # Wipe all collected data and generated reports (dangerous, see SECURITY.md)\n" + " %(prog)s --config config.yaml --reset-data\n" + "\n" + "Web/API examples:\n" + " # HTTPS: PEM cert + key (TLS >= 1.2)\n" + " %(prog)s --config config.yaml --web --https-cert-file server.crt --https-key-file server.key\n" + "\n" + " # Plaintext HTTP (explicit risk acceptance; required when not using TLS)\n" + " %(prog)s --config config.yaml --web --allow-insecure-http\n" + "\n" + " # Explicit port or bind (same flags as before, still need TLS or --allow-insecure-http)\n" + " %(prog)s --config config.yaml --web --allow-insecure-http --port 9090\n" + " %(prog)s --config config.yaml --web --allow-insecure-http --host 0.0.0.0\n" + "\n" + " # Zero-config demo (synthetic corpus, loopback dashboard — no config.yaml)\n" + " %(prog)s --demo\n" + "\n" + "Once a one-shot scan finishes, an Excel report and heatmap PNG are written under\n" + "the configured report.output_dir (default: current directory). When the API is\n" + "running, you can navigate to the documented endpoints (see README.md) to trigger\n" + "scans, list sessions and download the latest reports through the browser." + )