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
33 changes: 33 additions & 0 deletions core/about.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,38 @@ def _package_version() -> str:
return _FALLBACK_VERSION


def format_release_advisory(payload: object | None) -> str:
"""Format the Report info Advisory line. Never empty; never raises.

``None`` / unloadable → ``unknown`` (air-gap). No ``sev`` → ``none``.
Pending ``sev`` is loud text only — not a command (#1765).
"""
try:
if not isinstance(payload, dict):
return "unknown"
sev = payload.get("sev")
if sev is None or str(sev).strip() == "" or str(sev).strip().lower() == "none":
return "none"
parts = [f"sev={sev}"]
current = payload.get("current") or payload.get("from")
target = payload.get("target") or payload.get("to")
if current and target:
parts.append(f"{current} → {target}")
parts.append("fix disponível")
seen = payload.get("seen_at") or payload.get("observed_at")
if seen:
parts.append(f"visto em {seen}")
text = " · ".join(str(p) for p in parts if p)
return text if text.strip() else "unknown"
except Exception:
return "unknown"


def _release_advisory_payload() -> dict | None:
"""Optional beacon/cache payload. Fail-open: never raise into reports."""
return None


def get_http_user_agent() -> str:
"""
Default User-Agent for outbound HTTP(S) from Data Boar (REST/API discovery, OAuth token
Expand Down Expand Up @@ -55,6 +87,7 @@ def get_about_info() -> dict:
"version": ver,
"integrity_state": snap.get("integrity_state", "unknown"),
"build_trust": snap.get("trust_level", "unknown"),
"advisory": format_release_advisory(_release_advisory_payload()),
# Note: the template already prints `about.name` before `about.description`,
# so `description` must not repeat the product name.
"description": "Audits personal and sensitive data across databases and filesystems, aligned with LGPD, GDPR, CCPA, HIPAA, and GLBA.",
Expand Down
2 changes: 2 additions & 0 deletions report/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -1497,6 +1497,8 @@ def _build_report_info(
"Field": "Integrity state",
"Value": about.get("integrity_state", "unknown"),
},
# #1765: pending release severity is loud for auditors, never a gate.
{"Field": "Advisory", "Value": about.get("advisory", "unknown")},
{"Field": "Author", "Value": about["author"]},
{"Field": "License", "Value": about["license"]},
{"Field": "Copyright", "Value": about["copyright"]},
Expand Down
125 changes: 125 additions & 0 deletions tests/test_report_advisory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"""#1765: Report info Advisory is loud and never blocks report generation."""

from __future__ import annotations

from report.generator import _build_report_info

_META = {
"started_at": "",
"tenant_name": "",
"technician_name": "",
"config_scope_hash": "",
}

_PENDING = "sev=critical · 1.7.4 → 1.7.6 · fix disponível · visto em 2026-08-26T14:02Z"


def _about(**overrides: object) -> dict:
about = {
"name": "Data Boar",
"version": "1.8.0-beta",
"author": "Fabio Leitao",
"license": "BSD 3-Clause License",
"copyright": "Copyright (c) 2025-2026, Fabio Leitao",
"build_trust": "expected",
"integrity_state": "validated",
}
about.update(overrides)
return about


def _rows(about: dict) -> list[dict]:
return _build_report_info(
session_id="s-advisory",
meta=_META,
about=about,
db_rows=[],
fs_rows=[],
license_ctx=None,
config={},
)


def test_report_info_advisory_loud_when_severity_pending() -> None:
fields = {r["Field"]: r["Value"] for r in _rows(_about(advisory=_PENDING))}
assert fields["Advisory"] == _PENDING


def test_report_info_advisory_unknown_when_absent_still_builds() -> None:
"""Missing advisory must not refuse the report; default matches Integrity state."""
about = _about()
assert "advisory" not in about
rows = _rows(about)
fields = {r["Field"]: r["Value"] for r in rows}
assert fields["Advisory"] == "unknown"
assert fields["Application"] == "Data Boar"
assert fields["Integrity state"] == "validated"


def test_generate_report_succeeds_without_pending_severity(
tmp_path, monkeypatch
) -> None:
from pathlib import Path

import pandas as pd

from report.generator import generate_report

class _Mgr:
def get_findings(self, _sid):
db = [
{
"target_name": "t1",
"table_name": "tbl",
"column_name": "cpf",
"pattern_detected": "LGPD_CPF",
"sensitivity_level": "HIGH",
"sample_value": "***",
}
]
return db, [], [], []

def save_aggregated_identification_risks(self, *_a, **_k):
return None

def get_aggregated_identification_risks(self, _sid):
return []

def list_sessions(self):
return [
{
"session_id": "sess-advisory-none",
"started_at": "2026-01-01T00:00:00",
"tenant_name": "lab",
"technician_name": "op",
}
]

monkeypatch.setattr(
"report.generator._create_heatmap",
lambda *_a, **_k: None,
)
path = generate_report(
_Mgr(), "sess-advisory-none", output_dir=str(tmp_path), config={}
)
assert path
assert Path(path).is_file()
info = pd.read_excel(path, sheet_name="Report info")
advisory = info.loc[info["Field"] == "Advisory", "Value"]
assert not advisory.empty
assert str(advisory.iloc[0]) == "unknown"


def test_format_release_advisory_none_vs_unknown() -> None:
from core.about import format_release_advisory, get_about_info

assert format_release_advisory(None) == "unknown"
assert format_release_advisory({}) == "none"
payload = {
"sev": "critical",
"current": "1.7.4",
"target": "1.7.6",
"seen_at": "2026-08-26T14:02Z",
}
assert format_release_advisory(payload) == _PENDING
assert get_about_info()["advisory"] == "unknown"