"
for item in findings
if isinstance(item, dict)
) or "
Findings absent.
"
table_rows = "\n".join(
"
"
f"
{esc(row.get('matched_role_name'))}
"
f"
{esc(row.get('profile_name'))}
"
f"
{esc(row.get('group_name'))}
"
f"
{esc(row.get('user_name'))}
"
f"
{esc(row.get('user_type'))}
"
f"
{esc(row.get('user_active'))}
"
f"
{esc(row.get('access_path'))}
"
"
"
for row in rows
if isinstance(row, dict)
)
artifact_links = "\n".join(
f"{esc(name)}"
for name, path in artifacts.items()
)
return f"""
1C Access Role Audit
1C Access Role Audit
Base: {esc(summary.get('base_id'))} | Role query: {esc(role)} | Generated: {esc(summary.get('generated_at'))}
Risk: {esc(risk)}
{esc(counts.get('users'))}users
{esc(counts.get('groups'))}groups
{esc(counts.get('profiles'))}profiles
{esc(counts.get('rows'))}rows
{esc(len(findings))}findings
Findings
{finding_items}
Artifacts: {artifact_links}
Rows
Role
Profile
Group
User
Type
Active
Access path
{table_rows}
"""
def load_audit_summaries(folder: Path, *, limit: int = 100) -> list[dict[str, Any]]:
summaries: list[dict[str, Any]] = []
for path in folder.glob("*.summary.json"):
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
continue
if isinstance(data, dict):
data.setdefault("summary_path", str(path))
summaries.append(data)
summaries.sort(key=lambda item: str(item.get("generated_at") or ""), reverse=True)
return summaries[:limit]
def summary_role(summary: dict[str, Any]) -> str | None:
query = summary.get("query") if isinstance(summary.get("query"), dict) else {}
role = query.get("role") if isinstance(query, dict) else None
return str(role) if role is not None else None
def compare_filename_for_pair(old_summary: dict[str, Any], new_summary: dict[str, Any], *, suffix: str) -> str | None:
old_path = old_summary.get("summary_path")
new_path = new_summary.get("summary_path")
if not old_path or not new_path:
return None
old_stem = Path(str(old_path)).stem.replace(".summary", "")
new_stem = Path(str(new_path)).stem.replace(".summary", "")
return f"compare-{old_stem}-{new_stem}.{suffix}"
def render_index_html(base_id: str, summaries: list[dict[str, Any]]) -> str:
rows: list[str] = []
previous_by_role: dict[str, dict[str, Any]] = {}
for item in reversed(summaries):
role_key = (summary_role(item) or "").casefold()
item["previous_summary_path"] = previous_by_role.get(role_key, {}).get("summary_path")
previous_by_role[role_key] = item
for item in summaries:
query = item.get("query") if isinstance(item.get("query"), dict) else {}
counts = item.get("counts") if isinstance(item.get("counts"), dict) else {}
findings = item.get("findings") if isinstance(item.get("findings"), list) else []
artifacts = item.get("artifacts") if isinstance(item.get("artifacts"), dict) else {}
previous = next((candidate for candidate in summaries if candidate.get("summary_path") == item.get("previous_summary_path")), None)
compare_name = compare_filename_for_pair(previous, item, suffix="html") if isinstance(previous, dict) else None
compare_link = f"compare_previous" if compare_name and (Path(str(item.get("summary_path"))).parent / compare_name).exists() else ""
links = " ".join(
f"{esc(name)}"
for name, path in artifacts.items()
if name != "index"
) + (f" {compare_link}" if compare_link else "")
rows.append(
"
"
f"
{esc(item.get('generated_at'))}
"
f"
{esc(query.get('role') if isinstance(query, dict) else None)}
"
f"
{esc(item.get('risk_level') or 'unknown')}
"
f"
{esc(counts.get('users'))}
"
f"
{esc(counts.get('rows'))}
"
f"
{esc(len(findings))}
"
f"
{links}
"
"
"
)
table_rows = "\n".join(rows) or "
Reports absent.
"
generated_at = datetime.now(timezone.utc).isoformat()
return f"""
1C Access Audit Index