clearview/containers/clearview/site/index.html

604 lines
29 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Clearview | Permission Deviations</title>
<meta name="description" content="Clearview scans Microsoft 365 SharePoint sites and Exchange Online mailboxes for permission deviations.">
<link rel="icon" href="assets/favicon.svg" type="image/svg+xml">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=IBM+Plex+Sans:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="styles.css">
</head>
<body class="app-shell">
<div class="bg-orb orb-one" aria-hidden="true"></div>
<div class="bg-orb orb-two" aria-hidden="true"></div>
<aside class="sidebar">
<div class="sidebar-brand">
<img src="assets/clearview-logo-dark.svg" alt="Clearview" class="brand-logo">
</div>
<nav class="sidebar-nav">
<a href="#/dashboard" class="nav-link" data-route="dashboard">Dashboard</a>
<a href="#/jobs" class="nav-link" data-route="jobs">Scan Jobs</a>
<div class="nav-section">SharePoint</div>
<a href="#/scan/sharepoint" class="nav-link" data-route="scan-sharepoint">New SP Scan</a>
<div class="nav-section">Mailboxes</div>
<a href="#/scan/mailbox" class="nav-link" data-route="scan-mailbox">New Mailbox Scan</a>
<div class="nav-section">Entra</div>
<a href="#/scan/entra" class="nav-link" data-route="scan-entra">New Entra Scan</a>
<div class="nav-spacer"></div>
<a href="#/tenants" class="nav-link" data-route="tenants">Tenants</a>
<a href="#/settings" class="nav-link" data-route="settings">Settings</a>
</nav>
<div class="sidebar-foot">
<span class="sidebar-version" id="appVersion" title="Running Clearview build"></span>
</div>
</aside>
<main class="content">
<header class="content-topbar">
<div class="content-title" id="contentTitle">Dashboard</div>
<div class="content-actions">
<button id="refreshJobsBtn" class="btn btn-outline" type="button">Refresh</button>
<div class="header-user" id="userBadge"></div>
</div>
</header>
<!-- =================================================================== -->
<!-- Route: Dashboard -->
<!-- =================================================================== -->
<section class="route-page" data-route-page="dashboard">
<div class="hero fade-up">
<p class="eyebrow">Permission Drift Detection</p>
<h1>Monitor Microsoft 365 permissions across all customers</h1>
<p class="lede">
Scan SharePoint sites for deviations from root permissions, and Exchange Online
mailboxes for delegated access (Full Access, Send As, Send on Behalf, folder delegations).
</p>
<div class="hero-stats">
<article>
<span class="kpi" id="statTenants">0</span>
<span class="label">Tenants</span>
</article>
<article>
<span class="kpi" id="statJobs">0</span>
<span class="label">Jobs</span>
</article>
<article>
<span class="kpi" id="statRunning">0</span>
<span class="label">Active Jobs</span>
</article>
<article>
<span class="kpi" id="statErrors">0</span>
<span class="label">With errors</span>
</article>
</div>
</div>
<div class="panel">
<div class="panel-header split">
<h2>Recent jobs</h2>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Job ID</th>
<th>Type</th>
<th>Tenant</th>
<th>Status</th>
<th>Targets</th>
<th>Updated</th>
</tr>
</thead>
<tbody id="dashRecentJobs">
<tr><td colspan="6">No jobs yet.</td></tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- =================================================================== -->
<!-- Route: Tenants -->
<!-- =================================================================== -->
<section class="route-page" data-route-page="tenants" hidden>
<div class="panel">
<div class="panel-header split">
<h2>Tenants</h2>
<button id="addTenantBtn" class="btn btn-outline" type="button">Add Tenant</button>
</div>
<div id="addTenantForm" class="scan-form" hidden>
<h3>New Tenant</h3>
<div id="tenantSetupAutomated" class="setup-note" hidden>
<h3>Azure App Setup (automated)</h3>
<p>Connect to the customer's Microsoft tenant, then create a dedicated scan app automatically.</p>
<ul>
<li>Click <strong>Connect Microsoft</strong> and approve admin consent.</li>
<li>Created scan app receives SharePoint <code>Sites.FullControl.All</code> with admin consent.</li>
<li>For mailbox scanning, the <strong>Exchange.ManageAsApp</strong> permission and <strong>Exchange Administrator</strong> Entra role must be added manually after creation — see the <em>Enable mailbox scanning</em> section below.</li>
</ul>
<form id="onboardingForm" class="onboarding-form" action="#" method="post">
<div class="onboarding-grid">
<div class="onboarding-wide">
<button id="connectMicrosoftBtn" class="btn btn-outline" type="button">Connect Microsoft</button>
</div>
<label class="onboarding-wide">
Connected Tenant ID
<input id="connectedTenantId" type="text" placeholder="Connect first to populate tenant id">
</label>
<label class="onboarding-wide">
New Scan App Display Name
<input id="scanAppDisplayName" type="text" value="Clearview Scan App">
</label>
</div>
<button class="btn btn-outline" type="submit">Create Scan App Automatically</button>
</form>
</div>
<div id="tenantSetupManual" class="setup-note" hidden>
<h3>Azure App Setup (manual)</h3>
<p>Create a dedicated Azure app registration in the customer's tenant.</p>
<ol class="setup-steps">
<li>Open <strong>Azure Portal</strong><strong>Entra ID → App registrations → New registration</strong>.</li>
<li>Pick a name (e.g. <em>Clearview Scan App</em>), select <strong>Single tenant</strong>, click <strong>Register</strong>.</li>
<li>Copy <strong>Directory (tenant) ID</strong> and <strong>Application (client) ID</strong>.</li>
<li>For SharePoint: <strong>API permissions → Add a permission → SharePoint → Application permissions</strong>, select <code>Sites.FullControl.All</code>, then click <strong>Grant admin consent</strong>.</li>
<li>For group resolution (recommended): also add <strong>Microsoft Graph → Application permissions → <code>Group.Read.All</code></strong> and grant admin consent. This lets Clearview expand Microsoft 365 / Azure AD security groups to their members and owners during the <em>Resolve groups</em> action. Without it, M365 group entries are kept as a single line.</li>
<li>The primary domain is the tenant's default Microsoft 365 domain — typically <code>&lt;tenantname&gt;.onmicrosoft.com</code>. Find it in <strong>Microsoft 365 admin center → Settings → Domains</strong> (the <em>Default</em> entry).</li>
</ol>
</div>
<div id="tenantSetupMailbox" class="setup-note">
<h3>Enable mailbox scanning (Exchange Online)</h3>
<p>Mailbox scanning needs additional permissions on the scan app, on top of the SharePoint setup. Skip this section if the tenant only needs SharePoint scans.</p>
<ol class="setup-steps">
<li><strong>Add the API permission.</strong> Azure Portal → <strong>Entra ID → App registrations → [your scan app] → API permissions → Add a permission → APIs my organization uses</strong>. Search for <em>Office 365 Exchange Online</em>, choose <strong>Application permissions</strong> and tick <code>Exchange.ManageAsApp</code>. Click <strong>Add permissions</strong>.</li>
<li><strong>Grant admin consent.</strong> Still on the API permissions page, click <strong>Grant admin consent for &lt;tenant&gt;</strong>. Verify the status column shows <em>Granted for &lt;tenant&gt;</em>.</li>
<li><strong>Assign the Exchange Administrator role.</strong> Entra ID → <strong>Roles and administrators</strong> → search <em>Exchange Administrator</em> → click the role → <strong>Add assignments</strong> → search the scan app by name (you'll need to switch the picker to include <em>Service principals / Apps</em>) → select it and confirm. This role grants the app the right to read mailbox permissions; it cannot be granted via Microsoft Graph and must be done in the portal.</li>
<li><strong>Generate a certificate.</strong> Save the tenant first (this section's form), then use the <strong>Certificate</strong> button in the Tenants table to generate a self-signed RSA-2048 key. The public PEM appears in a panel — click <strong>Download .cer</strong>.</li>
<li><strong>Upload the certificate to Azure.</strong> Back in the scan app, go to <strong>Certificates &amp; secrets → Certificates → Upload certificate</strong>, pick the downloaded <code>.cer</code> file, and confirm. Azure shows the SHA-1 thumbprint — it must match the one shown in the Tenants table.</li>
<li><strong>Fill in the Primary Domain field</strong> on the tenant form (e.g. <code>contoso.onmicrosoft.com</code>). Clearview uses this for <code>Connect-ExchangeOnline -Organization</code> and to auto-fill the Mailbox scan form.</li>
<li><strong>Test the connection.</strong> Run a <em>Scan all mailboxes</em> job for this tenant; preflight on the first target validates that authentication works end-to-end.</li>
</ol>
<p class="setup-hint">Exchange Online does <strong>not</strong> support client-secret app-only authentication. Mailbox scans require a certificate. The same certificate is reused for SharePoint scans, so generating it once is enough.</p>
</div>
<div class="auth-grid">
<label class="onboarding-wide">
Tenant Name (label for your reference)
<input id="newTenantName" type="text" placeholder="Contoso">
</label>
<label>
Tenant ID
<input id="newTenantTenantId" type="text" placeholder="00000000-0000-0000-0000-000000000000">
</label>
<label>
Primary Domain <span style="font-weight:400;font-size:0.82rem">(used by mailbox scanning, e.g. contoso.onmicrosoft.com)</span>
<input id="newTenantPrimaryDomain" type="text" placeholder="contoso.onmicrosoft.com">
</label>
<label>
Client ID
<input id="newTenantClientId" type="text" placeholder="00000000-0000-0000-0000-000000000000">
</label>
<label class="auth-secret">
Client Secret <span style="font-weight:400;font-size:0.82rem">(optional — not needed when using a certificate; not supported for mailbox scans)</span>
<input id="newTenantClientSecret" type="password" placeholder="Leave empty if you will generate a certificate">
</label>
</div>
<div class="form-actions">
<button id="saveTenantBtn" class="btn btn-solid" type="button">Save Tenant</button>
<button id="cancelTenantBtn" class="btn btn-outline" type="button">Cancel</button>
</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Name</th>
<th>Tenant ID</th>
<th>Client ID</th>
<th>Auth</th>
<th>Added</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="tenantsTableBody">
<tr><td colspan="6">No tenants configured yet.</td></tr>
</tbody>
</table>
</div>
<div id="tenantFeedback" class="feedback" aria-live="polite"></div>
<div id="certBlock" class="cert-block" hidden>
<h3>Public Certificate</h3>
<p>Upload this certificate in <strong>Azure Portal → App registrations → [your app] → Certificates &amp; secrets → Certificates → Upload certificate</strong>.</p>
<textarea id="certPem" class="cert-pem" rows="10" readonly></textarea>
<div class="form-actions">
<button id="downloadCertBtn" class="btn btn-solid" type="button">Download .cer</button>
<button id="copyCertBtn" class="btn btn-outline" type="button">Copy to clipboard</button>
<button id="closeCertBtn" class="btn btn-outline" type="button">Close</button>
</div>
</div>
</div>
</section>
<!-- =================================================================== -->
<!-- Route: Scan SharePoint -->
<!-- =================================================================== -->
<section class="route-page" data-route-page="scan-sharepoint" hidden>
<div class="panel">
<div class="panel-header split">
<h2>New SharePoint Scan</h2>
<span class="badge">SharePoint</span>
</div>
<div class="scan-form auth-block">
<h3>Scan mode</h3>
<label>
What to collect
<select id="sharepointScanMode">
<option value="sharepoint">Deviations from root (libraries, folders, files)</option>
<option value="sharepoint_root">Root permissions only (site-level role assignments)</option>
</select>
</label>
<p class="setup-hint">
<strong>Deviations from root</strong> traverses every document library and reports only permissions that
differ from the site root baseline. <strong>Root permissions only</strong> lists the role assignments
on the site root itself — much faster, useful for an inventory of who has site-level access.
</p>
</div>
<div class="scan-form auth-block">
<h3>Tenant</h3>
<label>
Select Tenant Profile
<select id="scanTenantSelect" data-shared-tenant-select>
<option value="">-- Select a tenant --</option>
<option value="__manual__">Manual credentials...</option>
</select>
</label>
</div>
<div id="manualCredentialsBlock" class="scan-form auth-block" hidden>
<h3>Microsoft App Credentials</h3>
<div class="auth-grid">
<label>
Tenant ID
<input id="tenantId" type="text" placeholder="00000000-0000-0000-0000-000000000000">
</label>
<label>
Client ID
<input id="clientId" type="text" placeholder="00000000-0000-0000-0000-000000000000">
</label>
<label class="auth-secret">
Client Secret
<input id="clientSecret" type="password" placeholder="Client secret">
</label>
</div>
</div>
<div class="form-grid">
<form id="manualScanForm" class="scan-form" action="#" method="post">
<h3>Manual URLs</h3>
<label>
Site URLs (one per line)
<textarea id="manualUrls" rows="6" placeholder="https://contoso.sharepoint.com/sites/finance&#10;https://contoso.sharepoint.com/sites/hr"></textarea>
</label>
<label class="checkline">
<input id="manualSkipDefaults" type="checkbox" checked>
<span>Skip default sites (tenant root, app catalog)</span>
</label>
<button class="btn btn-solid" type="submit">Queue manual scan</button>
</form>
<form id="csvScanForm" class="scan-form" action="#" method="post" enctype="multipart/form-data">
<h3>CSV Import</h3>
<label>
Microsoft Sites export (CSV)
<input id="csvFile" type="file" accept=".csv,text/csv">
</label>
<label class="checkline">
<input id="csvSkipDefaults" type="checkbox" checked>
<span>Skip default sites (tenant root, app catalog)</span>
</label>
<button class="btn btn-solid" type="submit">Queue CSV scan</button>
</form>
</div>
<div id="submitFeedback" class="feedback" aria-live="polite"></div>
</div>
</section>
<!-- =================================================================== -->
<!-- Route: Scan Mailbox -->
<!-- =================================================================== -->
<section class="route-page" data-route-page="scan-mailbox" hidden>
<div class="panel">
<div class="panel-header split">
<h2>New Mailbox Scan</h2>
<span class="badge">Exchange Online</span>
</div>
<div class="scan-form auth-block">
<h3>Tenant</h3>
<label>
Select Tenant Profile
<select id="mailboxScanTenantSelect" data-shared-tenant-select>
<option value="">-- Select a tenant --</option>
</select>
</label>
<p class="setup-hint">
Mailbox scanning requires a certificate on the tenant profile and the
<code>Exchange.ManageAsApp</code> permission with the Exchange Administrator role.
Client-secret authentication is not supported for Exchange Online.
</p>
</div>
<div class="form-grid">
<form id="manualMailboxForm" class="scan-form" action="#" method="post">
<h3>Manual UPNs</h3>
<label>
User Principal Names (one per line)
<textarea id="manualMailboxes" rows="6" placeholder="alice@contoso.com&#10;bob@contoso.com"></textarea>
</label>
<button class="btn btn-solid" type="submit">Queue mailbox scan</button>
</form>
<form id="csvMailboxForm" class="scan-form" action="#" method="post" enctype="multipart/form-data">
<h3>CSV Import</h3>
<label>
CSV with <code>UserPrincipalName</code> / <code>Email</code> column
<input id="csvMailboxFile" type="file" accept=".csv,text/csv">
</label>
<button class="btn btn-solid" type="submit">Queue CSV scan</button>
</form>
<form id="allMailboxesForm" class="scan-form" action="#" method="post">
<h3>All mailboxes in tenant</h3>
<label>
Organization (primary tenant domain)
<input id="allMailboxesOrg" type="text" placeholder="contoso.onmicrosoft.com">
</label>
<p class="setup-hint">
Clearview enumerates every mailbox in the tenant via <code>Get-EXOMailbox -ResultSize Unlimited</code>
and queues one target per mailbox. Can take 1060 seconds for large tenants.
</p>
<button class="btn btn-solid" type="submit">Queue scan for all mailboxes</button>
</form>
</div>
<div id="mailboxSubmitFeedback" class="feedback" aria-live="polite"></div>
</div>
</section>
<!-- =================================================================== -->
<!-- Route: Scan Entra Groups -->
<!-- =================================================================== -->
<section class="route-page" data-route-page="scan-entra" hidden>
<div class="panel">
<div class="panel-header split">
<h2>New Entra Group Scan</h2>
<span class="badge">Microsoft Graph</span>
</div>
<div class="scan-form auth-block">
<h3>Tenant</h3>
<label>
Select Tenant Profile
<select id="entraScanTenantSelect">
<option value="">-- Select a tenant --</option>
</select>
</label>
<p class="setup-hint">
Entra group scans use the <strong>Microsoft Graph</strong> API. The scan app needs the
Application permission <code>Group.Read.All</code> with admin consent. Authentication
uses the same tenant certificate as SharePoint and Mailbox scans.
</p>
</div>
<div class="form-grid">
<form id="manualEntraForm" class="scan-form" action="#" method="post">
<h3>Manual Object IDs</h3>
<label>
Group identifiers (one per line — Object ID, mail address, or display name)
<textarea id="manualEntraIds" rows="6" placeholder="00000000-0000-0000-0000-000000000000&#10;Pharmacology@contoso.onmicrosoft.com"></textarea>
</label>
<button class="btn btn-solid" type="submit">Queue Entra scan</button>
</form>
<form id="csvEntraForm" class="scan-form" action="#" method="post" enctype="multipart/form-data">
<h3>CSV Import (Entra export)</h3>
<label>
CSV with <code>Object ID</code> column (Entra "Groups" export)
<input id="csvEntraFile" type="file" accept=".csv,text/csv">
</label>
<p class="setup-hint">
Export from Entra portal → Groups → All groups → Download. Clearview reads the
<code>Object ID</code> / <code>id</code> column; other columns are ignored.
</p>
<button class="btn btn-solid" type="submit">Queue CSV scan</button>
</form>
<form id="allEntraForm" class="scan-form" action="#" method="post">
<h3>All groups in tenant</h3>
<p class="setup-hint">
Enumerates every group in the tenant (any type) via Microsoft Graph and queues one
target per group. Can take 30120 seconds for large tenants.
</p>
<button class="btn btn-solid" type="submit">Queue scan for all groups</button>
</form>
</div>
<div id="entraSubmitFeedback" class="feedback" aria-live="polite"></div>
</div>
</section>
<!-- =================================================================== -->
<!-- Route: Jobs (list + selected job details) -->
<!-- =================================================================== -->
<section class="route-page" data-route-page="jobs" hidden>
<div class="panel">
<div class="panel-header split">
<h2>Scan Jobs</h2>
<div class="panel-header-right">
<select id="jobTypeFilter" class="filter-select">
<option value="">All types</option>
<option value="sharepoint">SharePoint deviations</option>
<option value="sharepoint_root">SharePoint root</option>
<option value="mailbox">Mailbox</option>
<option value="entra_groups">Entra groups</option>
</select>
<select id="jobTenantFilter" class="filter-select">
<option value="">All tenants</option>
</select>
<span id="jobAutoRefresh" class="badge">Auto refresh: on</span>
</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Job ID</th>
<th>Type</th>
<th>Tenant</th>
<th>Source</th>
<th>Status</th>
<th>Targets</th>
<th>Items</th>
<th>Updated</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="jobsTableBody">
<tr><td colspan="9">No jobs yet.</td></tr>
</tbody>
</table>
</div>
</div>
<div class="panel">
<div class="panel-header split">
<h2>Selected Job Details</h2>
<div class="panel-header-right">
<select id="jobSiteFilter" class="filter-select">
<option value="">All targets</option>
</select>
<button id="exportJobBtn" class="btn btn-outline" type="button" hidden>Export Excel</button>
<span id="selectedJobId" class="badge">No selection</span>
</div>
</div>
<div id="jobSummary" class="job-summary">Select a job to inspect targets and deviations.</div>
<div id="jobActivity" class="job-activity" hidden></div>
<h3 class="subheading" id="targetsHeading">Targets</h3>
<div class="table-wrap compact-wrap">
<table>
<thead id="targetsTableHead">
<tr>
<th>URL</th>
<th>Status</th>
<th>Attempts</th>
<th>Error</th>
<th>Connection test</th>
<th></th>
</tr>
</thead>
<tbody id="targetsTableBody">
<tr><td colspan="6">No job selected.</td></tr>
</tbody>
</table>
</div>
<div id="sharingLinksResolveBlock" hidden>
<h3 class="subheading">Resolve Sharing Links</h3>
<p class="resolve-hint">Fetch the actual recipients for the selected link types. Anonymous links have no resolvable members.</p>
<div id="sharingLinksTypes" class="sharing-link-types"></div>
<div class="form-actions" style="margin-top:0.6rem">
<button id="resolveSharingLinksBtn" class="btn btn-outline" type="button">Resolve</button>
</div>
<div id="resolveFeedback" class="feedback" aria-live="polite"></div>
</div>
<div id="resolveGroupsBlock" hidden>
<h3 class="subheading">Resolve SharePoint Groups</h3>
<p class="resolve-hint">
Expand SharePoint groups (Owners / Members / Visitors / custom site groups) to the underlying
user list. When a member is itself a Microsoft 365 / Azure AD group, Clearview recursively
expands it via Microsoft Graph (members + owners, depth 3) — requires
<code>Group.Read.All</code> on Microsoft Graph for that tenant. Without that permission the
M365 group lines stay collapsed. Members are written to the deviation rows and Excel export.
</p>
<div class="form-actions" style="margin-top:0.6rem">
<button id="resolveGroupsBtn" class="btn btn-outline" type="button">Resolve groups</button>
</div>
<div id="resolveGroupsFeedback" class="feedback" aria-live="polite"></div>
</div>
<h3 class="subheading">Permission Deviations</h3>
<div class="table-wrap deviations-wrap">
<table>
<thead id="deviationsTableHead">
<tr>
<th>Site</th>
<th>Object</th>
<th>Type</th>
<th>Principal</th>
<th>Role</th>
<th>Delta</th>
</tr>
</thead>
<tbody id="deviationsTableBody">
<tr><td colspan="6">No deviation data yet.</td></tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- =================================================================== -->
<!-- Route: Settings (placeholder) -->
<!-- =================================================================== -->
<section class="route-page" data-route-page="settings" hidden>
<div class="panel">
<div class="panel-header split">
<h2>Settings</h2>
</div>
<nav class="settings-tabs" role="tablist">
<a href="#/settings/general" class="settings-tab" data-settings-tab="general" role="tab">General</a>
<a href="#/settings/users" class="settings-tab" data-settings-tab="users" data-admin-only role="tab">Users</a>
<a href="#/settings/audit" class="settings-tab" data-settings-tab="audit" data-admin-only role="tab">Audit log</a>
</nav>
<div class="settings-pane" data-settings-pane="general">
<p class="setup-hint">Runtime configuration is currently controlled via environment variables in <code>stack/.env</code>. See the <strong>TECHNICAL.md</strong> document for the full list (timeouts, retries, scan caps, onboarding).</p>
</div>
<div class="settings-pane" data-settings-pane="users" hidden>
<div class="users-toolbar"><button id="newUserBtn" class="btn btn-primary" type="button">New user</button></div>
<div id="usersTable"></div>
</div>
<div class="settings-pane" data-settings-pane="audit" hidden>
<div id="auditTable"></div>
</div>
</div>
</section>
</main>
<script src="app.js"></script>
</body>
</html>