Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
120 commits
Select commit Hold shift + click to select a range
58da138
feat(auth): add Microsoft 365 login + security hardening
sameerk27 Jun 22, 2026
85db60e
feat(auth): role-based access + in-app user management
sameerk27 Jun 23, 2026
9afa1a8
feat(users): invite/resend email for pre-provisioned users
sameerk27 Jun 23, 2026
3b26910
feat(audit): append-only audit trail for security-relevant actions
sameerk27 Jun 23, 2026
c57c24c
feat(security): enforce HTTPS in production + TLS deployment docs
sameerk27 Jun 23, 2026
354ee10
feat(setup): one-shot installer + in-app first-run setup wizard
sameerk27 Jun 23, 2026
cd964f1
fix(install): resolve repo root reliably, ASCII text, correct run gui…
sameerk27 Jun 24, 2026
ff13319
feat(deploy): automated production deploy script
sameerk27 Jun 24, 2026
1db9f57
feat(deploy): register-app.ps1 to script the Entra app registration
sameerk27 Jun 24, 2026
e25249b
feat(docker): cross-platform secrets + Linux/Docker deployment
sameerk27 Jun 24, 2026
748c2c0
docs: add .env.example for Docker deployment
sameerk27 Jun 24, 2026
3a81424
feat(deploy): self-signed cert for internal hostname + fix Production…
sameerk27 Jun 24, 2026
91dcc69
fix(auth): read login identity from AzureAd in /api/auth/config
sameerk27 Jun 24, 2026
1d99767
docs: enterprise backlog + org-readiness plan
sameerk27 Jun 25, 2026
08b61f3
docs: expand backlog (perfect-tabs, alert coverage, gaps) + roadmap g…
sameerk27 Jun 26, 2026
70126e7
docs(readme): lead with one-command install (Docker / deploy.ps1)
sameerk27 Jun 27, 2026
c3cc5b2
Checkpoint before enterprise Overview redesign
sameerk27 Jun 27, 2026
d8bd88f
feat: enterprise refactor + trends, compliance engine, recommendation…
sameerk27 Jul 2, 2026
879b863
feat: ship-gate plumbing — health, audit hardening, retention, role c…
sameerk27 Jul 2, 2026
5f9a667
docs(backlog): mark plumbing ship-gate items done (health, audit, ret…
sameerk27 Jul 2, 2026
796f237
fix(security): enforce RBAC + deny-by-default auth; honest demo data
sameerk27 Jul 2, 2026
6a27a41
feat(ux): alert-first redesign + fix broken pages, fake claims, dead CSS
sameerk27 Jul 3, 2026
a264aee
feat: enterprise hardening + analyst-triage UX (veteran-review pass)
sameerk27 Jul 3, 2026
cc725fb
feat(nav): hash routing + alert permalinks; consolidate 17 pages into…
sameerk27 Jul 3, 2026
e85e09e
fix(overview): dedupe policy fires in Needs Attention; remove fake se…
sameerk27 Jul 3, 2026
73ab241
polish: design-system detail pass (senior-designer audit batch 1)
sameerk27 Jul 4, 2026
fb1da8d
polish: judgment-call batch — login tokenized, theme toasts, ISO coun…
sameerk27 Jul 4, 2026
63610f7
feat(overview): remove Needs Attention card (owner call); honest char…
sameerk27 Jul 4, 2026
8180479
feat+polish: single open alert per policy (server-side) + card UX QA …
sameerk27 Jul 4, 2026
9e346a9
docs(backlog): true up against July 3-4 shipping (RBAC, rate limit, d…
sameerk27 Jul 4, 2026
17517e6
docs: compiled implementation plan (audits + competitive research -> …
sameerk27 Jul 4, 2026
483bfc8
feat(p0): certificate auth for Graph + EF Core migrations with legacy…
sameerk27 Jul 4, 2026
ac51eca
docs(backlog): Phase 0 shipped — gate is now owner secret rotation only
sameerk27 Jul 4, 2026
41a89b2
feat(p1): alert workbench — assign/notes/SLA, queue toolkit, slide-in…
sameerk27 Jul 4, 2026
7ec0cc1
fix(alert-detail): triage above entities; cap and filter the entity list
sameerk27 Jul 4, 2026
06e2961
feat(p2): Ctrl+K global search, sticky tabs, undo toasts, pause refre…
sameerk27 Jul 4, 2026
be4d04c
feat(p3.1): activity-based alerting — alert on WHAT HAPPENED in the t…
sameerk27 Jul 11, 2026
fb1e6cc
fix(collector): null-safe trims for optional audit-event fields
sameerk27 Jul 11, 2026
23f1166
feat(p3.2): anomaly/baseline alerts + Tenant Activity feed page
sameerk27 Jul 17, 2026
d47b736
fix(migrations): regenerate AnomalyAlertPolicies via dotnet-ef
sameerk27 Jul 17, 2026
6aaa529
fix(migrations): preserve anomaly baseline defaults
sameerk27 Jul 17, 2026
e7b05c4
ci: build and test on GitHub Actions
sameerk27 Jul 17, 2026
d53416e
docs: add alert-first product implementation plan
sameerk27 Jul 17, 2026
ea102a6
feat(alerts): show collection health in alert center
sameerk27 Jul 17, 2026
367897c
feat(alerts): flag stale collection data in queue
sameerk27 Jul 17, 2026
66e51a9
feat(alerts): add collection run history
sameerk27 Jul 17, 2026
0c35c01
feat(ops): add rolling structured application logs
sameerk27 Jul 17, 2026
1f06695
docs(ops): add backup restore and upgrade runbook
sameerk27 Jul 17, 2026
8d18e26
feat(alerts): add saved investigation views
sameerk27 Jul 17, 2026
586484f
feat(alerts): open triage queue by default
sameerk27 Jul 17, 2026
6bb20b0
feat(alerts): add evidence timeline to alert detail panel
sameerk27 Jul 17, 2026
862444c
feat(p3.3): report library — scheduled executive digest
sameerk27 Jul 17, 2026
ca4b0ba
feat(p3.4): notification digest mode + delivery-failure alerting
sameerk27 Jul 17, 2026
27f50e6
fix(qa): restore note-adding in triaged alert panel; exclude snoozed …
sameerk27 Jul 18, 2026
aaa7e4d
feat(p4.2): incident <-> alert join
sameerk27 Jul 18, 2026
08a373c
feat(p4.1): entity investigation drill-down page
sameerk27 Jul 18, 2026
3f9dae1
feat(p4.3): Conditional Access gap analysis
sameerk27 Jul 18, 2026
8c4b465
feat(p4.4): SharePoint/OneDrive external-sharing posture
sameerk27 Jul 19, 2026
368a116
refactor(coherence): one audit view, honest posture, single findings hub
sameerk27 Jul 19, 2026
e89df04
chore(qa): strip dead CSS — old modal system, deleted framework cards…
sameerk27 Jul 20, 2026
2ce37e5
fix(m0): seven confirmed bugs from independent fresh-eyes audit
sameerk27 Jul 21, 2026
9ae523b
feat(m1): CSV injection guard, in-app confirms, timezone clarity, CI …
sameerk27 Jul 21, 2026
5f26508
fix(overview): collection 'Details' pointed at Microsoft advisories, …
sameerk27 Jul 21, 2026
6e67bfc
feat(nav): make Collection Health a real drill-down to the runs page
sameerk27 Jul 21, 2026
e8a09c7
fix(overview): banner 'Which source?' opens Collection Runs, not a sc…
sameerk27 Jul 21, 2026
2a46d55
feat(m1): translate Graph failures into actionable permission hints
sameerk27 Jul 22, 2026
6949c11
feat(m1): idle and absolute session timeouts
sameerk27 Jul 22, 2026
5ee70b7
feat(m1): retryable error states and self-updating relative times
sameerk27 Jul 22, 2026
323dbcc
feat(m2): first-run setup checklist
sameerk27 Jul 23, 2026
a6bf077
feat(m2): live in-app Graph permissions reference
sameerk27 Jul 23, 2026
afe1ec0
docs(m2): correct README against what the app actually does
sameerk27 Jul 24, 2026
fa4b309
feat(m2): 'next steps' guidance on the setup wizard
sameerk27 Jul 24, 2026
84cbc5e
feat(m2): contextual per-page help in the header
sameerk27 Jul 25, 2026
add37b8
fix(overview): stop blaming missing setup for transient panel failures
sameerk27 Jul 25, 2026
9b84fad
feat(m3): alert-ops metrics — MTTA, MTTR, resolution rate, analyst wo…
sameerk27 Jul 26, 2026
545fb8b
feat(m3): standing suppression rules — the top alert-fatigue fix
sameerk27 Jul 26, 2026
95da8b3
feat(m3): policy dry-run — replay a policy against stored history
sameerk27 Jul 27, 2026
57d596e
feat(m3): policy export/import as portable JSON packs
sameerk27 Jul 29, 2026
3988f75
feat(m4): formal type scale and compact-density toggle
sameerk27 Jul 29, 2026
80a1f78
feat(m4): PDF digest, API tokens, SIEM export and signed webhooks
sameerk27 Jul 31, 2026
a5bf7ab
test(m5): frontend test infrastructure, deploy smoke test, drop unuse…
sameerk27 Jul 31, 2026
a45c51b
feat(m4): keyboard access for clickable rows, skip link, main landmark
sameerk27 Jul 31, 2026
f2062d3
refactor(m5): split Program.cs into per-domain endpoint modules
sameerk27 Aug 1, 2026
fb78877
refactor(m5): finish the split — Platform module and orphaned-comment…
sameerk27 Aug 1, 2026
dd1e0b5
chore(m5): release machinery — changelog, single-source version, drif…
sameerk27 Aug 1, 2026
8ccab7e
fix(trends): stop the chart claiming a range it is not showing
sameerk27 Aug 1, 2026
edd0173
test(m4): automated accessibility checks in CI
sameerk27 Aug 1, 2026
5c313b6
fix(qa): consistent severity filters, readable CA controls, honest bu…
sameerk27 Aug 1, 2026
600154c
fix(qa): stop the dashboard asserting things its data cannot support
sameerk27 Aug 1, 2026
ccb6043
fix(qa): guard self-demotion, one name for Vigil365's own alerts
sameerk27 Aug 1, 2026
81e714a
fix(qa): dead notification links, and country chips beyond 25 countries
sameerk27 Aug 1, 2026
55bbf53
fix(qa): scope the Email search, and stop exports claiming to be comp…
sameerk27 Aug 1, 2026
41da540
fix(css): restore the 51 dynamic styles the inline-style extraction b…
sameerk27 Aug 3, 2026
56cc8c4
feat(deploy): script for serving Vigil365 on a public hostname
sameerk27 Aug 4, 2026
abe3371
fix(deploy): bind dual-stack, not IPv4-only
sameerk27 Aug 4, 2026
d1c699d
fix: repair type-check and test breakage, and the login page's real-t…
sameerk27 Aug 4, 2026
8430ce2
feat(deploy): helper to trust the self-signed cert locally while testing
sameerk27 Aug 4, 2026
88cec2c
feat(deploy): add request-cert.ps1 for real Let's Encrypt certificates
sameerk27 Aug 4, 2026
2083673
fix(deploy): make DNS-01 survive GoDaddy, and add a TXT preflight
sameerk27 Aug 4, 2026
f0ff271
feat(installer): set up HTTPS, instead of asking the customer to
sameerk27 Aug 4, 2026
207194e
fix(installer): make the install work, and stop asking answerable que…
sameerk27 Aug 4, 2026
57b0b60
feat(installer): ship a self-contained exe instead of building on the…
sameerk27 Aug 4, 2026
cc5bea8
chore: ignore certs/, which holds private key material
sameerk27 Aug 4, 2026
420788b
fix(installer): repair the database SQL, and fail with a way forward
sameerk27 Aug 5, 2026
0f355ed
fix(installer): actually create the service, and notice when it fails
sameerk27 Aug 5, 2026
2878789
fix: let a fresh install actually start, and stop the wizard freezing
sameerk27 Aug 5, 2026
4d96edc
fix(auth): recover from a stranded MSAL sign-in instead of dead-ending
sameerk27 Aug 5, 2026
7690e6c
fix(installer): register in the administrator's tenant, not an ambien…
sameerk27 Aug 5, 2026
d1a5733
chore: ignore installer-bin/, which held committed build output
sameerk27 Aug 5, 2026
bcaa25e
fix(installer): run az login in a window the user can answer
sameerk27 Aug 5, 2026
2e45ccf
Merge master: keep the wizard docs and restore the from-source path
sameerk27 Aug 5, 2026
ee81849
feat(installer): grant Graph permissions and create the collector secret
sameerk27 Aug 5, 2026
963e4f1
fix(deps): patch four High-severity transitive vulnerabilities
sameerk27 Aug 5, 2026
8de8955
fix(graph): survive an unopenable certificate store on Linux
sameerk27 Aug 5, 2026
ec9bab7
fix(installer): do not re-define an existing Entra scope on reuse
sameerk27 Aug 6, 2026
7774046
fix(setup): import EmptyState so the Setup page renders
sameerk27 Aug 6, 2026
3877529
pre-ui-revamp-checkpoint
sameerk27 Aug 7, 2026
598bb4c
feat: digest categorisation and UI polish; fix checkpoint type errors
sameerk27 Aug 7, 2026
b2507ec
docs: consolidate CHANGELOG into 1.0.0 and point README at Releases
sameerk27 Aug 7, 2026
16a6394
feat(brand): one consistent icon — navy shield + V — across tab, app …
sameerk27 Aug 7, 2026
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
13 changes: 13 additions & 0 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "8.0.28",
"commands": [
"dotnet-ef"
],
"rollForward": false
}
}
}
22 changes: 22 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Build outputs and deps — rebuilt inside the image
**/bin
**/obj
**/node_modules
**/dist
publish/

# Local config & secrets — never bake into the image
**/appsettings.Production.json
**/appsettings.*.local.json
.env

# Source control / IDE / docs
.git
.gitignore
.vs
.vscode
*.md
docs/

# Data Protection keys / runtime artifacts
keys/
15 changes: 15 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copy to .env and fill in. Do NOT commit .env.

# SQL Server SA password (8+ chars, must include upper, lower, digit, symbol).
MSSQL_SA_PASSWORD=Change_me_strong_123!

# From your Entra app registration (run register-app.ps1 or see README).
TENANT_ID=00000000-0000-0000-0000-000000000000
CLIENT_ID=00000000-0000-0000-0000-000000000000

# First user to sign in with this email becomes Admin.
ADMIN_EMAIL=you@yourdomain.com

# Must match a SPA redirect URI on the app registration.
# For local Docker, http://localhost:8080 works (Entra allows http://localhost).
REDIRECT_URI=http://localhost:8080
103 changes: 103 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
name: CI

on:
push:
# This repository's default branch is master; listing only main meant
# push-triggered CI never actually ran.
branches: [master, main]
pull_request:

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build-and-test:
name: Build and test
runs-on: ubuntu-latest

steps:
- name: Check out source
uses: actions/checkout@v4

- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: npm
cache-dependency-path: src/m365-security-dashboard-client/package-lock.json

- name: Check API and client versions match
shell: pwsh
run: ./scripts/check-version.ps1

- name: Restore frontend dependencies
working-directory: src/m365-security-dashboard-client
run: npm ci

- name: Type-check frontend
working-directory: src/m365-security-dashboard-client
run: npx tsc --noEmit

- name: Run frontend tests
working-directory: src/m365-security-dashboard-client
run: npm test

- name: Build frontend
working-directory: src/m365-security-dashboard-client
run: npm run build

# Supply-chain gates. A security product should not ship on top of
# known-vulnerable dependencies; fail the build rather than warn.
- name: Audit npm dependencies
working-directory: src/m365-security-dashboard-client
run: npm audit --audit-level=high

- name: Restore .NET dependencies
run: dotnet restore M365SecurityAlertDashboard.sln

# The installer is WPF and only builds on Windows, so it is compiled in the
# separate build-installer job below. Everything shipped to the server —
# the API and its tests — builds here.
- name: Build .NET projects
run: |
dotnet build src/M365SecurityDashboard.Api/M365SecurityDashboard.Api.csproj --configuration Release --no-restore
dotnet build src/M365SecurityDashboard.Api.Tests/M365SecurityDashboard.Api.Tests.csproj --configuration Release --no-restore

- name: Audit NuGet dependencies
run: |
dotnet list M365SecurityAlertDashboard.sln package --vulnerable --include-transitive 2>&1 | tee audit.txt
if grep -q "has the following vulnerable packages" audit.txt; then
echo "::error::Vulnerable NuGet packages detected"; exit 1
fi

# Point at the test project, not the solution: `dotnet test <sln>` evaluates
# every project, and the WPF installer does not resolve on Linux.
- name: Run .NET tests
run: dotnet test src/M365SecurityDashboard.Api.Tests/M365SecurityDashboard.Api.Tests.csproj --configuration Release --no-build --verbosity normal

build-installer:
name: Build installer (Windows)
runs-on: windows-latest
steps:
- name: Check out source
uses: actions/checkout@v4

- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x

# Compiles the WPF installer so a break here fails CI instead of surfacing
# only when someone builds the release. Does not produce the shipping
# single-file exe (that is scripts/build-installer.ps1, run at release time).
- name: Build the installer project
run: dotnet build src/M365SecurityDashboard.GuiInstaller/M365SecurityDashboard.GuiInstaller.csproj --configuration Release
17 changes: 17 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,20 @@ dotnet
*.sqlite
*.mdf
*.ldf
publish/
publish-install-test/
keys/

*.pfx
vigil365.pfx
# Local SQL backups and exported key rings are operational secrets.
backups/

# Installer payload — 50MB build artifact produced by scripts/build-installer.ps1
src/M365SecurityDashboard.GuiInstaller/payload.zip

# ACME account keys and issued certificates — private key material
certs/

# Installer build output — superseded by dist/, which is also ignored
installer-bin/
162 changes: 162 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# Changelog

All notable changes to Vigil365 are recorded here.

The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). The
version lives in exactly two places — the API's `<Version>` and the client's
`package.json` — kept in step by `scripts/set-version.ps1` and enforced in CI by
`scripts/check-version.ps1`.

## [1.0.0] — 2026-08-07

First public release of Vigil365: a self-hosted, **read-only** Microsoft 365
security monitoring dashboard. It collects from Microsoft Graph on a schedule,
evaluates metric / activity / anomaly alert policies, notifies over
Teams / email / webhook, and reports through trends, compliance assessment and an
executive digest — with in-app RBAC over a tamper-evident audit trail. It reports
and recommends, and never changes anything in the tenant.

Distributed as a single self-contained Windows installer (`Vigil365-Setup.exe`)
that carries the application, the web UI and the .NET runtime — the target server
needs no source tree, Node.js or .NET.

### Installation

- **Self-contained setup wizard** — one `Vigil365-Setup.exe` (~120 MB), built at
release time by `scripts/build-installer.ps1`. It checks prerequisites,
registers the Entra application, prepares the database, sets up HTTPS and
installs an auto-starting Windows service. The only external tool is Azure CLI,
used solely for the Entra registration, and installed automatically if missing.
- **Deployment scope choice** — "Just this computer" binds loopback with no
certificate (Entra permits `http://localhost` redirect URIs), or "Other people
on our network" takes a certificate from the Windows store, a `.pfx`, or a
generated self-signed one. `scripts/request-cert.ps1` obtains a real Let's
Encrypt certificate for an internet-reachable host.
- **Automatic Entra provisioning** — the wizard registers the app in the
administrator's own tenant (resolved from their email via OpenID discovery,
not whatever the CLI happened to be signed into), grants the fourteen required
Graph permissions, grants admin consent, and creates the collector's client
secret — so collection works on first run with nothing to configure in the
portal.

### Security

- CSV exports are guarded against spreadsheet formula injection. Alert titles,
display names and audit actors are tenant-controlled, so a value beginning
`=`, `+`, `-` or `@` would execute on open in Excel or Sheets. Applied to all
three exporters.
- Idle (30 min) and absolute (12 h) session timeouts. Idle counts real user
input only — the app's own polling is not evidence anyone is present — and the
session start is held in `sessionStorage` so a refresh cannot reset the cap.
- API tokens for SIEM access: 32 CSPRNG bytes, stored only as a SHA-256 hash
with a short display prefix, plus scopes, expiry, revocation and last-used.
The raw token is shown exactly once, at creation.
- Outbound webhooks are signed Stripe-style — HMAC-SHA256 over
`{timestamp}.{body}`, with the timestamp sent alongside so receivers can
reject replays. The signing secret is encrypted at rest.
- Unknown `/api/*` paths now return `404` JSON instead of `200` HTML from the
SPA fallback, which previously masked broken clients and confused scanners.
- Removed `react-router-dom`. It carried a high-severity advisory
(GHSA-qwww-vcr4-c8h2) and was never imported — the app has its own hash
router. With a `postcss` fix this took the project from three high-severity
advisories to zero.
- CI now fails on vulnerable NuGet or npm packages, and the push trigger was
corrected — it listed only `main`, so push-triggered CI had never run.
- Patched four High-severity transitive advisories surfaced once the full
solution audit ran — `System.Security.Cryptography.Xml`, `System.Formats.Asn1`,
`System.Net.Http` and `System.Text.RegularExpressions` — pinned to fixed
versions across the API, tests and installer.
- CI gained a Windows job that compiles the WPF installer, so a break there fails
the build instead of surfacing only at release time.

### Added

- **Standing suppression rules** — silence known-noisy alert classes at source
rather than acknowledging them repeatedly. Mutations are Admin-only and
audited, because suppressing an alert class is a security decision.
- **Policy dry-run** — replay a policy against stored history before saving it
("would have fired 3 times in 30 days"). Counts *episodes*, not evaluation
cycles, because the evaluator keeps one open alert per policy; and reports
honestly when history cannot answer rather than returning a misleading zero.
- **Alert-ops metrics** — MTTA, MTTR, resolution rate and per-analyst workload,
computed from timestamps the workflow already recorded.
- **Policy export/import** as portable JSON packs. Runtime state never travels,
and notification recipients are stripped by default since packs get shared.
- **Executive digest as PDF**, alongside the existing HTML email and CSV.
- Digest entries now carry each alert's **category, status and assignee** in both
the HTML and CSV, so a digest can be triaged without opening the app.
- **SIEM export** — `/api/siem/alerts` and `/api/siem/health`, authenticated by
scoped API token.
- **First-run setup checklist** and a live **Graph permissions reference**
showing granted/missing status per permission, inferred from the last run.
- **Contextual per-page help** describing what each page shows.
- **Entity investigation** is now reachable from an alert, not only from the
Ctrl+K palette.
- **Compact density toggle** and a formal ten-step type scale.
- Frontend test suite (vitest) and a post-deploy smoke test
(`scripts/smoke-test.ps1`) that verifies a running instance end to end.

### Changed

- Graph failures are translated into instructions. A denied collector source
used to render as raw JSON; it now names the exact permission to grant and
where.
- `Program.cs` split from 2,545 lines into nine per-domain endpoint modules,
leaving 380 lines of host, DI and middleware. Verified by diffing the full
90-endpoint route table, including the authorization on every endpoint.
- Every clickable row is keyboard-accessible, with a skip link and a `<main>`
landmark. Previously the app was mouse-only for its core action — opening an
alert.
- Dashboard panels now distinguish "failed to load this cycle" from "not
configured", instead of telling users to run a collection that had already
succeeded.
- The version shown in the UI is injected from `package.json` at build time
rather than hardcoded, so it cannot claim a version the build is not.
- README corrected against what the app actually does — it had promised a
geographic sign-in map that does not exist, described server-side alerts as
browser storage, claimed every Graph permission was read-only when attack
simulation requires `ReadWrite.All`, and listed several endpoints that had
been renamed or removed.

### Fixed

- Tenant Activity rendered twice (duplicate conditional), causing a double fetch.
- "Tampering detected" — the most serious signal the product emits — displayed
as a green success toast.
- Overview's total and the alert queue disagreed once a tenant passed 200 open
alerts; the queue now states what it is showing.
- Dashboard fetch failures were swallowed while the header still stamped a fresh
"Updated" time over stale cards.
- The collection banner's "Details" link opened Microsoft's service advisories,
which cannot explain a Vigil365 collector failure; it now opens Collection
Runs, where the per-source error is readable.
- Error states offered no retry, and relative timestamps froze at render.

### Fixed — installer and first run

Each of these previously produced an install that reported success and did not
work:

- Registered the app in the wrong tenant (whichever the CLI was signed into)
rather than the administrator's own; now resolved and verified.
- Windows service was never created — `sc` was invoked through `cmd.exe`, which
split the quoted binary path; now invoked directly with the exit code checked
and startup confirmed to reach RUNNING.
- The service account had no SQL login (SQL Express grants sysadmin only to local
administrators), so it could never connect; the login and database are now
created during install.
- DataProtection keys were written under `Program Files`, unwritable by the
service, so the keyring never persisted; moved to `ProgramData` with an ACL.
- A fresh database crashed on first start — `NotificationSettings` / `GraphConfig`
are single-row tables with a fixed key, but the migration made those keys
identity columns; corrected with a migration.
- Re-running the wizard failed to reconfigure an existing app registration
(`CannotDeleteOrUpdateEnabledEntitlement`); the exposed scope is now left
untouched when it already exists.
- Certificate-thumbprint auth threw a cryptic error on Linux (the Docker path)
instead of a clear message when a certificate store could not be opened.
- Sign-in dead-ended with `interaction_in_progress` after an abandoned redirect;
the stale MSAL state is now cleared and retried.
- The Setup page threw `EmptyState is not defined` because the component was used
without importing it — shipped because the build does not type-check.
37 changes: 37 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# syntax=docker/dockerfile:1
# Multi-stage build: compile the React frontend, publish the .NET API, run on Linux.
# Build context is the repo root: docker build -t vigil365 .

# ---- 1. Build the frontend (vite outputs into the API's wwwroot) ----
FROM node:20-alpine AS frontend
WORKDIR /src/m365-security-dashboard-client
COPY src/m365-security-dashboard-client/package*.json ./
RUN npm install --no-audit --no-fund
COPY src/m365-security-dashboard-client/ ./
RUN npm run build
# vite is configured with outDir ../M365SecurityDashboard.Api/wwwroot,
# so the bundle lands at /src/M365SecurityDashboard.Api/wwwroot

# ---- 2. Publish the API, including the built wwwroot ----
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY src/M365SecurityDashboard.Api/ ./M365SecurityDashboard.Api/
COPY --from=frontend /src/M365SecurityDashboard.Api/wwwroot ./M365SecurityDashboard.Api/wwwroot
RUN dotnet publish M365SecurityDashboard.Api/M365SecurityDashboard.Api.csproj -c Release -o /app

# ---- 3. Runtime ----
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app ./

# Container serves HTTP on 8080; TLS is terminated by a reverse proxy (compose/ingress).
ENV ASPNETCORE_URLS=http://+:8080 \
ASPNETCORE_ENVIRONMENT=Production \
Security__RequireHttps=false \
DataProtection__KeyPath=/keys

# Persist the Data Protection key ring so encrypted secrets survive restarts.
VOLUME ["/keys", "/app/logs"]
EXPOSE 8080

ENTRYPOINT ["dotnet", "M365SecurityDashboard.Api.dll"]
Loading
Loading