Wires the Alert Center to the new per-alert snooze endpoints and renders
the new server-side fields (snoozedUntil, lastEvaluatedAt).
- TriggeredAlert interface extended with status union members
('snoozed' | 'auto_resolved') and the four new fields.
- acApi gains snooze(id, durationHours) and unsnooze(id).
- statusTone maps the new statuses to neutral (snoozed) and info
(auto_resolved) tones so they read distinctly from new / acknowledged.
- Status filter dropdown gets two new options so users can isolate
snoozed or auto-resolved rows.
- Action cell gains a snooze <select> with 4h / 24h / 7d presets and an
Unsnooze button that appears when snoozedUntil is in the future.
- Detail modal surfaces Snoozed until and Last evaluated rows when
populated.
- Mini-list and active-alerts table both render a muted 'snoozed until'
indicator so the state is visible at a glance.
Adds M365SecurityDashboard.Api.Tests with seven focused tests for the
new auto-resolve loop in AlertEvaluator:
- Streak increments on a below-threshold observation
- Auto-resolve after AutoResolveDebounceCycles consecutive observations
- Streak resets to zero on any above-threshold observation
- Debounce-of-one auto-resolves on the first observation
- Terminal-state alerts (resolved, auto_resolved) are skipped
- No notification dispatch on auto-resolve
- AcknowledgedAt and AcknowledgedBy survive an auto-resolve transition
Uses Microsoft.EntityFrameworkCore.InMemory for the test DbContext.
NotificationSender is constructed but never invoked by the loop under
test, so no HTTP/DPAPI dependencies are exercised.
Per-alert snooze (4h / 24h / 7d presets) plus silent auto-resolve on metric
recovery. Snooze data lives on TriggeredAlert and composes with the existing
per-policy SuppressionMinutes.
Schema: adds SnoozedUntil, SnoozedBy, BelowThresholdStreakCount, and
LastEvaluatedAt columns to TriggeredAlerts via idempotent IF COL_LENGTH
ALTER blocks, mirroring the existing SourceFailureDetails pattern.
API: POST /api/triggered-alerts/{id}/snooze accepts an absolute timestamp
or a duration in hours; POST .../unsnooze clears the state. Snoozing a
terminal-state alert returns 400.
Evaluator: after the existing dispatch loop, scans non-terminal alerts,
increments or resets a per-alert streak counter on each evaluation, and
transitions to Status="auto_resolved" after AutoResolveDebounceCycles
(default 2) consecutive below-threshold observations. Resolution is silent
- no notification dispatch, no NotificationLog write. Manual resolve
remains the user-driven terminal state; the first transition wins.
Config: new Alerting section in appsettings.json; bound via the existing
IOptions pattern.
sqlcmd without -I causes the filtered index on ExternalId to fail silently,
leaving the schema half-applied. Reported by Firdous Parray.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- SQL connection: Encrypt=True (was Encrypt=False)
- Generic error responses — ex.Message no longer returned to clients;
full error logged server-side via app.Logger.LogError
- Swagger only enabled in Development environment
- CORS restricted to explicit methods (GET POST PUT DELETE OPTIONS)
- Security headers added: X-Frame-Options DENY, X-Content-Type-Options
nosniff, Referrer-Policy no-referrer
- Microsoft.Identity.Web + Azure.Identity packages added (prep for
certificate auth)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The countdown ticker called setRefreshKey() inside the setCountdown()
updater. React state updaters must be pure — side effects inside them
are unreliable, so the countdown reached 0 but the refresh often never
triggered.
Split into two effects: the ticker now only decrements (pure), and a
separate effect fires the refresh when countdown hits 0 (guarded by
!loading to avoid double-fire). load() resets the countdown on completion.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Silent collector failures (caught and only logged) are now visible.
Backend:
- Capture which sources failed and why in CollectionRun.SourceFailureDetails
(JSON), not just a count; idempotent column add for existing DBs
- /api/collector/runs returns the new field
Frontend:
- New "Collection Health" card on Overview: last-run status, alerts
collected, duration, source-failure count, and an expandable list of
failing sources with their error — or "all sources collecting normally"
- Refreshes with the dashboard (and after Run Collection)
Turns the previously-invisible SourceFailures signal into something the
operator can actually see.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mail flow issues collection threw "requires an element of type Object,
but the target element has type Array" because Get(e,"details","url")
traversed into "details", which is an array in serviceAnnouncement/issues.
TryGetProperty throws on non-object elements.
Guard the element kind before each nested TryGetProperty so any path that
hits an array/primitive returns null instead of throwing. Fixes the
recurring 1-source-failure per collection run; mail-flow advisories now
collect (run upserts 83 vs 81).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The identity endpoint hung 90s+ because directoryAudits used
GetCollectionAsync, which follows @odata.nextLink through every page —
walking the whole audit history even though only the latest 10 are
wanted ($top is page size, not a limit). Same issue in sign-in-locations.
- directoryAudits and signIns now use GetSinglePageAsync (latest page only)
- Time-box the identity endpoint best-effort Graph calls to 10s as a
safety net under throttling
- Guard SecretProtector DPAPI calls with OperatingSystem.IsWindows()
to clear CA1416 and run cleanly off-Windows
identity 90s+ -> 3.2s; signin-locations now 2.8s.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix flexbox collapse hiding alert titles in Overview mini-lists
(missing min-width:0 on .mini-row/.mr-user collapsed them to 0 width
in both light and dark mode)
- Establish token-based color system: 3-tier surface elevation, unified
severity/status scales, text scale — all with proper light/dark parity
- Replace scattered hardcoded hex values across CSS and JSX inline styles
with design tokens so both themes stay consistent
- Full dark-mode contrast audit (WCAG AA): badges, KPI tiles/icons,
source badges, tone classes, charts/gauges, form inputs, focus rings
- sevColor() now returns CSS vars so severity colors adapt per theme
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Addresses external review feedback on the project.
- Add SecretProtector (Windows DPAPI, machine scope) and encrypt SMTP
password and Teams/Slack/generic webhook URLs at rest in the database.
Values are decrypted only in memory at send time; SMTP password is
never returned by the API. Legacy plaintext rows are read transparently.
- Rewrite README "Security & Maturity" section: honest beta positioning,
read-only/least-privilege scope, credential handling, and a host
hardening checklist (dedicated low-priv host, BitLocker, no public
exposure, rotation). Notes certificate auth as recommended next step.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Normalize sevColor() input so lowercase backend severities map to
correct colors (were all falling back to grey)
- Add missing .sev-dot.sev-* CSS rules so Recent Alerts severity dots
are visible
- Add suppressionMinutes to AlertPolicy TS interface (was silently dropped)
- Correct misleading "no email sent" label on the policy modal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously the alert engine was browser-only (localStorage) and only ran
while the dashboard was open. It now runs server-side on the 15-minute
collection cycle and actually delivers notifications.
Backend:
- New EF entities: AlertPolicy, TriggeredAlert, NotificationSettings, NotificationLog
- AlertEvaluator computes metrics from SecurityAlerts and fires policies with
per-policy suppression windows to prevent alert fatigue
- NotificationSender delivers to Teams/Slack incoming webhooks, SMTP email,
and a generic JSON webhook (SIEM/Power Automate); every attempt is logged
- Evaluation hooked into GraphCollectionWorker so alerts fire with no browser open
- Idempotent table creation + default policy seeding (works on existing DBs)
- REST endpoints for policies CRUD, triggered alerts ack/resolve, settings, test, log
Frontend:
- Alert Center now reads/writes via the API instead of localStorage
- New Notifications tab: Teams/webhook/SMTP config, send-test, delivery history
- Acknowledge/resolve/policy edits persist to the database
Verified against live tenant: 5 policies fired (MFA 7, risky users 1,
non-compliant 2, high alerts 10, service health 15); dedup and ack confirmed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- SECURITY.md documents endpoint stability per area, how to report
broken Graph endpoints, and links to the MS Graph changelog
- New issue template for broken API reports with structured fields
(page, error, date, endpoint, license tier)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Full-stack Microsoft 365 security monitoring dashboard built with
ASP.NET Core 8 + React 18 + TypeScript. Aggregates security signals
from Microsoft Graph API across Defender XDR, Entra ID Protection,
Intune, Exchange Online, and M365 Compliance into a single
self-hosted dashboard.
Features:
- 13 monitoring pages: Identity, Devices, Email, Incidents, Compliance,
Service Health, Licenses, Conditional Access, Audit Log, Sign-in Locations,
M365 Connectivity, Alert Center, Overview
- Alert Policy Engine with 9 pre-built templates and custom policy builder
- Detail modals with direct M365 portal deep links per item type
- Per-page search, filter, sort, CSV export, saved filter presets
- Dark/light mode, collapsible sidebar, toast notifications
- Responsive layout, sticky filter bars, sortable table columns
All credentials must be supplied via .NET User Secrets (dev) or
appsettings.Production.json (prod) — never committed to source.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>