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."
+ )