Add scheduled reports + app-only cert auth; fix tenant-wide user-access audit

Feature work:
- Certificate (app-only) auth per profile: cert store, context/Graph client
  factories, automated app-registration provisioning (delegated + application
  permissions, admin consent), and a SessionManager seam that resolves the auth
  model per profile.
- Scheduled reports: repositories, hosted service/runner/coordinator, report
  pages, and email delivery (app-only Mail.Send).
- Tenant-wide user-access audit when no site is selected.

Audit fixes:
- Site enumeration: app-only discovery used Graph getAllSites (needs Graph
  Sites.Read.All the cert app lacks) and silently returned empty. Switched to
  the admin-host CSOM TenantSiteEnumerator, matching the scheduler; both auth
  models now share one enumeration path.
- Group expansion: the scan records a SharePoint group as a single principal, so
  user-centric audits found nothing for group-granted access. Resolve group
  membership (shared by audit + scheduler) and attribute it to the target user.
- M365 group claims: the resolver only recognized AAD security groups
  (c:0t.c|). Group-connected/Teams sites grant via the M365 group claim
  (c:0o.c|…|<guid>[_o]); now expanded too, resolving owners for the "_o" claim.
- Provision Directory.Read.All as an application permission so M365/AAD group
  expansion works under the cert identity.

Also: ignore data/appcerts/ (encrypted certificate key material).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 17:55:28 +02:00
parent 1b0f4ce588
commit 6d9c79ad5a
40 changed files with 3020 additions and 269 deletions
+35 -18
View File
@@ -1,35 +1,35 @@
:root {
--sidebar-width: 248px;
--sidebar-collapsed-width: 78px;
--bg: #eef0f7;
--page-bg: #eef0f7;
--bg: #eff6ff;
--page-bg: #eff6ff;
--sidebar-bg: #ffffff;
--sidebar-text: #3f4254;
--sidebar-muted: #8a8d9b;
--sidebar-hover: #f2f3f9;
--sidebar-accent: #5b5bd6;
--sidebar-active: #5b5bd6;
--sidebar-hover: #eff6ff;
--sidebar-accent: #006cd2;
--sidebar-active: #006cd2;
--card-bg: #fff;
--surface-hover: #f2f3f9;
--th-bg: #f4f5fb;
--surface-hover: #eff6ff;
--th-bg: #eff6ff;
--input-bg: #fff;
--border: #e6e7f0;
--accent: #5b5bd6;
--accent-dark: #4a4ac0;
--accent-soft: rgba(91,91,214,.12);
--border: #d8e6f5;
--accent: #006cd2;
--accent-dark: #092c55;
--accent-soft: rgba(0,108,210,.12);
--danger: #d13438;
--success: #107c10;
--warn: #797673;
--text: #323130;
--text-muted: #605e5c;
--surface-2: #2d2d4e;
--warn: #fea20a;
--text: #092c55;
--text-muted: #5a6b80;
--surface-2: #092c55;
--font: 'Segoe UI', system-ui, sans-serif;
/* shape + depth — match sidebar */
--radius-lg: 20px;
--radius-md: 12px;
--radius-sm: 10px;
--shadow-card: 0 10px 34px rgba(30,30,70,.10);
--shadow-soft: 0 6px 16px rgba(91,91,214,.22);
--shadow-soft: 0 6px 16px rgba(0,108,210,.22);
}
*, *::before, *::after { box-sizing: border-box; }
@@ -133,7 +133,7 @@ body {
.nav-item:hover { background: var(--sidebar-hover); }
.nav-item.active {
background: var(--sidebar-accent); color: #fff;
box-shadow: 0 6px 16px rgba(91,91,214,.35);
box-shadow: 0 6px 16px rgba(0,108,210,.35);
}
.nav-icon { font-size: 16px; min-width: 22px; text-align: center; }
.nav-label { overflow: hidden; text-overflow: ellipsis; }
@@ -371,9 +371,26 @@ body {
100% { margin-left: 100%; }
}
/* ── User→Sites access drill-down ── */
.site-drill { border: 1px solid var(--border); border-radius: var(--radius-md); overflow: hidden; margin-bottom: 8px; background: var(--card-bg); }
.site-drill-header {
display: flex; align-items: center; gap: 10px; width: 100%;
padding: 11px 14px; border: none; background: none; cursor: pointer;
font-family: inherit; font-size: 13.5px; text-align: left; color: var(--text);
transition: background .12s;
}
.site-drill-header:hover { background: var(--surface-hover); }
.site-drill.open .site-drill-header { background: var(--surface-hover); }
.drill-caret { color: var(--text-muted); font-size: 11px; transition: transform .15s; flex-shrink: 0; }
.drill-caret.open { transform: rotate(90deg); }
.drill-title { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.drill-url { font-size: 11px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.site-drill-body { border-top: 1px solid var(--border); }
.site-drill-body .data-table-wrap { border: none; border-radius: 0; }
/* ── Feature cards (Home) ── */
.feature-card { cursor: pointer; transition: box-shadow .15s, transform .15s; }
.feature-card:hover { box-shadow: 0 14px 36px rgba(91, 91, 214, .22); transform: translateY(-2px); }
.feature-card:hover { box-shadow: 0 14px 36px rgba(0, 108, 210, .22); transform: translateY(-2px); }
/* ── Visual folder-structure builder ── */
.folder-builder { display: flex; flex-direction: column; gap: 6px; padding: 10px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); }