Reconcile: Merge main to dev - #144
Merged
Merged
Conversation
* UI forensic workflow phases 1–3: authenticated media, drill-down inspector, Evidence page (#48)
* Promote dev to main: testing-channel install docs + publish-testing/release fixes (#43)
* Fix publish-testing.yml: WiX DefineConstants collapses ProductVersion (#38)
publish-testing.yml's build-windows-dev job used
-p:DefineConstants="PublishDir=...;ProductVersion=..." (a single
semicolon-joined value), which collapses to one -d argument to wix.exe
and silently drops ProductVersion, surfacing as WIX0150 (undefined
preprocessor variable). release-installers.yml already avoids this by
passing PublishDir and ProductVersion as separate -p: properties;
apply the same fix here.
Reproduced locally: the broken pattern fails with WIX0150 building
deploy/windows/VideoForensics.Bootstrapper.Wix; the fixed pattern
builds both VideoForensics.msi and VideoForensicsBootstrapper.exe
successfully.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Fix release/testing organize-release-files: nested artifact dirs silently dropped (#39)
Both release-installers.yml's create-release job and publish-testing.yml's
publish-dev-release job used a flat `cp dir/*` to gather downloaded artifacts
into release-files/. actions/upload-artifact preserves subdirectories below
the common ancestor of the paths given to it, and the Windows installer
artifacts' paths (VideoForensics.Installer.Wix/... and
VideoForensics.Bootstrapper.Wix/...) only share deploy/windows/ as an
ancestor, so they land nested (e.g.
windows-installer/VideoForensics.Installer.Wix/bin/Release/VideoForensics.msi)
rather than flat. `cp dir/*` only copies top-level entries and silently
no-ops on directories (errors were swallowed by `2>/dev/null || true`), so
the MSI and Bootstrapper .exe never made it into the release - this is why
v0.1.0's GitHub Release only had the Debian package attached even after the
403 permissions fix. Manually uploaded the missing Windows assets to v0.1.0
to fix it immediately; this change fixes future runs.
Replaced with `find ... -type f -exec cp {} release-files/ \;`, which
copies every file regardless of nesting depth.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Fix publish-testing.yml: publish-dev-release job needs full clone for NBGV (#40)
The publish-dev-release job's dotnet pack steps use Nerdbank.GitVersioning,
which requires full commit history to calculate version height. Its
checkout step used actions/checkout@v4's default shallow clone
(fetch-depth: 1), unlike every other job in this workflow, causing:
Nerdbank.GitVersioning.GitException: Shallow clone lacks the objects
required to calculate version height.
This aborted the job before it ever reached the "Create/Update testing
release" step, so the dev branch's rolling testing release was never
actually being updated with new builds despite build-debian-dev and
build-windows-dev succeeding.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Document Testing channel usage for bootstrap installer scripts (#41)
Add example commands for install.ps1/install.sh Testing channel invocation
to README.md and a new Bootstrap Installer Scripts section to deploy/README.md.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Fix broken iex/-- parameter-passing syntax in installer one-liners (#42)
irm|iex piped through '--' doesn't forward args to the downloaded script
in PowerShell; use the scriptblock-wrap pattern instead, and fix iwr's
usage to reference .Content since it returns a response object, not a string.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Switch license to MIT with a Syncfusion carve-out
Syncfusion's Blazor/MAUI components are proprietary and commercially
licensed, consumed only via NuGet reference and never vendored, so
the MIT grant explicitly excludes them and points to CREDITS.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Replace WiX installer with Inno Setup, add first-run setup wizard and configurable storage paths
Removes the WiX-based bootstrapper/MSI (licensing risk under FireGiant's Open
Source Maintenance Fee for proprietary commercial use) in favor of Inno Setup,
which is free for any use. The new installer supports independent Server/
Desktop component selection, bundled FFmpeg, shortcuts, per-category data
directory configuration with resolved default paths, proper upgrade/uninstall
service lifecycle management, and an uninstall-time prompt to keep, back up,
or delete existing data.
Also adds a first-run /setup wizard so the installing user picks their own
admin username/password instead of a fixed admin/ChangeMe123! seed (which
remains available behind VIDEOFORENSICS_ENABLE_DEFAULT_ADMIN for headless
deployments), a DbSetup CLI tool for provisioning the database ahead of
service start, and a hot-swap script for iterating on the installed service
during development without a full installer round-trip.
Fixes two real bugs found along the way: VideoForensics.Hosting.csproj had a
stale PackageReference to Microsoft.AspNetCore.Http.Abstractions that shadowed
the real shared-framework DLL in self-contained publishes, crashing the
service under the SYSTEM account; and ConfigurationLoader.LoadAndApplyAsync
silently dropped 4 of 6 storage location settings when loading persisted
config.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Add installer admin/network setup, DB repair tooling, and fix a real auth race
Installer additions: optional in-installer SuperAdmin creation (skippable,
falls back to the browser /setup wizard), a 2-way network binding picker
(localhost/LAN) with an explicit warning when LAN access is chosen without
setting up an admin account here, per-path NTFS ACL grants and auto-create
for custom data directories, an opt-in network share for Reports/Media
(off by default, seeds the current installing user into a new
VideoForensicsSuperUser group), a Windows Firewall rule for LAN access, an
Optional Tools Start Menu group, and a 9-language installer-chrome picker.
Fixes a real security gap found during testing: the admin-creation and
network-tier DbSetup invocations were gated behind the separate "Optional
Tools" component instead of "server", so filling in the installer's admin
page silently did nothing unless Optional Tools was also checked - the
browser's /setup wizard then became the only path to claim SuperAdmin,
which is a race any device on the LAN could win once local network access
is enabled. DbSetup is now always bundled with the server, its DB-init/
admin-creation/network-tier calls moved from declarative [Run] entries into
[Code] so failures can be detected and surfaced instead of silently
swallowed, and a proactive warning fires when LAN access is chosen without
an admin account being set up here.
Also fixes a second real bug: ReadConfiguredNetworkTierBeforeHostBuilds
hardcoded the default database path, ignoring any registry-configured
custom Database location - extracted into a testable
NetworkTierConfigReader that correctly uses StorageLocationProvider.
New tooling: VideoForensics.Diagnostics library (extracted from
DbDiagnostics, TDD-covered) backing a new DbRepair CLI that fixes exact
duplicate rows and orphaned records - dry-run by default, requires --apply
plus a typed confirmation, transaction-wrapped. DbSetup gains
--set-network-tier and env-var-based (not CLI-arg) admin account creation.
Registers DbSetup/DbDiagnostics/DbRepair/VideoForensics.Diagnostics(.Tests)
in VideoForensics.sln - they were never added, so prior solution-wide
build/test runs silently skipped them. Bumps Radzen.Blazor, Syncfusion.Blazor.*,
and the MAUI package set to their latest patch versions (full gate).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Add media access tickets and client URL provider for authenticated media
Browser img/video tags can't send a bearer token, so media content will be
served via short-lived (10 min) signed tickets scoped to one media item and
one operator. Adds IMediaAccessTicketService (Data Protection based, mirrors
StepUpAuthService), MediaTicket DTOs and route helper, the client-side
IMediaContentUrlProvider contract, and RemoteMediaContentUrlProvider for
MAUI (batched POST /api/v1/media/tickets, absolute URLs). Server endpoints
follow in the next commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Require auth on media API and serve content via per-item tickets
/api/v1/devices, /media-items and /integrity-records now require bearer
auth. New POST /api/v1/media/tickets issues 10-minute tickets per media
item; GET /api/v1/media/{id}/content accepts either a bearer token or a
?ticket= (for img/video tags), re-checks the ticket's operator is still
active and approved, and records each initial view in the access audit
log (continuation Range requests skipped; audit failure returns 500).
Adds LocalMediaContentUrlProvider for the WebApp's Blazor Server UI,
which attributes tickets to the validated session principal rather than
the browser-stored operator id.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Load event media through ticketed URLs and add Ui.Shared bUnit tests
Events.razor and EventDetailsDialog hardcoded an unversioned
/api/media/{id}/content URL that no server route matched. They now get
ticketed URLs from IMediaContentUrlProvider: thumbnails are resolved in
one batch after events load, and the details dialog fetches a fresh URL
on open since tickets expire. The dialog shows "Media preview
unavailable" when no URL can be issued. Image/video format detection is
consolidated into MediaFormatHelper. Adds VideoForensics.Ui.Shared.Tests
(xUnit v3 + bUnit) as the first test project for the shared UI.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Add LinqPad-style DumpView for drilling into raw forensic data
DumpNodeBuilder turns JSON or any object into a navigable tree (paths,
tabular detection for arrays of objects, expansion of JSON embedded in
string fields such as MetadataJson, depth guard). DumpView renders it
with collapsible nodes, tables for record arrays, search that filters
and auto-expands matches, and copy-as-JSON. Manual expand state is kept
per node path so it survives search filtering.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Phase 0.5: Security event audit logging and visibility (#45)
* Add Phase 0 security hardening: break-glass SuperAdmin, lockout, geo/threat-intel blocking, configurable 2FA
Groundwork for upcoming external auth provider support (Entra/Google/AD)
and a domestic-violence/high-value-target threat model, both of which
demand hardening the existing password-only login path before any new
login surface is added:
- Break-glass primary SuperAdmin: the bootstrap /setup account is now
restricted to loopback-only login, permanently, so it stays reachable
even if every other credential or provider is compromised or misconfigured.
- Account lockout: per-operator failed-attempt tracking with configurable
threshold/duration, timing-safe dummy password verification to prevent
username enumeration, and generic failure responses across every cause.
- Layered IP/geo blocking: admin-managed banned-CIDR list and MaxMind
GeoLite2 country blocking, both fail-open by default (configurable to
fail-closed) so a lookup outage can't itself become a denial of service.
- Configurable two-factor policy: per-role defaults plus per-operator
overrides, enforced by reusing the existing WebAuthn passkey sign-in
endpoints as a second step (via a short-lived correlation token) rather
than inventing a new auth mechanism. Operators with no passkey yet get
a bootstrap grace path instead of being locked out by the new policy.
- SuperAdmin settings UI for lockout policy, 2FA role/operator requirements,
and manual unlock.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Add Phase 0.5: Security event audit logging infrastructure
- Add SecurityEvent entity (Id, OperatorId, EventType enum, Success, IpAddress, OccurredAtUtc, Reason)
- Add SecurityEventConfiguration with indexes (OperatorId+OccurredAtUtc, OccurredAtUtc)
- Add migration 20260924000000_AddPhase05SecurityEvents
- Add ISecurityAuditService interface with Record* and Get* methods for self-service and SuperAdmin queries
- Add SecurityAuditService implementation with write-once logging and async enumerable results
- Integrate audit logging into OperatorAuthEndpoints.LoginPasswordAsync:
- Record login attempt success/failure with IP
- Record account lockout when threshold crossed
- Add test file with 4 xUnit tests (TDD-style stubs for: successful login, failed password, lockout, unlock)
- Register ISecurityAuditService in DI
This phase establishes the security event log that feeds the MCP visibility tool (Phase 0.5)
and the admin settings page (Phase 1+).
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Add SecurityEventTools MCP endpoint for security visibility (Phase 0.5)
- Add SecurityEventTools class extending ForensicsToolBase
- Register get-security-events MCP tool with parameters: limit, offset, operatorId
- Self-service queries return caller's own events; cross-account queries require SuperAdminLocal
- Tool uses ISecurityAuditService.GetOperatorEventsAsync() to stream results
- Register tool in Program.cs MCP Tools section
Note: Stub implementation - self-service HTTP endpoint to follow.
Full authorization and self-service path require HTTP context integration.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Add /api/v1/security-events HTTP endpoint for Phase 0.5 visibility
- POST /api/v1/security-events with limit (1-1000), offset, and optional operatorId
- Self-service: caller queries own events (ReadOnly+ policy)
- Cross-account: SuperAdminLocal policy required (SuperAdmin + Local tier)
- Returns SecurityEventDto array (id, operatorId, eventType, success, ipAddress, occurredAtUtc, reason)
- Error handling: 400 invalid params, 401 unauthenticated, 403 policy failure, 500 service error
- Integrated into Program.cs endpoint registration
This enables both self-service visibility and SuperAdmin audit access to the security event log.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Refine SecurityEventTools MCP tool with proper authorization (Phase 0.5)
- Add IHttpContextAccessor to extract caller identity and role from claims
- Implement both self-service and cross-account authorization paths in tool
- Self-service: any authenticated caller queries their own events
- Cross-account: requires SuperAdmin role; Local tier check deferred to HTTP endpoint
- Proper error handling for missing context, invalid operatorId format, authorization failure
- Log authorization decisions and authorization denials for audit trail
- Register IHttpContextAccessor in Program.cs
This enables the MCP tool to enforce caller-based authorization rules
without requiring explicit tier resolution (which is HTTP-specific).
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Add SecurityEvents.razor component for Phase 0.5 security event UI
Implements two-tab Blazor component for operator security event visibility:
- Self-service tab: operators view their own login/breach/lockout events
- Admin audit tab: SuperAdmins query cross-account events with filtering
Features:
- Syncfusion SfGrid, SfAutoComplete, SfDatePicker integration
- Client-side event type and date range filtering
- CSV export capability
- Toast notifications for user feedback
- Role-based UI visibility (admin tab shown only to SuperAdmins)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Implement Phase 0.5 security audit integration tests
Adds three test methods for ISecurityAuditService integration:
- LoginPasswordAsync_SuccessfulLogin_RecordsAuditEvent: verifies successful login records success event
- LoginPasswordAsync_FailedPassword_RecordsFailureEvent: verifies failed password attempt records failure event with reason
- LoginPasswordAsync_LockedOutAfterThreshold_RecordsLockoutEvent: verifies account lockout after max attempts records lockout event
Includes OperatorAuthEndpointsInvoker helper class using reflection to invoke private endpoint method for testing.
All tests use Moq for mocking and verify audit service calls with expected parameters.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix SecurityEvents.razor compilation errors
Replace non-existent ReadAsAsync<T>() with ReadFromJsonAsync<T>() from System.Net.Http.Json.
The method ReadAsAsync<T>() does not exist in the standard HttpContent API.
Correct method is ReadFromJsonAsync<T>() from System.Net.Http.Json namespace.
- Add @using System.Net.Http.Json directive
- Replace ReadAsAsync calls on lines 389 and 454
Fixes build failure in PR #45.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix build errors: HasName does not exist on IndexBuilder
- SecurityEventConfiguration.cs: replace IndexBuilder.HasName() (removed/never existed in
EF Core's fluent API) with HasDatabaseName(), the correct method for naming an index.
- SecurityAuditService.cs: add [EnumeratorCancellation] to the CancellationToken parameter
on both async-iterator methods (GetOperatorEventsAsync, GetAllEventsAsync) so the
compiler-generated GetAsyncEnumerator forwards the caller's token instead of discarding it.
Fixes build-and-test and CodeQL Analyze failures on PR #45.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix SecurityEventConfiguration: remove explicit index naming
HasDatabaseName/HasName are extension methods defined in
Microsoft.EntityFrameworkCore.Relational, which VideoForensics.Data.Database.csproj
does not reference (it only references the core Microsoft.EntityFrameworkCore
package) - hence CS1061. No other entity configuration in this codebase names
indexes explicitly; EF Core's default naming convention already produces
IX_SecurityEvents_OperatorId_OccurredAtUtc and IX_SecurityEvents_OccurredAtUtc,
which is exactly what the migration expects, so the explicit calls are dropped
rather than adding a new package reference for no behavioral change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix migration compile error and add missing Designer.cs snapshot
- 20260924000000_AddPhase05SecurityEvents.cs: CreateIndex's single-column overload
takes descending as bool[]? not bool; wrap the literal in an array (CS1503).
- Add the migration's missing .Designer.cs file (every other migration has one).
Without it, the migration lacks the [DbContext]/[Migration] attributes and its
point-in-time BuildTargetModel snapshot, which EF Core's migration history and
scaffolding rely on - the hand-written migration was missing this pairing.
- VideoForensicsDbContextModelSnapshot.cs was also missing the SecurityEvent
entity entirely; add it so the running model matches the migration instead of
reporting a phantom pending model change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix SecurityEventsEndpoints: missing using and invalid logger generic arg
- INetworkTierResolver lives in VideoForensics.Hosting, not VideoForensics.WebApp.Auth (CS0246).
- ILogger<SecurityEventsEndpoints> fails to compile because the class is static and static
types cannot be used as type arguments (CS0718). Match the established pattern in this
codebase (see MediaApiEndpoints.cs) and use ILogger<Program> instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix missing using directives in security audit integration tests
Add VideoForensics.WebApp.Api (LoginPasswordRequest), VideoForensics.WebApp.Services
(IBannedIpMatchService, IThreatIntelBlocklistService, IGeoIpLookupService), and
VideoForensics.Providers.Common.Contracts (INotificationDispatcher) - all CS0246
in the previous push. Also drop the now-unused System.Security.Claims and
VideoForensics.WebApp.Auth usings and the CreateAuthenticatedHttpContext helper,
leftover from a fourth test method dropped earlier since DeviceManagementEndpoints.UnlockAsync
doesn't take an ISecurityAuditService parameter.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix pre-existing OperatorAuthEndpointsTests: add ISecurityAuditService mock
LoginPasswordAsync's private-method reflection lookup in this file's own
OperatorAuthEndpointsInvoker used a fixed parameter-type array that no longer
matched the real method after the Phase 0.5 auth integration added an
ISecurityAuditService parameter - GetMethod silently returned null, and all 5
tests calling it threw "Could not find LoginPasswordAsync method" at runtime
(a MethodInfo lookup failure, not a compile error, so the prior CI failures
never surfaced it until the WebApp/WebApp.Tests projects finally compiled).
Add the missing mock and thread it through every call site and the invoker's
reflection signature.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
---------
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Add inspector panel and ForensicGrid for row drill-down
Selecting a row in any ForensicGrid opens it in a right-panel inspector
with Fields (DumpView of the entity), Raw (provider JSON), Related
(links) and Provenance tabs; the inspector clears on navigation.
ForensicGrid standardises paging, sorting, checkbox filtering, column
chooser, search and CSV export across forensic tables. InspectorState is
registered in both the WebApp and MAUI hosts. EventDetailsDialogTests
moves off the obsolete bUnit TestContext.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Add UI forensic workflow plan with phase status
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Add global forensic scope with left-side ScopeRail synced to the URL
ScopeState holds the investigation scope (devices, UTC date window,
search) and serialises it to/from the query string; ScopeUrl merges it
into the current URL while keeping unrelated keys. ScopeRail renders in
the left pane for signed-in operators with device checkboxes, from/to
dates, search, and 24h/7d/30d/Reset quick ranges driven by TimeProvider,
so links, reloads and back/forward preserve the scope. Registered in
both hosts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Move Events page onto global scope, ForensicGrid and inspector
Events now reads devices/date window/search from the left ScopeRail and
reloads when the scope changes (multi-device fan-out, merged newest
first, per-device failures reported without losing other rows). Loading
moves to a testable EventRowLoader; selecting a row opens the inspector
with the event and media entities, raw provider JSON and provenance
(hash, integrity, legal hold). ForensicGrid gains context-menu
passthrough so hold/release/verify/export keep working per row.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Move Query API page onto ForensicGrid, inspector and global scope
Every live provider result (locations, devices, device events) is now a
ForensicGrid whose rows open in the inspector with source call,
parameters and retrieval time as provenance, plus a collapsible
DumpView of the whole response. The device-events window comes from the
ScopeRail; selecting a location or device fills the id inputs for the
next query; device config renders via DumpView. Marks phase 2 complete
in the UI workflow plan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Support media lookup by device/date range and id over the API
GET /api/v1/media-items accepts from/to (with deviceId; 400 otherwise
or when reversed) and GET /api/v1/media-items/{id} returns one item.
RemoteMediaItemRepository implements GetByDeviceAndDateRangeAsync and
GetAsync against them, so MAUI can browse all media in a scope (not
only event-linked media) for the Evidence gallery.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Add unified evidence stream and timeline grouping
EvidenceLoader merges events (via EventRowLoader) with every stored
media file in scope, de-duplicating media already attached to events,
skipping purged media, classifying media-only items as snapshot, video
or file, and keeping other devices' items when one device fails.
EvidenceTimeline groups items by local day then device and computes
prev/next neighbours for the upcoming media viewer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Add full-pane MediaViewer for snapshots and event video
Images zoom (buttons, wheel, keys; 1-8x) and drag-pan when zoomed;
video gets playback speed, frame-accurate stepping from the media's
frame rate (30 fps fallback) and play/pause via a small JS module.
Arrow keys move prev/next, Esc closes. Header shows device, UTC time
and the file's SHA-256. Zoom/pan math and key mapping are pure,
unit-tested helpers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Add Evidence page with timeline, grid and gallery over the scope
/evidence shows every event and stored media file in the current scope
as a day/device timeline, a ForensicGrid, or a gallery. Selecting an
item opens the MediaViewer with the inspector docked, fetching a fresh
ticketed URL per item and ignoring stale responses; view and open item
are kept in the URL for deep links. Evidence is the first nav item.
Marks phase 3 complete in the UI workflow plan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Add mobile-friendly layout for MAUI/narrow viewports (#46)
* Fix WebApp startup crash from missing DI registrations
IBannedIpRangeRepository, ILockoutPolicySettingsRepository, and
ITwoFactorRoleRequirementRepository were never registered in
AddVideoForensicsDatabase(), even though BannedIpMatchService and
OperatorAuthEndpoints consume them directly. This crashed the host at
startup (DI validation failure / "Failure to infer one or more
parameters"), since BackgroundServiceExceptionBehavior is StopHost.
Add the missing TryAddScoped registrations and a test that builds the
full service collection with BuildServiceProvider(validateScopes: true)
so a future missing registration fails a test instead of only failing
at runtime.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Add mobile-friendly layout for MAUI/narrow viewports
MAUI mobile (iOS/Android) and narrow browser windows previously got the
same desktop-first SfSplitter layout as full-size desktop/web, with no
touch-friendly alternative. Add IViewportService (matchMedia-backed,
600px breakpoint) and a MobileLayout with a slide-in nav drawer and a
bottom-sheet settings panel, and swap between MobileLayout/MainLayout
via a new ResponsiveLayout wrapper at the Routes.razor level.
MainLayout itself is unchanged, per CLAUDE.md's layout guidance.
Verified live in-browser: the mobile drawer/settings sheet and the
desktop MainLayout both render correctly at their respective viewport
widths.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Update outdated NuGet packages solution-wide
Moq 4.20.72 -> 4.21.0 (all test projects), Radzen.Blazor 11.4.2 ->
11.4.3 (Ui.Shared), Microsoft.Extensions.Logging.Console 10.0.11 ->
10.0.12 (DbSetup/DbRepair/MigrateMediaPaths). Part of the full gate
required before opening a PR.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Remove stale CI test exclusions (#47)
The three --filter-not-* exclusions in ci.yml's Test step were added
before these tests were made hermetic. Verified against current main
that all three now pass without any special environment:
PathUtilitiesTests (uses a fixed OS-independent sanitization set),
ReportGenerationServiceTests.WriteReportAsync_WithJsonFormat_WritesJsonFile
(mocks IStorageLocationProvider to an isolated temp dir), and the SQLite
ServiceCollectionExtensionsTests test (renamed to
AddVideoForensicsSqlite_ExplicitPath_ResolvesFactory and now uses an
isolated temp path instead of the real /var/lib/videoforensics default).
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Fix full-gate build warnings
Make the RemoteMediaContentUrlProvider cancellation test assert the
forwarded token (CS0219), set Range headers via Append in media endpoint
tests (ASP0019), and suppress CS8002 in the four legacy strong-named
Ring projects that reference unsigned assemblies (strong names are not
validated on .NET).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
---------
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Allow PRs into main only from dev (#49)
* Promote dev to main: testing-channel install docs + publish-testing/release fixes (#43)
* Fix publish-testing.yml: WiX DefineConstants collapses ProductVersion (#38)
publish-testing.yml's build-windows-dev job used
-p:DefineConstants="PublishDir=...;ProductVersion=..." (a single
semicolon-joined value), which collapses to one -d argument to wix.exe
and silently drops ProductVersion, surfacing as WIX0150 (undefined
preprocessor variable). release-installers.yml already avoids this by
passing PublishDir and ProductVersion as separate -p: properties;
apply the same fix here.
Reproduced locally: the broken pattern fails with WIX0150 building
deploy/windows/VideoForensics.Bootstrapper.Wix; the fixed pattern
builds both VideoForensics.msi and VideoForensicsBootstrapper.exe
successfully.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Fix release/testing organize-release-files: nested artifact dirs silently dropped (#39)
Both release-installers.yml's create-release job and publish-testing.yml's
publish-dev-release job used a flat `cp dir/*` to gather downloaded artifacts
into release-files/. actions/upload-artifact preserves subdirectories below
the common ancestor of the paths given to it, and the Windows installer
artifacts' paths (VideoForensics.Installer.Wix/... and
VideoForensics.Bootstrapper.Wix/...) only share deploy/windows/ as an
ancestor, so they land nested (e.g.
windows-installer/VideoForensics.Installer.Wix/bin/Release/VideoForensics.msi)
rather than flat. `cp dir/*` only copies top-level entries and silently
no-ops on directories (errors were swallowed by `2>/dev/null || true`), so
the MSI and Bootstrapper .exe never made it into the release - this is why
v0.1.0's GitHub Release only had the Debian package attached even after the
403 permissions fix. Manually uploaded the missing Windows assets to v0.1.0
to fix it immediately; this change fixes future runs.
Replaced with `find ... -type f -exec cp {} release-files/ \;`, which
copies every file regardless of nesting depth.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Fix publish-testing.yml: publish-dev-release job needs full clone for NBGV (#40)
The publish-dev-release job's dotnet pack steps use Nerdbank.GitVersioning,
which requires full commit history to calculate version height. Its
checkout step used actions/checkout@v4's default shallow clone
(fetch-depth: 1), unlike every other job in this workflow, causing:
Nerdbank.GitVersioning.GitException: Shallow clone lacks the objects
required to calculate version height.
This aborted the job before it ever reached the "Create/Update testing
release" step, so the dev branch's rolling testing release was never
actually being updated with new builds despite build-debian-dev and
build-windows-dev succeeding.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Document Testing channel usage for bootstrap installer scripts (#41)
Add example commands for install.ps1/install.sh Testing channel invocation
to README.md and a new Bootstrap Installer Scripts section to deploy/README.md.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Fix broken iex/-- parameter-passing syntax in installer one-liners (#42)
irm|iex piped through '--' doesn't forward args to the downloaded script
in PowerShell; use the scriptblock-wrap pattern instead, and fix iwr's
usage to reference .Content since it returns a response object, not a string.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Switch license to MIT with a Syncfusion carve-out
Syncfusion's Blazor/MAUI components are proprietary and commercially
licensed, consumed only via NuGet reference and never vendored, so
the MIT grant explicitly excludes them and points to CREDITS.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Replace WiX installer with Inno Setup, add first-run setup wizard and configurable storage paths
Removes the WiX-based bootstrapper/MSI (licensing risk under FireGiant's Open
Source Maintenance Fee for proprietary commercial use) in favor of Inno Setup,
which is free for any use. The new installer supports independent Server/
Desktop component selection, bundled FFmpeg, shortcuts, per-category data
directory configuration with resolved default paths, proper upgrade/uninstall
service lifecycle management, and an uninstall-time prompt to keep, back up,
or delete existing data.
Also adds a first-run /setup wizard so the installing user picks their own
admin username/password instead of a fixed admin/ChangeMe123! seed (which
remains available behind VIDEOFORENSICS_ENABLE_DEFAULT_ADMIN for headless
deployments), a DbSetup CLI tool for provisioning the database ahead of
service start, and a hot-swap script for iterating on the installed service
during development without a full installer round-trip.
Fixes two real bugs found along the way: VideoForensics.Hosting.csproj had a
stale PackageReference to Microsoft.AspNetCore.Http.Abstractions that shadowed
the real shared-framework DLL in self-contained publishes, crashing the
service under the SYSTEM account; and ConfigurationLoader.LoadAndApplyAsync
silently dropped 4 of 6 storage location settings when loading persisted
config.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Add installer admin/network setup, DB repair tooling, and fix a real auth race
Installer additions: optional in-installer SuperAdmin creation (skippable,
falls back to the browser /setup wizard), a 2-way network binding picker
(localhost/LAN) with an explicit warning when LAN access is chosen without
setting up an admin account here, per-path NTFS ACL grants and auto-create
for custom data directories, an opt-in network share for Reports/Media
(off by default, seeds the current installing user into a new
VideoForensicsSuperUser group), a Windows Firewall rule for LAN access, an
Optional Tools Start Menu group, and a 9-language installer-chrome picker.
Fixes a real security gap found during testing: the admin-creation and
network-tier DbSetup invocations were gated behind the separate "Optional
Tools" component instead of "server", so filling in the installer's admin
page silently did nothing unless Optional Tools was also checked - the
browser's /setup wizard then became the only path to claim SuperAdmin,
which is a race any device on the LAN could win once local network access
is enabled. DbSetup is now always bundled with the server, its DB-init/
admin-creation/network-tier calls moved from declarative [Run] entries into
[Code] so failures can be detected and surfaced instead of silently
swallowed, and a proactive warning fires when LAN access is chosen without
an admin account being set up here.
Also fixes a second real bug: ReadConfiguredNetworkTierBeforeHostBuilds
hardcoded the default database path, ignoring any registry-configured
custom Database location - extracted into a testable
NetworkTierConfigReader that correctly uses StorageLocationProvider.
New tooling: VideoForensics.Diagnostics library (extracted from
DbDiagnostics, TDD-covered) backing a new DbRepair CLI that fixes exact
duplicate rows and orphaned records - dry-run by default, requires --apply
plus a typed confirmation, transaction-wrapped. DbSetup gains
--set-network-tier and env-var-based (not CLI-arg) admin account creation.
Registers DbSetup/DbDiagnostics/DbRepair/VideoForensics.Diagnostics(.Tests)
in VideoForensics.sln - they were never added, so prior solution-wide
build/test runs silently skipped them. Bumps Radzen.Blazor, Syncfusion.Blazor.*,
and the MAUI package set to their latest patch versions (full gate).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Phase 0.5: Security event audit logging and visibility (#45)
* Add Phase 0 security hardening: break-glass SuperAdmin, lockout, geo/threat-intel blocking, configurable 2FA
Groundwork for upcoming external auth provider support (Entra/Google/AD)
and a domestic-violence/high-value-target threat model, both of which
demand hardening the existing password-only login path before any new
login surface is added:
- Break-glass primary SuperAdmin: the bootstrap /setup account is now
restricted to loopback-only login, permanently, so it stays reachable
even if every other credential or provider is compromised or misconfigured.
- Account lockout: per-operator failed-attempt tracking with configurable
threshold/duration, timing-safe dummy password verification to prevent
username enumeration, and generic failure responses across every cause.
- Layered IP/geo blocking: admin-managed banned-CIDR list and MaxMind
GeoLite2 country blocking, both fail-open by default (configurable to
fail-closed) so a lookup outage can't itself become a denial of service.
- Configurable two-factor policy: per-role defaults plus per-operator
overrides, enforced by reusing the existing WebAuthn passkey sign-in
endpoints as a second step (via a short-lived correlation token) rather
than inventing a new auth mechanism. Operators with no passkey yet get
a bootstrap grace path instead of being locked out by the new policy.
- SuperAdmin settings UI for lockout policy, 2FA role/operator requirements,
and manual unlock.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Add Phase 0.5: Security event audit logging infrastructure
- Add SecurityEvent entity (Id, OperatorId, EventType enum, Success, IpAddress, OccurredAtUtc, Reason)
- Add SecurityEventConfiguration with indexes (OperatorId+OccurredAtUtc, OccurredAtUtc)
- Add migration 20260924000000_AddPhase05SecurityEvents
- Add ISecurityAuditService interface with Record* and Get* methods for self-service and SuperAdmin queries
- Add SecurityAuditService implementation with write-once logging and async enumerable results
- Integrate audit logging into OperatorAuthEndpoints.LoginPasswordAsync:
- Record login attempt success/failure with IP
- Record account lockout when threshold crossed
- Add test file with 4 xUnit tests (TDD-style stubs for: successful login, failed password, lockout, unlock)
- Register ISecurityAuditService in DI
This phase establishes the security event log that feeds the MCP visibility tool (Phase 0.5)
and the admin settings page (Phase 1+).
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Add SecurityEventTools MCP endpoint for security visibility (Phase 0.5)
- Add SecurityEventTools class extending ForensicsToolBase
- Register get-security-events MCP tool with parameters: limit, offset, operatorId
- Self-service queries return caller's own events; cross-account queries require SuperAdminLocal
- Tool uses ISecurityAuditService.GetOperatorEventsAsync() to stream results
- Register tool in Program.cs MCP Tools section
Note: Stub implementation - self-service HTTP endpoint to follow.
Full authorization and self-service path require HTTP context integration.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Add /api/v1/security-events HTTP endpoint for Phase 0.5 visibility
- POST /api/v1/security-events with limit (1-1000), offset, and optional operatorId
- Self-service: caller queries own events (ReadOnly+ policy)
- Cross-account: SuperAdminLocal policy required (SuperAdmin + Local tier)
- Returns SecurityEventDto array (id, operatorId, eventType, success, ipAddress, occurredAtUtc, reason)
- Error handling: 400 invalid params, 401 unauthenticated, 403 policy failure, 500 service error
- Integrated into Program.cs endpoint registration
This enables both self-service visibility and SuperAdmin audit access to the security event log.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Refine SecurityEventTools MCP tool with proper authorization (Phase 0.5)
- Add IHttpContextAccessor to extract caller identity and role from claims
- Implement both self-service and cross-account authorization paths in tool
- Self-service: any authenticated caller queries their own events
- Cross-account: requires SuperAdmin role; Local tier check deferred to HTTP endpoint
- Proper error handling for missing context, invalid operatorId format, authorization failure
- Log authorization decisions and authorization denials for audit trail
- Register IHttpContextAccessor in Program.cs
This enables the MCP tool to enforce caller-based authorization rules
without requiring explicit tier resolution (which is HTTP-specific).
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Add SecurityEvents.razor component for Phase 0.5 security event UI
Implements two-tab Blazor component for operator security event visibility:
- Self-service tab: operators view their own login/breach/lockout events
- Admin audit tab: SuperAdmins query cross-account events with filtering
Features:
- Syncfusion SfGrid, SfAutoComplete, SfDatePicker integration
- Client-side event type and date range filtering
- CSV export capability
- Toast notifications for user feedback
- Role-based UI visibility (admin tab shown only to SuperAdmins)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Implement Phase 0.5 security audit integration tests
Adds three test methods for ISecurityAuditService integration:
- LoginPasswordAsync_SuccessfulLogin_RecordsAuditEvent: verifies successful login records success event
- LoginPasswordAsync_FailedPassword_RecordsFailureEvent: verifies failed password attempt records failure event with reason
- LoginPasswordAsync_LockedOutAfterThreshold_RecordsLockoutEvent: verifies account lockout after max attempts records lockout event
Includes OperatorAuthEndpointsInvoker helper class using reflection to invoke private endpoint method for testing.
All tests use Moq for mocking and verify audit service calls with expected parameters.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix SecurityEvents.razor compilation errors
Replace non-existent ReadAsAsync<T>() with ReadFromJsonAsync<T>() from System.Net.Http.Json.
The method ReadAsAsync<T>() does not exist in the standard HttpContent API.
Correct method is ReadFromJsonAsync<T>() from System.Net.Http.Json namespace.
- Add @using System.Net.Http.Json directive
- Replace ReadAsAsync calls on lines 389 and 454
Fixes build failure in PR #45.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix build errors: HasName does not exist on IndexBuilder
- SecurityEventConfiguration.cs: replace IndexBuilder.HasName() (removed/never existed in
EF Core's fluent API) with HasDatabaseName(), the correct method for naming an index.
- SecurityAuditService.cs: add [EnumeratorCancellation] to the CancellationToken parameter
on both async-iterator methods (GetOperatorEventsAsync, GetAllEventsAsync) so the
compiler-generated GetAsyncEnumerator forwards the caller's token instead of discarding it.
Fixes build-and-test and CodeQL Analyze failures on PR #45.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix SecurityEventConfiguration: remove explicit index naming
HasDatabaseName/HasName are extension methods defined in
Microsoft.EntityFrameworkCore.Relational, which VideoForensics.Data.Database.csproj
does not reference (it only references the core Microsoft.EntityFrameworkCore
package) - hence CS1061. No other entity configuration in this codebase names
indexes explicitly; EF Core's default naming convention already produces
IX_SecurityEvents_OperatorId_OccurredAtUtc and IX_SecurityEvents_OccurredAtUtc,
which is exactly what the migration expects, so the explicit calls are dropped
rather than adding a new package reference for no behavioral change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix migration compile error and add missing Designer.cs snapshot
- 20260924000000_AddPhase05SecurityEvents.cs: CreateIndex's single-column overload
takes descending as bool[]? not bool; wrap the literal in an array (CS1503).
- Add the migration's missing .Designer.cs file (every other migration has one).
Without it, the migration lacks the [DbContext]/[Migration] attributes and its
point-in-time BuildTargetModel snapshot, which EF Core's migration history and
scaffolding rely on - the hand-written migration was missing this pairing.
- VideoForensicsDbContextModelSnapshot.cs was also missing the SecurityEvent
entity entirely; add it so the running model matches the migration instead of
reporting a phantom pending model change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix SecurityEventsEndpoints: missing using and invalid logger generic arg
- INetworkTierResolver lives in VideoForensics.Hosting, not VideoForensics.WebApp.Auth (CS0246).
- ILogger<SecurityEventsEndpoints> fails to compile because the class is static and static
types cannot be used as type arguments (CS0718). Match the established pattern in this
codebase (see MediaApiEndpoints.cs) and use ILogger<Program> instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix missing using directives in security audit integration tests
Add VideoForensics.WebApp.Api (LoginPasswordRequest), VideoForensics.WebApp.Services
(IBannedIpMatchService, IThreatIntelBlocklistService, IGeoIpLookupService), and
VideoForensics.Providers.Common.Contracts (INotificationDispatcher) - all CS0246
in the previous push. Also drop the now-unused System.Security.Claims and
VideoForensics.WebApp.Auth usings and the CreateAuthenticatedHttpContext helper,
leftover from a fourth test method dropped earlier since DeviceManagementEndpoints.UnlockAsync
doesn't take an ISecurityAuditService parameter.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix pre-existing OperatorAuthEndpointsTests: add ISecurityAuditService mock
LoginPasswordAsync's private-method reflection lookup in this file's own
OperatorAuthEndpointsInvoker used a fixed parameter-type array that no longer
matched the real method after the Phase 0.5 auth integration added an
ISecurityAuditService parameter - GetMethod silently returned null, and all 5
tests calling it threw "Could not find LoginPasswordAsync method" at runtime
(a MethodInfo lookup failure, not a compile error, so the prior CI failures
never surfaced it until the WebApp/WebApp.Tests projects finally compiled).
Add the missing mock and thread it through every call site and the invoker's
reflection signature.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
---------
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Add mobile-friendly layout for MAUI/narrow viewports (#46)
* Fix WebApp startup crash from missing DI registrations
IBannedIpRangeRepository, ILockoutPolicySettingsRepository, and
ITwoFactorRoleRequirementRepository were never registered in
AddVideoForensicsDatabase(), even though BannedIpMatchService and
OperatorAuthEndpoints consume them directly. This crashed the host at
startup (DI validation failure / "Failure to infer one or more
parameters"), since BackgroundServiceExceptionBehavior is StopHost.
Add the missing TryAddScoped registrations and a test that builds the
full service collection with BuildServiceProvider(validateScopes: true)
so a future missing registration fails a test instead of only failing
at runtime.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Add mobile-friendly layout for MAUI/narrow viewports
MAUI mobile (iOS/Android) and narrow browser windows previously got the
same desktop-first SfSplitter layout as full-size desktop/web, with no
touch-friendly alternative. Add IViewportService (matchMedia-backed,
600px breakpoint) and a MobileLayout with a slide-in nav drawer and a
bottom-sheet settings panel, and swap between MobileLayout/MainLayout
via a new ResponsiveLayout wrapper at the Routes.razor level.
MainLayout itself is unchanged, per CLAUDE.md's layout guidance.
Verified live in-browser: the mobile drawer/settings sheet and the
desktop MainLayout both render correctly at their respective viewport
widths.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Update outdated NuGet packages solution-wide
Moq 4.20.72 -> 4.21.0 (all test projects), Radzen.Blazor 11.4.2 ->
11.4.3 (Ui.Shared), Microsoft.Extensions.Logging.Console 10.0.11 ->
10.0.12 (DbSetup/DbRepair/MigrateMediaPaths). Part of the full gate
required before opening a PR.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Remove stale CI test exclusions (#47)
The three --filter-not-* exclusions in ci.yml's Test step were added
before these tests were made hermetic. Verified against current main
that all three now pass without any special environment:
PathUtilitiesTests (uses a fixed OS-independent sanitization set),
ReportGenerationServiceTests.WriteReportAsync_WithJsonFormat_WritesJsonFile
(mocks IStorageLocationProvider to an isolated temp dir), and the SQLite
ServiceCollectionExtensionsTests test (renamed to
AddVideoForensicsSqlite_ExplicitPath_ResolvesFactory and now uses an
isolated temp path instead of the real /var/lib/videoforensics default).
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Allow PRs into main only from dev
Adds a main-source-guard workflow that fails any pull request into main
whose head is not this repository's dev branch, and documents the
feature → dev → main flow in CLAUDE.md. The only-from-dev check must be
made a required status check on main to enforce it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
---------
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* UI forensic workflow phase 4: investigation Cases (#51)
* Promote dev to main: testing-channel install docs + publish-testing/release fixes (#43)
* Fix publish-testing.yml: WiX DefineConstants collapses ProductVersion (#38)
publish-testing.yml's build-windows-dev job used
-p:DefineConstants="PublishDir=...;ProductVersion=..." (a single
semicolon-joined value), which collapses to one -d argument to wix.exe
and silently drops ProductVersion, surfacing as WIX0150 (undefined
preprocessor variable). release-installers.yml already avoids this by
passing PublishDir and ProductVersion as separate -p: properties;
apply the same fix here.
Reproduced locally: the broken pattern fails with WIX0150 building
deploy/windows/VideoForensics.Bootstrapper.Wix; the fixed pattern
builds both VideoForensics.msi and VideoForensicsBootstrapper.exe
successfully.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Fix release/testing organize-release-files: nested artifact dirs silently dropped (#39)
Both release-installers.yml's create-release job and publish-testing.yml's
publish-dev-release job used a flat `cp dir/*` to gather downloaded artifacts
into release-files/. actions/upload-artifact preserves subdirectories below
the common ancestor of the paths given to it, and the Windows installer
artifacts' paths (VideoForensics.Installer.Wix/... and
VideoForensics.Bootstrapper.Wix/...) only share deploy/windows/ as an
ancestor, so they land nested (e.g.
windows-installer/VideoForensics.Installer.Wix/bin/Release/VideoForensics.msi)
rather than flat. `cp dir/*` only copies top-level entries and silently
no-ops on directories (errors were swallowed by `2>/dev/null || true`), so
the MSI and Bootstrapper .exe never made it into the release - this is why
v0.1.0's GitHub Release only had the Debian package attached even after the
403 permissions fix. Manually uploaded the missing Windows assets to v0.1.0
to fix it immediately; this change fixes future runs.
Replaced with `find ... -type f -exec cp {} release-files/ \;`, which
copies every file regardless of nesting depth.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Fix publish-testing.yml: publish-dev-release job needs full clone for NBGV (#40)
The publish-dev-release job's dotnet pack steps use Nerdbank.GitVersioning,
which requires full commit history to calculate version height. Its
checkout step used actions/checkout@v4's default shallow clone
(fetch-depth: 1), unlike every other job in this workflow, causing:
Nerdbank.GitVersioning.GitException: Shallow clone lacks the objects
required to calculate version height.
This aborted the job before it ever reached the "Create/Update testing
release" step, so the dev branch's rolling testing release was never
actually being updated with new builds despite build-debian-dev and
build-windows-dev succeeding.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Document Testing channel usage for bootstrap installer scripts (#41)
Add example commands for install.ps1/install.sh Testing channel invocation
to README.md and a new Bootstrap Installer Scripts section to deploy/README.md.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Fix broken iex/-- parameter-passing syntax in installer one-liners (#42)
irm|iex piped through '--' doesn't forward args to the downloaded script
in PowerShell; use the scriptblock-wrap pattern instead, and fix iwr's
usage to reference .Content since it returns a response object, not a string.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Switch license to MIT with a Syncfusion carve-out
Syncfusion's Blazor/MAUI components are proprietary and commercially
licensed, consumed only via NuGet reference and never vendored, so
the MIT grant explicitly excludes them and points to CREDITS.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Replace WiX installer with Inno Setup, add first-run setup wizard and configurable storage paths
Removes the WiX-based bootstrapper/MSI (licensing risk under FireGiant's Open
Source Maintenance Fee for proprietary commercial use) in favor of Inno Setup,
which is free for any use. The new installer supports independent Server/
Desktop component selection, bundled FFmpeg, shortcuts, per-category data
directory configuration with resolved default paths, proper upgrade/uninstall
service lifecycle management, and an uninstall-time prompt to keep, back up,
or delete existing data.
Also adds a first-run /setup wizard so the installing user picks their own
admin username/password instead of a fixed admin/ChangeMe123! seed (which
remains available behind VIDEOFORENSICS_ENABLE_DEFAULT_ADMIN for headless
deployments), a DbSetup CLI tool for provisioning the database ahead of
service start, and a hot-swap script for iterating on the installed service
during development without a full installer round-trip.
Fixes two real bugs found along the way: VideoForensics.Hosting.csproj had a
stale PackageReference to Microsoft.AspNetCore.Http.Abstractions that shadowed
the real shared-framework DLL in self-contained publishes, crashing the
service under the SYSTEM account; and ConfigurationLoader.LoadAndApplyAsync
silently dropped 4 of 6 storage location settings when loading persisted
config.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Add installer admin/network setup, DB repair tooling, and fix a real auth race
Installer additions: optional in-installer SuperAdmin creation (skippable,
falls back to the browser /setup wizard), a 2-way network binding picker
(localhost/LAN) with an explicit warning when LAN access is chosen without
setting up an admin account here, per-path NTFS ACL grants and auto-create
for custom data directories, an opt-in network share for Reports/Media
(off by default, seeds the current installing user into a new
VideoForensicsSuperUser group), a Windows Firewall rule for LAN access, an
Optional Tools Start Menu group, and a 9-language installer-chrome picker.
Fixes a real security gap found during testing: the admin-creation and
network-tier DbSetup invocations were gated behind the separate "Optional
Tools" component instead of "server", so filling in the installer's admin
page silently did nothing unless Optional Tools was also checked - the
browser's /setup wizard then became the only path to claim SuperAdmin,
which is a race any device on the LAN could win once local network access
is enabled. DbSetup is now always bundled with the server, its DB-init/
admin-creation/network-tier calls moved from declarative [Run] entries into
[Code] so failures can be detected and surfaced instead of silently
swallowed, and a proactive warning fires when LAN access is chosen without
an admin account being set up here.
Also fixes a second real bug: ReadConfiguredNetworkTierBeforeHostBuilds
hardcoded the default database path, ignoring any registry-configured
custom Database location - extracted into a testable
NetworkTierConfigReader that correctly uses StorageLocationProvider.
New tooling: VideoForensics.Diagnostics library (extracted from
DbDiagnostics, TDD-covered) backing a new DbRepair CLI that fixes exact
duplicate rows and orphaned records - dry-run by default, requires --apply
plus a typed confirmation, transaction-wrapped. DbSetup gains
--set-network-tier and env-var-based (not CLI-arg) admin account creation.
Registers DbSetup/DbDiagnostics/DbRepair/VideoForensics.Diagnostics(.Tests)
in VideoForensics.sln - they were never added, so prior solution-wide
build/test runs silently skipped them. Bumps Radzen.Blazor, Syncfusion.Blazor.*,
and the MAUI package set to their latest patch versions (full gate).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Add media access tickets and client URL provider for authenticated media
Browser img/video tags can't send a bearer token, so media content will be
served via short-lived (10 min) signed tickets scoped to one media item and
one operator. Adds IMediaAccessTicketService (Data Protection based, mirrors
StepUpAuthService), MediaTicket DTOs and route helper, the client-side
IMediaContentUrlProvider contract, and RemoteMediaContentUrlProvider for
MAUI (batched POST /api/v1/media/tickets, absolute URLs). Server endpoints
follow in the next commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Require auth on media API and serve content via per-item tickets
/api/v1/devices, /media-items and /integrity-records now require bearer
auth. New POST /api/v1/media/tickets issues 10-minute tickets per media
item; GET /api/v1/media/{id}/content accepts either a bearer token or a
?ticket= (for img/video tags), re-checks the ticket's operator is still
active and approved, and records each initial view in the access audit
log (continuation Range requests skipped; audit failure returns 500).
Adds LocalMediaContentUrlProvider for the WebApp's Blazor Server UI,
which attributes tickets to the validated session principal rather than
the browser-stored operator id.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Load event media through ticketed URLs and add Ui.Shared bUnit tests
Events.razor and EventDetailsDialog hardcoded an unversioned
/api/media/{id}/content URL that no server route matched. They now get
ticketed URLs from IMediaContentUrlProvider: thumbnails are resolved in
one batch after events load, and the details dialog fetches a fresh URL
on open since tickets expire. The dialog shows "Media preview
unavailable" when no URL can be issued. Image/video format detection is
consolidated into MediaFormatHelper. Adds VideoForensics.Ui.Shared.Tests
(xUnit v3 + bUnit) as the first test project for the shared UI.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Add LinqPad-style DumpView for drilling into raw forensic data
DumpNodeBuilder turns JSON or any object into a navigable tree (paths,
tabular detection for arrays of objects, expansion of JSON embedded in
string fields such as MetadataJson, depth guard). DumpView renders it
with collapsible nodes, tables for record arrays, search that filters
and auto-expands matches, and copy-as-JSON. Manual expand state is kept
per node path so it survives search filtering.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Phase 0.5: Security event audit logging and visibility (#45)
* Add Phase 0 security hardening: break-glass SuperAdmin, lockout, geo/threat-intel blocking, configurable 2FA
Groundwork for upcoming external auth provider support (Entra/Google/AD)
and a domestic-violence/high-value-target threat model, both of which
demand hardening the existing password-only login path before any new
login surface is added:
- Break-glass primary SuperAdmin: the bootstrap /setup account is now
restricted to loopback-only login, permanently, so it stays reachable
even if every other credenti…
* UI forensic workflow phases 1–3: authenticated media, drill-down inspector, Evidence page (#48)
* Promote dev to main: testing-channel install docs + publish-testing/release fixes (#43)
* Fix publish-testing.yml: WiX DefineConstants collapses ProductVersion (#38)
publish-testing.yml's build-windows-dev job used
-p:DefineConstants="PublishDir=...;ProductVersion=..." (a single
semicolon-joined value), which collapses to one -d argument to wix.exe
and silently drops ProductVersion, surfacing as WIX0150 (undefined
preprocessor variable). release-installers.yml already avoids this by
passing PublishDir and ProductVersion as separate -p: properties;
apply the same fix here.
Reproduced locally: the broken pattern fails with WIX0150 building
deploy/windows/VideoForensics.Bootstrapper.Wix; the fixed pattern
builds both VideoForensics.msi and VideoForensicsBootstrapper.exe
successfully.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Fix release/testing organize-release-files: nested artifact dirs silently dropped (#39)
Both release-installers.yml's create-release job and publish-testing.yml's
publish-dev-release job used a flat `cp dir/*` to gather downloaded artifacts
into release-files/. actions/upload-artifact preserves subdirectories below
the common ancestor of the paths given to it, and the Windows installer
artifacts' paths (VideoForensics.Installer.Wix/... and
VideoForensics.Bootstrapper.Wix/...) only share deploy/windows/ as an
ancestor, so they land nested (e.g.
windows-installer/VideoForensics.Installer.Wix/bin/Release/VideoForensics.msi)
rather than flat. `cp dir/*` only copies top-level entries and silently
no-ops on directories (errors were swallowed by `2>/dev/null || true`), so
the MSI and Bootstrapper .exe never made it into the release - this is why
v0.1.0's GitHub Release only had the Debian package attached even after the
403 permissions fix. Manually uploaded the missing Windows assets to v0.1.0
to fix it immediately; this change fixes future runs.
Replaced with `find ... -type f -exec cp {} release-files/ \;`, which
copies every file regardless of nesting depth.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Fix publish-testing.yml: publish-dev-release job needs full clone for NBGV (#40)
The publish-dev-release job's dotnet pack steps use Nerdbank.GitVersioning,
which requires full commit history to calculate version height. Its
checkout step used actions/checkout@v4's default shallow clone
(fetch-depth: 1), unlike every other job in this workflow, causing:
Nerdbank.GitVersioning.GitException: Shallow clone lacks the objects
required to calculate version height.
This aborted the job before it ever reached the "Create/Update testing
release" step, so the dev branch's rolling testing release was never
actually being updated with new builds despite build-debian-dev and
build-windows-dev succeeding.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Document Testing channel usage for bootstrap installer scripts (#41)
Add example commands for install.ps1/install.sh Testing channel invocation
to README.md and a new Bootstrap Installer Scripts section to deploy/README.md.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Fix broken iex/-- parameter-passing syntax in installer one-liners (#42)
irm|iex piped through '--' doesn't forward args to the downloaded script
in PowerShell; use the scriptblock-wrap pattern instead, and fix iwr's
usage to reference .Content since it returns a response object, not a string.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Switch license to MIT with a Syncfusion carve-out
Syncfusion's Blazor/MAUI components are proprietary and commercially
licensed, consumed only via NuGet reference and never vendored, so
the MIT grant explicitly excludes them and points to CREDITS.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Replace WiX installer with Inno Setup, add first-run setup wizard and configurable storage paths
Removes the WiX-based bootstrapper/MSI (licensing risk under FireGiant's Open
Source Maintenance Fee for proprietary commercial use) in favor of Inno Setup,
which is free for any use. The new installer supports independent Server/
Desktop component selection, bundled FFmpeg, shortcuts, per-category data
directory configuration with resolved default paths, proper upgrade/uninstall
service lifecycle management, and an uninstall-time prompt to keep, back up,
or delete existing data.
Also adds a first-run /setup wizard so the installing user picks their own
admin username/password instead of a fixed admin/ChangeMe123! seed (which
remains available behind VIDEOFORENSICS_ENABLE_DEFAULT_ADMIN for headless
deployments), a DbSetup CLI tool for provisioning the database ahead of
service start, and a hot-swap script for iterating on the installed service
during development without a full installer round-trip.
Fixes two real bugs found along the way: VideoForensics.Hosting.csproj had a
stale PackageReference to Microsoft.AspNetCore.Http.Abstractions that shadowed
the real shared-framework DLL in self-contained publishes, crashing the
service under the SYSTEM account; and ConfigurationLoader.LoadAndApplyAsync
silently dropped 4 of 6 storage location settings when loading persisted
config.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Add installer admin/network setup, DB repair tooling, and fix a real auth race
Installer additions: optional in-installer SuperAdmin creation (skippable,
falls back to the browser /setup wizard), a 2-way network binding picker
(localhost/LAN) with an explicit warning when LAN access is chosen without
setting up an admin account here, per-path NTFS ACL grants and auto-create
for custom data directories, an opt-in network share for Reports/Media
(off by default, seeds the current installing user into a new
VideoForensicsSuperUser group), a Windows Firewall rule for LAN access, an
Optional Tools Start Menu group, and a 9-language installer-chrome picker.
Fixes a real security gap found during testing: the admin-creation and
network-tier DbSetup invocations were gated behind the separate "Optional
Tools" component instead of "server", so filling in the installer's admin
page silently did nothing unless Optional Tools was also checked - the
browser's /setup wizard then became the only path to claim SuperAdmin,
which is a race any device on the LAN could win once local network access
is enabled. DbSetup is now always bundled with the server, its DB-init/
admin-creation/network-tier calls moved from declarative [Run] entries into
[Code] so failures can be detected and surfaced instead of silently
swallowed, and a proactive warning fires when LAN access is chosen without
an admin account being set up here.
Also fixes a second real bug: ReadConfiguredNetworkTierBeforeHostBuilds
hardcoded the default database path, ignoring any registry-configured
custom Database location - extracted into a testable
NetworkTierConfigReader that correctly uses StorageLocationProvider.
New tooling: VideoForensics.Diagnostics library (extracted from
DbDiagnostics, TDD-covered) backing a new DbRepair CLI that fixes exact
duplicate rows and orphaned records - dry-run by default, requires --apply
plus a typed confirmation, transaction-wrapped. DbSetup gains
--set-network-tier and env-var-based (not CLI-arg) admin account creation.
Registers DbSetup/DbDiagnostics/DbRepair/VideoForensics.Diagnostics(.Tests)
in VideoForensics.sln - they were never added, so prior solution-wide
build/test runs silently skipped them. Bumps Radzen.Blazor, Syncfusion.Blazor.*,
and the MAUI package set to their latest patch versions (full gate).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Add media access tickets and client URL provider for authenticated media
Browser img/video tags can't send a bearer token, so media content will be
served via short-lived (10 min) signed tickets scoped to one media item and
one operator. Adds IMediaAccessTicketService (Data Protection based, mirrors
StepUpAuthService), MediaTicket DTOs and route helper, the client-side
IMediaContentUrlProvider contract, and RemoteMediaContentUrlProvider for
MAUI (batched POST /api/v1/media/tickets, absolute URLs). Server endpoints
follow in the next commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Require auth on media API and serve content via per-item tickets
/api/v1/devices, /media-items and /integrity-records now require bearer
auth. New POST /api/v1/media/tickets issues 10-minute tickets per media
item; GET /api/v1/media/{id}/content accepts either a bearer token or a
?ticket= (for img/video tags), re-checks the ticket's operator is still
active and approved, and records each initial view in the access audit
log (continuation Range requests skipped; audit failure returns 500).
Adds LocalMediaContentUrlProvider for the WebApp's Blazor Server UI,
which attributes tickets to the validated session principal rather than
the browser-stored operator id.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Load event media through ticketed URLs and add Ui.Shared bUnit tests
Events.razor and EventDetailsDialog hardcoded an unversioned
/api/media/{id}/content URL that no server route matched. They now get
ticketed URLs from IMediaContentUrlProvider: thumbnails are resolved in
one batch after events load, and the details dialog fetches a fresh URL
on open since tickets expire. The dialog shows "Media preview
unavailable" when no URL can be issued. Image/video format detection is
consolidated into MediaFormatHelper. Adds VideoForensics.Ui.Shared.Tests
(xUnit v3 + bUnit) as the first test project for the shared UI.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Add LinqPad-style DumpView for drilling into raw forensic data
DumpNodeBuilder turns JSON or any object into a navigable tree (paths,
tabular detection for arrays of objects, expansion of JSON embedded in
string fields such as MetadataJson, depth guard). DumpView renders it
with collapsible nodes, tables for record arrays, search that filters
and auto-expands matches, and copy-as-JSON. Manual expand state is kept
per node path so it survives search filtering.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Phase 0.5: Security event audit logging and visibility (#45)
* Add Phase 0 security hardening: break-glass SuperAdmin, lockout, geo/threat-intel blocking, configurable 2FA
Groundwork for upcoming external auth provider support (Entra/Google/AD)
and a domestic-violence/high-value-target threat model, both of which
demand hardening the existing password-only login path before any new
login surface is added:
- Break-glass primary SuperAdmin: the bootstrap /setup account is now
restricted to loopback-only login, permanently, so it stays reachable
even if every other credential or provider is compromised or misconfigured.
- Account lockout: per-operator failed-attempt tracking with configurable
threshold/duration, timing-safe dummy password verification to prevent
username enumeration, and generic failure responses across every cause.
- Layered IP/geo blocking: admin-managed banned-CIDR list and MaxMind
GeoLite2 country blocking, both fail-open by default (configurable to
fail-closed) so a lookup outage can't itself become a denial of service.
- Configurable two-factor policy: per-role defaults plus per-operator
overrides, enforced by reusing the existing WebAuthn passkey sign-in
endpoints as a second step (via a short-lived correlation token) rather
than inventing a new auth mechanism. Operators with no passkey yet get
a bootstrap grace path instead of being locked out by the new policy.
- SuperAdmin settings UI for lockout policy, 2FA role/operator requirements,
and manual unlock.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Add Phase 0.5: Security event audit logging infrastructure
- Add SecurityEvent entity (Id, OperatorId, EventType enum, Success, IpAddress, OccurredAtUtc, Reason)
- Add SecurityEventConfiguration with indexes (OperatorId+OccurredAtUtc, OccurredAtUtc)
- Add migration 20260924000000_AddPhase05SecurityEvents
- Add ISecurityAuditService interface with Record* and Get* methods for self-service and SuperAdmin queries
- Add SecurityAuditService implementation with write-once logging and async enumerable results
- Integrate audit logging into OperatorAuthEndpoints.LoginPasswordAsync:
- Record login attempt success/failure with IP
- Record account lockout when threshold crossed
- Add test file with 4 xUnit tests (TDD-style stubs for: successful login, failed password, lockout, unlock)
- Register ISecurityAuditService in DI
This phase establishes the security event log that feeds the MCP visibility tool (Phase 0.5)
and the admin settings page (Phase 1+).
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Add SecurityEventTools MCP endpoint for security visibility (Phase 0.5)
- Add SecurityEventTools class extending ForensicsToolBase
- Register get-security-events MCP tool with parameters: limit, offset, operatorId
- Self-service queries return caller's own events; cross-account queries require SuperAdminLocal
- Tool uses ISecurityAuditService.GetOperatorEventsAsync() to stream results
- Register tool in Program.cs MCP Tools section
Note: Stub implementation - self-service HTTP endpoint to follow.
Full authorization and self-service path require HTTP context integration.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Add /api/v1/security-events HTTP endpoint for Phase 0.5 visibility
- POST /api/v1/security-events with limit (1-1000), offset, and optional operatorId
- Self-service: caller queries own events (ReadOnly+ policy)
- Cross-account: SuperAdminLocal policy required (SuperAdmin + Local tier)
- Returns SecurityEventDto array (id, operatorId, eventType, success, ipAddress, occurredAtUtc, reason)
- Error handling: 400 invalid params, 401 unauthenticated, 403 policy failure, 500 service error
- Integrated into Program.cs endpoint registration
This enables both self-service visibility and SuperAdmin audit access to the security event log.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Refine SecurityEventTools MCP tool with proper authorization (Phase 0.5)
- Add IHttpContextAccessor to extract caller identity and role from claims
- Implement both self-service and cross-account authorization paths in tool
- Self-service: any authenticated caller queries their own events
- Cross-account: requires SuperAdmin role; Local tier check deferred to HTTP endpoint
- Proper error handling for missing context, invalid operatorId format, authorization failure
- Log authorization decisions and authorization denials for audit trail
- Register IHttpContextAccessor in Program.cs
This enables the MCP tool to enforce caller-based authorization rules
without requiring explicit tier resolution (which is HTTP-specific).
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Add SecurityEvents.razor component for Phase 0.5 security event UI
Implements two-tab Blazor component for operator security event visibility:
- Self-service tab: operators view their own login/breach/lockout events
- Admin audit tab: SuperAdmins query cross-account events with filtering
Features:
- Syncfusion SfGrid, SfAutoComplete, SfDatePicker integration
- Client-side event type and date range filtering
- CSV export capability
- Toast notifications for user feedback
- Role-based UI visibility (admin tab shown only to SuperAdmins)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Implement Phase 0.5 security audit integration tests
Adds three test methods for ISecurityAuditService integration:
- LoginPasswordAsync_SuccessfulLogin_RecordsAuditEvent: verifies successful login records success event
- LoginPasswordAsync_FailedPassword_RecordsFailureEvent: verifies failed password attempt records failure event with reason
- LoginPasswordAsync_LockedOutAfterThreshold_RecordsLockoutEvent: verifies account lockout after max attempts records lockout event
Includes OperatorAuthEndpointsInvoker helper class using reflection to invoke private endpoint method for testing.
All tests use Moq for mocking and verify audit service calls with expected parameters.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix SecurityEvents.razor compilation errors
Replace non-existent ReadAsAsync<T>() with ReadFromJsonAsync<T>() from System.Net.Http.Json.
The method ReadAsAsync<T>() does not exist in the standard HttpContent API.
Correct method is ReadFromJsonAsync<T>() from System.Net.Http.Json namespace.
- Add @using System.Net.Http.Json directive
- Replace ReadAsAsync calls on lines 389 and 454
Fixes build failure in PR #45.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix build errors: HasName does not exist on IndexBuilder
- SecurityEventConfiguration.cs: replace IndexBuilder.HasName() (removed/never existed in
EF Core's fluent API) with HasDatabaseName(), the correct method for naming an index.
- SecurityAuditService.cs: add [EnumeratorCancellation] to the CancellationToken parameter
on both async-iterator methods (GetOperatorEventsAsync, GetAllEventsAsync) so the
compiler-generated GetAsyncEnumerator forwards the caller's token instead of discarding it.
Fixes build-and-test and CodeQL Analyze failures on PR #45.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix SecurityEventConfiguration: remove explicit index naming
HasDatabaseName/HasName are extension methods defined in
Microsoft.EntityFrameworkCore.Relational, which VideoForensics.Data.Database.csproj
does not reference (it only references the core Microsoft.EntityFrameworkCore
package) - hence CS1061. No other entity configuration in this codebase names
indexes explicitly; EF Core's default naming convention already produces
IX_SecurityEvents_OperatorId_OccurredAtUtc and IX_SecurityEvents_OccurredAtUtc,
which is exactly what the migration expects, so the explicit calls are dropped
rather than adding a new package reference for no behavioral change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix migration compile error and add missing Designer.cs snapshot
- 20260924000000_AddPhase05SecurityEvents.cs: CreateIndex's single-column overload
takes descending as bool[]? not bool; wrap the literal in an array (CS1503).
- Add the migration's missing .Designer.cs file (every other migration has one).
Without it, the migration lacks the [DbContext]/[Migration] attributes and its
point-in-time BuildTargetModel snapshot, which EF Core's migration history and
scaffolding rely on - the hand-written migration was missing this pairing.
- VideoForensicsDbContextModelSnapshot.cs was also missing the SecurityEvent
entity entirely; add it so the running model matches the migration instead of
reporting a phantom pending model change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix SecurityEventsEndpoints: missing using and invalid logger generic arg
- INetworkTierResolver lives in VideoForensics.Hosting, not VideoForensics.WebApp.Auth (CS0246).
- ILogger<SecurityEventsEndpoints> fails to compile because the class is static and static
types cannot be used as type arguments (CS0718). Match the established pattern in this
codebase (see MediaApiEndpoints.cs) and use ILogger<Program> instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix missing using directives in security audit integration tests
Add VideoForensics.WebApp.Api (LoginPasswordRequest), VideoForensics.WebApp.Services
(IBannedIpMatchService, IThreatIntelBlocklistService, IGeoIpLookupService), and
VideoForensics.Providers.Common.Contracts (INotificationDispatcher) - all CS0246
in the previous push. Also drop the now-unused System.Security.Claims and
VideoForensics.WebApp.Auth usings and the CreateAuthenticatedHttpContext helper,
leftover from a fourth test method dropped earlier since DeviceManagementEndpoints.UnlockAsync
doesn't take an ISecurityAuditService parameter.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix pre-existing OperatorAuthEndpointsTests: add ISecurityAuditService mock
LoginPasswordAsync's private-method reflection lookup in this file's own
OperatorAuthEndpointsInvoker used a fixed parameter-type array that no longer
matched the real method after the Phase 0.5 auth integration added an
ISecurityAuditService parameter - GetMethod silently returned null, and all 5
tests calling it threw "Could not find LoginPasswordAsync method" at runtime
(a MethodInfo lookup failure, not a compile error, so the prior CI failures
never surfaced it until the WebApp/WebApp.Tests projects finally compiled).
Add the missing mock and thread it through every call site and the invoker's
reflection signature.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
---------
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Add inspector panel and ForensicGrid for row drill-down
Selecting a row in any ForensicGrid opens it in a right-panel inspector
with Fields (DumpView of the entity), Raw (provider JSON), Related
(links) and Provenance tabs; the inspector clears on navigation.
ForensicGrid standardises paging, sorting, checkbox filtering, column
chooser, search and CSV export across forensic tables. InspectorState is
registered in both the WebApp and MAUI hosts. EventDetailsDialogTests
moves off the obsolete bUnit TestContext.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Add UI forensic workflow plan with phase status
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Add global forensic scope with left-side ScopeRail synced to the URL
ScopeState holds the investigation scope (devices, UTC date window,
search) and serialises it to/from the query string; ScopeUrl merges it
into the current URL while keeping unrelated keys. ScopeRail renders in
the left pane for signed-in operators with device checkboxes, from/to
dates, search, and 24h/7d/30d/Reset quick ranges driven by TimeProvider,
so links, reloads and back/forward preserve the scope. Registered in
both hosts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Move Events page onto global scope, ForensicGrid and inspector
Events now reads devices/date window/search from the left ScopeRail and
reloads when the scope changes (multi-device fan-out, merged newest
first, per-device failures reported without losing other rows). Loading
moves to a testable EventRowLoader; selecting a row opens the inspector
with the event and media entities, raw provider JSON and provenance
(hash, integrity, legal hold). ForensicGrid gains context-menu
passthrough so hold/release/verify/export keep working per row.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Move Query API page onto ForensicGrid, inspector and global scope
Every live provider result (locations, devices, device events) is now a
ForensicGrid whose rows open in the inspector with source call,
parameters and retrieval time as provenance, plus a collapsible
DumpView of the whole response. The device-events window comes from the
ScopeRail; selecting a location or device fills the id inputs for the
next query; device config renders via DumpView. Marks phase 2 complete
in the UI workflow plan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Support media lookup by device/date range and id over the API
GET /api/v1/media-items accepts from/to (with deviceId; 400 otherwise
or when reversed) and GET /api/v1/media-items/{id} returns one item.
RemoteMediaItemRepository implements GetByDeviceAndDateRangeAsync and
GetAsync against them, so MAUI can browse all media in a scope (not
only event-linked media) for the Evidence gallery.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Add unified evidence stream and timeline grouping
EvidenceLoader merges events (via EventRowLoader) with every stored
media file in scope, de-duplicating media already attached to events,
skipping purged media, classifying media-only items as snapshot, video
or file, and keeping other devices' items when one device fails.
EvidenceTimeline groups items by local day then device and computes
prev/next neighbours for the upcoming media viewer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Add full-pane MediaViewer for snapshots and event video
Images zoom (buttons, wheel, keys; 1-8x) and drag-pan when zoomed;
video gets playback speed, frame-accurate stepping from the media's
frame rate (30 fps fallback) and play/pause via a small JS module.
Arrow keys move prev/next, Esc closes. Header shows device, UTC time
and the file's SHA-256. Zoom/pan math and key mapping are pure,
unit-tested helpers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Add Evidence page with timeline, grid and gallery over the scope
/evidence shows every event and stored media file in the current scope
as a day/device timeline, a ForensicGrid, or a gallery. Selecting an
item opens the MediaViewer with the inspector docked, fetching a fresh
ticketed URL per item and ignoring stale responses; view and open item
are kept in the URL for deep links. Evidence is the first nav item.
Marks phase 3 complete in the UI workflow plan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Add mobile-friendly layout for MAUI/narrow viewports (#46)
* Fix WebApp startup crash from missing DI registrations
IBannedIpRangeRepository, ILockoutPolicySettingsRepository, and
ITwoFactorRoleRequirementRepository were never registered in
AddVideoForensicsDatabase(), even though BannedIpMatchService and
OperatorAuthEndpoints consume them directly. This crashed the host at
startup (DI validation failure / "Failure to infer one or more
parameters"), since BackgroundServiceExceptionBehavior is StopHost.
Add the missing TryAddScoped registrations and a test that builds the
full service collection with BuildServiceProvider(validateScopes: true)
so a future missing registration fails a test instead of only failing
at runtime.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Add mobile-friendly layout for MAUI/narrow viewports
MAUI mobile (iOS/Android) and narrow browser windows previously got the
same desktop-first SfSplitter layout as full-size desktop/web, with no
touch-friendly alternative. Add IViewportService (matchMedia-backed,
600px breakpoint) and a MobileLayout with a slide-in nav drawer and a
bottom-sheet settings panel, and swap between MobileLayout/MainLayout
via a new ResponsiveLayout wrapper at the Routes.razor level.
MainLayout itself is unchanged, per CLAUDE.md's layout guidance.
Verified live in-browser: the mobile drawer/settings sheet and the
desktop MainLayout both render correctly at their respective viewport
widths.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Update outdated NuGet packages solution-wide
Moq 4.20.72 -> 4.21.0 (all test projects), Radzen.Blazor 11.4.2 ->
11.4.3 (Ui.Shared), Microsoft.Extensions.Logging.Console 10.0.11 ->
10.0.12 (DbSetup/DbRepair/MigrateMediaPaths). Part of the full gate
required before opening a PR.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Remove stale CI test exclusions (#47)
The three --filter-not-* exclusions in ci.yml's Test step were added
before these tests were made hermetic. Verified against current main
that all three now pass without any special environment:
PathUtilitiesTests (uses a fixed OS-independent sanitization set),
ReportGenerationServiceTests.WriteReportAsync_WithJsonFormat_WritesJsonFile
(mocks IStorageLocationProvider to an isolated temp dir), and the SQLite
ServiceCollectionExtensionsTests test (renamed to
AddVideoForensicsSqlite_ExplicitPath_ResolvesFactory and now uses an
isolated temp path instead of the real /var/lib/videoforensics default).
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Fix full-gate build warnings
Make the RemoteMediaContentUrlProvider cancellation test assert the
forwarded token (CS0219), set Range headers via Append in media endpoint
tests (ASP0019), and suppress CS8002 in the four legacy strong-named
Ring projects that reference unsigned assemblies (strong names are not
validated on .NET).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
---------
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Allow PRs into main only from dev (#49)
* Promote dev to main: testing-channel install docs + publish-testing/release fixes (#43)
* Fix publish-testing.yml: WiX DefineConstants collapses ProductVersion (#38)
publish-testing.yml's build-windows-dev job used
-p:DefineConstants="PublishDir=...;ProductVersion=..." (a single
semicolon-joined value), which collapses to one -d argument to wix.exe
and silently drops ProductVersion, surfacing as WIX0150 (undefined
preprocessor variable). release-installers.yml already avoids this by
passing PublishDir and ProductVersion as separate -p: properties;
apply the same fix here.
Reproduced locally: the broken pattern fails with WIX0150 building
deploy/windows/VideoForensics.Bootstrapper.Wix; the fixed pattern
builds both VideoForensics.msi and VideoForensicsBootstrapper.exe
successfully.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Fix release/testing organize-release-files: nested artifact dirs silently dropped (#39)
Both release-installers.yml's create-release job and publish-testing.yml's
publish-dev-release job used a flat `cp dir/*` to gather downloaded artifacts
into release-files/. actions/upload-artifact preserves subdirectories below
the common ancestor of the paths given to it, and the Windows installer
artifacts' paths (VideoForensics.Installer.Wix/... and
VideoForensics.Bootstrapper.Wix/...) only share deploy/windows/ as an
ancestor, so they land nested (e.g.
windows-installer/VideoForensics.Installer.Wix/bin/Release/VideoForensics.msi)
rather than flat. `cp dir/*` only copies top-level entries and silently
no-ops on directories (errors were swallowed by `2>/dev/null || true`), so
the MSI and Bootstrapper .exe never made it into the release - this is why
v0.1.0's GitHub Release only had the Debian package attached even after the
403 permissions fix. Manually uploaded the missing Windows assets to v0.1.0
to fix it immediately; this change fixes future runs.
Replaced with `find ... -type f -exec cp {} release-files/ \;`, which
copies every file regardless of nesting depth.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Fix publish-testing.yml: publish-dev-release job needs full clone for NBGV (#40)
The publish-dev-release job's dotnet pack steps use Nerdbank.GitVersioning,
which requires full commit history to calculate version height. Its
checkout step used actions/checkout@v4's default shallow clone
(fetch-depth: 1), unlike every other job in this workflow, causing:
Nerdbank.GitVersioning.GitException: Shallow clone lacks the objects
required to calculate version height.
This aborted the job before it ever reached the "Create/Update testing
release" step, so the dev branch's rolling testing release was never
actually being updated with new builds despite build-debian-dev and
build-windows-dev succeeding.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Document Testing channel usage for bootstrap installer scripts (#41)
Add example commands for install.ps1/install.sh Testing channel invocation
to README.md and a new Bootstrap Installer Scripts section to deploy/README.md.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Fix broken iex/-- parameter-passing syntax in installer one-liners (#42)
irm|iex piped through '--' doesn't forward args to the downloaded script
in PowerShell; use the scriptblock-wrap pattern instead, and fix iwr's
usage to reference .Content since it returns a response object, not a string.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Switch license to MIT with a Syncfusion carve-out
Syncfusion's Blazor/MAUI components are proprietary and commercially
licensed, consumed only via NuGet reference and never vendored, so
the MIT grant explicitly excludes them and points to CREDITS.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Replace WiX installer with Inno Setup, add first-run setup wizard and configurable storage paths
Removes the WiX-based bootstrapper/MSI (licensing risk under FireGiant's Open
Source Maintenance Fee for proprietary commercial use) in favor of Inno Setup,
which is free for any use. The new installer supports independent Server/
Desktop component selection, bundled FFmpeg, shortcuts, per-category data
directory configuration with resolved default paths, proper upgrade/uninstall
service lifecycle management, and an uninstall-time prompt to keep, back up,
or delete existing data.
Also adds a first-run /setup wizard so the installing user picks their own
admin username/password instead of a fixed admin/ChangeMe123! seed (which
remains available behind VIDEOFORENSICS_ENABLE_DEFAULT_ADMIN for headless
deployments), a DbSetup CLI tool for provisioning the database ahead of
service start, and a hot-swap script for iterating on the installed service
during development without a full installer round-trip.
Fixes two real bugs found along the way: VideoForensics.Hosting.csproj had a
stale PackageReference to Microsoft.AspNetCore.Http.Abstractions that shadowed
the real shared-framework DLL in self-contained publishes, crashing the
service under the SYSTEM account; and ConfigurationLoader.LoadAndApplyAsync
silently dropped 4 of 6 storage location settings when loading persisted
config.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Add installer admin/network setup, DB repair tooling, and fix a real auth race
Installer additions: optional in-installer SuperAdmin creation (skippable,
falls back to the browser /setup wizard), a 2-way network binding picker
(localhost/LAN) with an explicit warning when LAN access is chosen without
setting up an admin account here, per-path NTFS ACL grants and auto-create
for custom data directories, an opt-in network share for Reports/Media
(off by default, seeds the current installing user into a new
VideoForensicsSuperUser group), a Windows Firewall rule for LAN access, an
Optional Tools Start Menu group, and a 9-language installer-chrome picker.
Fixes a real security gap found during testing: the admin-creation and
network-tier DbSetup invocations were gated behind the separate "Optional
Tools" component instead of "server", so filling in the installer's admin
page silently did nothing unless Optional Tools was also checked - the
browser's /setup wizard then became the only path to claim SuperAdmin,
which is a race any device on the LAN could win once local network access
is enabled. DbSetup is now always bundled with the server, its DB-init/
admin-creation/network-tier calls moved from declarative [Run] entries into
[Code] so failures can be detected and surfaced instead of silently
swallowed, and a proactive warning fires when LAN access is chosen without
an admin account being set up here.
Also fixes a second real bug: ReadConfiguredNetworkTierBeforeHostBuilds
hardcoded the default database path, ignoring any registry-configured
custom Database location - extracted into a testable
NetworkTierConfigReader that correctly uses StorageLocationProvider.
New tooling: VideoForensics.Diagnostics library (extracted from
DbDiagnostics, TDD-covered) backing a new DbRepair CLI that fixes exact
duplicate rows and orphaned records - dry-run by default, requires --apply
plus a typed confirmation, transaction-wrapped. DbSetup gains
--set-network-tier and env-var-based (not CLI-arg) admin account creation.
Registers DbSetup/DbDiagnostics/DbRepair/VideoForensics.Diagnostics(.Tests)
in VideoForensics.sln - they were never added, so prior solution-wide
build/test runs silently skipped them. Bumps Radzen.Blazor, Syncfusion.Blazor.*,
and the MAUI package set to their latest patch versions (full gate).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Phase 0.5: Security event audit logging and visibility (#45)
* Add Phase 0 security hardening: break-glass SuperAdmin, lockout, geo/threat-intel blocking, configurable 2FA
Groundwork for upcoming external auth provider support (Entra/Google/AD)
and a domestic-violence/high-value-target threat model, both of which
demand hardening the existing password-only login path before any new
login surface is added:
- Break-glass primary SuperAdmin: the bootstrap /setup account is now
restricted to loopback-only login, permanently, so it stays reachable
even if every other credential or provider is compromised or misconfigured.
- Account lockout: per-operator failed-attempt tracking with configurable
threshold/duration, timing-safe dummy password verification to prevent
username enumeration, and generic failure responses across every cause.
- Layered IP/geo blocking: admin-managed banned-CIDR list and MaxMind
GeoLite2 country blocking, both fail-open by default (configurable to
fail-closed) so a lookup outage can't itself become a denial of service.
- Configurable two-factor policy: per-role defaults plus per-operator
overrides, enforced by reusing the existing WebAuthn passkey sign-in
endpoints as a second step (via a short-lived correlation token) rather
than inventing a new auth mechanism. Operators with no passkey yet get
a bootstrap grace path instead of being locked out by the new policy.
- SuperAdmin settings UI for lockout policy, 2FA role/operator requirements,
and manual unlock.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Add Phase 0.5: Security event audit logging infrastructure
- Add SecurityEvent entity (Id, OperatorId, EventType enum, Success, IpAddress, OccurredAtUtc, Reason)
- Add SecurityEventConfiguration with indexes (OperatorId+OccurredAtUtc, OccurredAtUtc)
- Add migration 20260924000000_AddPhase05SecurityEvents
- Add ISecurityAuditService interface with Record* and Get* methods for self-service and SuperAdmin queries
- Add SecurityAuditService implementation with write-once logging and async enumerable results
- Integrate audit logging into OperatorAuthEndpoints.LoginPasswordAsync:
- Record login attempt success/failure with IP
- Record account lockout when threshold crossed
- Add test file with 4 xUnit tests (TDD-style stubs for: successful login, failed password, lockout, unlock)
- Register ISecurityAuditService in DI
This phase establishes the security event log that feeds the MCP visibility tool (Phase 0.5)
and the admin settings page (Phase 1+).
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Add SecurityEventTools MCP endpoint for security visibility (Phase 0.5)
- Add SecurityEventTools class extending ForensicsToolBase
- Register get-security-events MCP tool with parameters: limit, offset, operatorId
- Self-service queries return caller's own events; cross-account queries require SuperAdminLocal
- Tool uses ISecurityAuditService.GetOperatorEventsAsync() to stream results
- Register tool in Program.cs MCP Tools section
Note: Stub implementation - self-service HTTP endpoint to follow.
Full authorization and self-service path require HTTP context integration.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Add /api/v1/security-events HTTP endpoint for Phase 0.5 visibility
- POST /api/v1/security-events with limit (1-1000), offset, and optional operatorId
- Self-service: caller queries own events (ReadOnly+ policy)
- Cross-account: SuperAdminLocal policy required (SuperAdmin + Local tier)
- Returns SecurityEventDto array (id, operatorId, eventType, success, ipAddress, occurredAtUtc, reason)
- Error handling: 400 invalid params, 401 unauthenticated, 403 policy failure, 500 service error
- Integrated into Program.cs endpoint registration
This enables both self-service visibility and SuperAdmin audit access to the security event log.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Refine SecurityEventTools MCP tool with proper authorization (Phase 0.5)
- Add IHttpContextAccessor to extract caller identity and role from claims
- Implement both self-service and cross-account authorization paths in tool
- Self-service: any authenticated caller queries their own events
- Cross-account: requires SuperAdmin role; Local tier check deferred to HTTP endpoint
- Proper error handling for missing context, invalid operatorId format, authorization failure
- Log authorization decisions and authorization denials for audit trail
- Register IHttpContextAccessor in Program.cs
This enables the MCP tool to enforce caller-based authorization rules
without requiring explicit tier resolution (which is HTTP-specific).
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Add SecurityEvents.razor component for Phase 0.5 security event UI
Implements two-tab Blazor component for operator security event visibility:
- Self-service tab: operators view their own login/breach/lockout events
- Admin audit tab: SuperAdmins query cross-account events with filtering
Features:
- Syncfusion SfGrid, SfAutoComplete, SfDatePicker integration
- Client-side event type and date range filtering
- CSV export capability
- Toast notifications for user feedback
- Role-based UI visibility (admin tab shown only to SuperAdmins)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Implement Phase 0.5 security audit integration tests
Adds three test methods for ISecurityAuditService integration:
- LoginPasswordAsync_SuccessfulLogin_RecordsAuditEvent: verifies successful login records success event
- LoginPasswordAsync_FailedPassword_RecordsFailureEvent: verifies failed password attempt records failure event with reason
- LoginPasswordAsync_LockedOutAfterThreshold_RecordsLockoutEvent: verifies account lockout after max attempts records lockout event
Includes OperatorAuthEndpointsInvoker helper class using reflection to invoke private endpoint method for testing.
All tests use Moq for mocking and verify audit service calls with expected parameters.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix SecurityEvents.razor compilation errors
Replace non-existent ReadAsAsync<T>() with ReadFromJsonAsync<T>() from System.Net.Http.Json.
The method ReadAsAsync<T>() does not exist in the standard HttpContent API.
Correct method is ReadFromJsonAsync<T>() from System.Net.Http.Json namespace.
- Add @using System.Net.Http.Json directive
- Replace ReadAsAsync calls on lines 389 and 454
Fixes build failure in PR #45.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix build errors: HasName does not exist on IndexBuilder
- SecurityEventConfiguration.cs: replace IndexBuilder.HasName() (removed/never existed in
EF Core's fluent API) with HasDatabaseName(), the correct method for naming an index.
- SecurityAuditService.cs: add [EnumeratorCancellation] to the CancellationToken parameter
on both async-iterator methods (GetOperatorEventsAsync, GetAllEventsAsync) so the
compiler-generated GetAsyncEnumerator forwards the caller's token instead of discarding it.
Fixes build-and-test and CodeQL Analyze failures on PR #45.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix SecurityEventConfiguration: remove explicit index naming
HasDatabaseName/HasName are extension methods defined in
Microsoft.EntityFrameworkCore.Relational, which VideoForensics.Data.Database.csproj
does not reference (it only references the core Microsoft.EntityFrameworkCore
package) - hence CS1061. No other entity configuration in this codebase names
indexes explicitly; EF Core's default naming convention already produces
IX_SecurityEvents_OperatorId_OccurredAtUtc and IX_SecurityEvents_OccurredAtUtc,
which is exactly what the migration expects, so the explicit calls are dropped
rather than adding a new package reference for no behavioral change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix migration compile error and add missing Designer.cs snapshot
- 20260924000000_AddPhase05SecurityEvents.cs: CreateIndex's single-column overload
takes descending as bool[]? not bool; wrap the literal in an array (CS1503).
- Add the migration's missing .Designer.cs file (every other migration has one).
Without it, the migration lacks the [DbContext]/[Migration] attributes and its
point-in-time BuildTargetModel snapshot, which EF Core's migration history and
scaffolding rely on - the hand-written migration was missing this pairing.
- VideoForensicsDbContextModelSnapshot.cs was also missing the SecurityEvent
entity entirely; add it so the running model matches the migration instead of
reporting a phantom pending model change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix SecurityEventsEndpoints: missing using and invalid logger generic arg
- INetworkTierResolver lives in VideoForensics.Hosting, not VideoForensics.WebApp.Auth (CS0246).
- ILogger<SecurityEventsEndpoints> fails to compile because the class is static and static
types cannot be used as type arguments (CS0718). Match the established pattern in this
codebase (see MediaApiEndpoints.cs) and use ILogger<Program> instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix missing using directives in security audit integration tests
Add VideoForensics.WebApp.Api (LoginPasswordRequest), VideoForensics.WebApp.Services
(IBannedIpMatchService, IThreatIntelBlocklistService, IGeoIpLookupService), and
VideoForensics.Providers.Common.Contracts (INotificationDispatcher) - all CS0246
in the previous push. Also drop the now-unused System.Security.Claims and
VideoForensics.WebApp.Auth usings and the CreateAuthenticatedHttpContext helper,
leftover from a fourth test method dropped earlier since DeviceManagementEndpoints.UnlockAsync
doesn't take an ISecurityAuditService parameter.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix pre-existing OperatorAuthEndpointsTests: add ISecurityAuditService mock
LoginPasswordAsync's private-method reflection lookup in this file's own
OperatorAuthEndpointsInvoker used a fixed parameter-type array that no longer
matched the real method after the Phase 0.5 auth integration added an
ISecurityAuditService parameter - GetMethod silently returned null, and all 5
tests calling it threw "Could not find LoginPasswordAsync method" at runtime
(a MethodInfo lookup failure, not a compile error, so the prior CI failures
never surfaced it until the WebApp/WebApp.Tests projects finally compiled).
Add the missing mock and thread it through every call site and the invoker's
reflection signature.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
---------
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Add mobile-friendly layout for MAUI/narrow viewports (#46)
* Fix WebApp startup crash from missing DI registrations
IBannedIpRangeRepository, ILockoutPolicySettingsRepository, and
ITwoFactorRoleRequirementRepository were never registered in
AddVideoForensicsDatabase(), even though BannedIpMatchService and
OperatorAuthEndpoints consume them directly. This crashed the host at
startup (DI validation failure / "Failure to infer one or more
parameters"), since BackgroundServiceExceptionBehavior is StopHost.
Add the missing TryAddScoped registrations and a test that builds the
full service collection with BuildServiceProvider(validateScopes: true)
so a future missing registration fails a test instead of only failing
at runtime.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Add mobile-friendly layout for MAUI/narrow viewports
MAUI mobile (iOS/Android) and narrow browser windows previously got the
same desktop-first SfSplitter layout as full-size desktop/web, with no
touch-friendly alternative. Add IViewportService (matchMedia-backed,
600px breakpoint) and a MobileLayout with a slide-in nav drawer and a
bottom-sheet settings panel, and swap between MobileLayout/MainLayout
via a new ResponsiveLayout wrapper at the Routes.razor level.
MainLayout itself is unchanged, per CLAUDE.md's layout guidance.
Verified live in-browser: the mobile drawer/settings sheet and the
desktop MainLayout both render correctly at their respective viewport
widths.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Update outdated NuGet packages solution-wide
Moq 4.20.72 -> 4.21.0 (all test projects), Radzen.Blazor 11.4.2 ->
11.4.3 (Ui.Shared), Microsoft.Extensions.Logging.Console 10.0.11 ->
10.0.12 (DbSetup/DbRepair/MigrateMediaPaths). Part of the full gate
required before opening a PR.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Remove stale CI test exclusions (#47)
The three --filter-not-* exclusions in ci.yml's Test step were added
before these tests were made hermetic. Verified against current main
that all three now pass without any special environment:
PathUtilitiesTests (uses a fixed OS-independent sanitization set),
ReportGenerationServiceTests.WriteReportAsync_WithJsonFormat_WritesJsonFile
(mocks IStorageLocationProvider to an isolated temp dir), and the SQLite
ServiceCollectionExtensionsTests test (renamed to
AddVideoForensicsSqlite_ExplicitPath_ResolvesFactory and now uses an
isolated temp path instead of the real /var/lib/videoforensics default).
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Allow PRs into main only from dev
Adds a main-source-guard workflow that fails any pull request into main
whose head is not this repository's dev branch, and documents the
feature → dev → main flow in CLAUDE.md. The only-from-dev check must be
made a required status check on main to enforce it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
---------
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* UI forensic workflow phase 4: investigation Cases (#51)
* Promote dev to main: testing-channel install docs + publish-testing/release fixes (#43)
* Fix publish-testing.yml: WiX DefineConstants collapses ProductVersion (#38)
publish-testing.yml's build-windows-dev job used
-p:DefineConstants="PublishDir=...;ProductVersion=..." (a single
semicolon-joined value), which collapses to one -d argument to wix.exe
and silently drops ProductVersion, surfacing as WIX0150 (undefined
preprocessor variable). release-installers.yml already avoids this by
passing PublishDir and ProductVersion as separate -p: properties;
apply the same fix here.
Reproduced locally: the broken pattern fails with WIX0150 building
deploy/windows/VideoForensics.Bootstrapper.Wix; the fixed pattern
builds both VideoForensics.msi and VideoForensicsBootstrapper.exe
successfully.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Fix release/testing organize-release-files: nested artifact dirs silently dropped (#39)
Both release-installers.yml's create-release job and publish-testing.yml's
publish-dev-release job used a flat `cp dir/*` to gather downloaded artifacts
into release-files/. actions/upload-artifact preserves subdirectories below
the common ancestor of the paths given to it, and the Windows installer
artifacts' paths (VideoForensics.Installer.Wix/... and
VideoForensics.Bootstrapper.Wix/...) only share deploy/windows/ as an
ancestor, so they land nested (e.g.
windows-installer/VideoForensics.Installer.Wix/bin/Release/VideoForensics.msi)
rather than flat. `cp dir/*` only copies top-level entries and silently
no-ops on directories (errors were swallowed by `2>/dev/null || true`), so
the MSI and Bootstrapper .exe never made it into the release - this is why
v0.1.0's GitHub Release only had the Debian package attached even after the
403 permissions fix. Manually uploaded the missing Windows assets to v0.1.0
to fix it immediately; this change fixes future runs.
Replaced with `find ... -type f -exec cp {} release-files/ \;`, which
copies every file regardless of nesting depth.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Fix publish-testing.yml: publish-dev-release job needs full clone for NBGV (#40)
The publish-dev-release job's dotnet pack steps use Nerdbank.GitVersioning,
which requires full commit history to calculate version height. Its
checkout step used actions/checkout@v4's default shallow clone
(fetch-depth: 1), unlike every other job in this workflow, causing:
Nerdbank.GitVersioning.GitException: Shallow clone lacks the objects
required to calculate version height.
This aborted the job before it ever reached the "Create/Update testing
release" step, so the dev branch's rolling testing release was never
actually being updated with new builds despite build-debian-dev and
build-windows-dev succeeding.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Document Testing channel usage for bootstrap installer scripts (#41)
Add example commands for install.ps1/install.sh Testing channel invocation
to README.md and a new Bootstrap Installer Scripts section to deploy/README.md.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Fix broken iex/-- parameter-passing syntax in installer one-liners (#42)
irm|iex piped through '--' doesn't forward args to the downloaded script
in PowerShell; use the scriptblock-wrap pattern instead, and fix iwr's
usage to reference .Content since it returns a response object, not a string.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Switch license to MIT with a Syncfusion carve-out
Syncfusion's Blazor/MAUI components are proprietary and commercially
licensed, consumed only via NuGet reference and never vendored, so
the MIT grant explicitly excludes them and points to CREDITS.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Replace WiX installer with Inno Setup, add first-run setup wizard and configurable storage paths
Removes the WiX-based bootstrapper/MSI (licensing risk under FireGiant's Open
Source Maintenance Fee for proprietary commercial use) in favor of Inno Setup,
which is free for any use. The new installer supports independent Server/
Desktop component selection, bundled FFmpeg, shortcuts, per-category data
directory configuration with resolved default paths, proper upgrade/uninstall
service lifecycle management, and an uninstall-time prompt to keep, back up,
or delete existing data.
Also adds a first-run /setup wizard so the installing user picks their own
admin username/password instead of a fixed admin/ChangeMe123! seed (which
remains available behind VIDEOFORENSICS_ENABLE_DEFAULT_ADMIN for headless
deployments), a DbSetup CLI tool for provisioning the database ahead of
service start, and a hot-swap script for iterating on the installed service
during development without a full installer round-trip.
Fixes two real bugs found along the way: VideoForensics.Hosting.csproj had a
stale PackageReference to Microsoft.AspNetCore.Http.Abstractions that shadowed
the real shared-framework DLL in self-contained publishes, crashing the
service under the SYSTEM account; and ConfigurationLoader.LoadAndApplyAsync
silently dropped 4 of 6 storage location settings when loading persisted
config.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Add installer admin/network setup, DB repair tooling, and fix a real auth race
Installer additions: optional in-installer SuperAdmin creation (skippable,
falls back to the browser /setup wizard), a 2-way network binding picker
(localhost/LAN) with an explicit warning when LAN access is chosen without
setting up an admin account here, per-path NTFS ACL grants and auto-create
for custom data directories, an opt-in network share for Reports/Media
(off by default, seeds the current installing user into a new
VideoForensicsSuperUser group), a Windows Firewall rule for LAN access, an
Optional Tools Start Menu group, and a 9-language installer-chrome picker.
Fixes a real security gap found during testing: the admin-creation and
network-tier DbSetup invocations were gated behind the separate "Optional
Tools" component instead of "server", so filling in the installer's admin
page silently did nothing unless Optional Tools was also checked - the
browser's /setup wizard then became the only path to claim SuperAdmin,
which is a race any device on the LAN could win once local network access
is enabled. DbSetup is now always bundled with the server, its DB-init/
admin-creation/network-tier calls moved from declarative [Run] entries into
[Code] so failures can be detected and surfaced instead of silently
swallowed, and a proactive warning fires when LAN access is chosen without
an admin account being set up here.
Also fixes a second real bug: ReadConfiguredNetworkTierBeforeHostBuilds
hardcoded the default database path, ignoring any registry-configured
custom Database location - extracted into a testable
NetworkTierConfigReader that correctly uses StorageLocationProvider.
New tooling: VideoForensics.Diagnostics library (extracted from
DbDiagnostics, TDD-covered) backing a new DbRepair CLI that fixes exact
duplicate rows and orphaned records - dry-run by default, requires --apply
plus a typed confirmation, transaction-wrapped. DbSetup gains
--set-network-tier and env-var-based (not CLI-arg) admin account creation.
Registers DbSetup/DbDiagnostics/DbRepair/VideoForensics.Diagnostics(.Tests)
in VideoForensics.sln - they were never added, so prior solution-wide
build/test runs silently skipped them. Bumps Radzen.Blazor, Syncfusion.Blazor.*,
and the MAUI package set to their latest patch versions (full gate).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Add media access tickets and client URL provider for authenticated media
Browser img/video tags can't send a bearer token, so media content will be
served via short-lived (10 min) signed tickets scoped to one media item and
one operator. Adds IMediaAccessTicketService (Data Protection based, mirrors
StepUpAuthService), MediaTicket DTOs and route helper, the client-side
IMediaContentUrlProvider contract, and RemoteMediaContentUrlProvider for
MAUI (batched POST /api/v1/media/tickets, absolute URLs). Server endpoints
follow in the next commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Require auth on media API and serve content via per-item tickets
/api/v1/devices, /media-items and /integrity-records now require bearer
auth. New POST /api/v1/media/tickets issues 10-minute tickets per media
item; GET /api/v1/media/{id}/content accepts either a bearer token or a
?ticket= (for img/video tags), re-checks the ticket's operator is still
active and approved, and records each initial view in the access audit
log (continuation Range requests skipped; audit failure returns 500).
Adds LocalMediaContentUrlProvider for the WebApp's Blazor Server UI,
which attributes tickets to the validated session principal rather than
the browser-stored operator id.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Load event media through ticketed URLs and add Ui.Shared bUnit tests
Events.razor and EventDetailsDialog hardcoded an unversioned
/api/media/{id}/content URL that no server route matched. They now get
ticketed URLs from IMediaContentUrlProvider: thumbnails are resolved in
one batch after events load, and the details dialog fetches a fresh URL
on open since tickets expire. The dialog shows "Media preview
unavailable" when no URL can be issued. Image/video format detection is
consolidated into MediaFormatHelper. Adds VideoForensics.Ui.Shared.Tests
(xUnit v3 + bUnit) as the first test project for the shared UI.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Add LinqPad-style DumpView for drilling into raw forensic data
DumpNodeBuilder turns JSON or any object into a navigable tree (paths,
tabular detection for arrays of objects, expansion of JSON embedded in
string fields such as MetadataJson, depth guard). DumpView renders it
with collapsible nodes, tables for record arrays, search that filters
and auto-expands matches, and copy-as-JSON. Manual expand state is kept
per node path so it survives search filtering.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Phase 0.5: Security event audit logging and visibility (#45)
* Add Phase 0 security hardening: break-glass SuperAdmin, lockout, geo/threat-intel blocking, configurable 2FA
Groundwork for upcoming external auth provider support (Entra/Google/AD)
and a domestic-violence/high-value-target threat model, both of which
demand hardening the existing password-only login path before any new
login surface is added:
- Break-glass primary SuperAdmin: the bootstrap /setup account is now
restricted to loopback-only login, permanently, so it stays reachable
even if every other credenti…
* UI forensic workflow phases 1–3: authenticated media, drill-down inspector, Evidence page (#48)
* Promote dev to main: testing-channel install docs + publish-testing/release fixes (#43)
* Fix publish-testing.yml: WiX DefineConstants collapses ProductVersion (#38)
publish-testing.yml's build-windows-dev job used
-p:DefineConstants="PublishDir=...;ProductVersion=..." (a single
semicolon-joined value), which collapses to one -d argument to wix.exe
and silently drops ProductVersion, surfacing as WIX0150 (undefined
preprocessor variable). release-installers.yml already avoids this by
passing PublishDir and ProductVersion as separate -p: properties;
apply the same fix here.
Reproduced locally: the broken pattern fails with WIX0150 building
deploy/windows/VideoForensics.Bootstrapper.Wix; the fixed pattern
builds both VideoForensics.msi and VideoForensicsBootstrapper.exe
successfully.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Fix release/testing organize-release-files: nested artifact dirs silently dropped (#39)
Both release-installers.yml's create-release job and publish-testing.yml's
publish-dev-release job used a flat `cp dir/*` to gather downloaded artifacts
into release-files/. actions/upload-artifact preserves subdirectories below
the common ancestor of the paths given to it, and the Windows installer
artifacts' paths (VideoForensics.Installer.Wix/... and
VideoForensics.Bootstrapper.Wix/...) only share deploy/windows/ as an
ancestor, so they land nested (e.g.
windows-installer/VideoForensics.Installer.Wix/bin/Release/VideoForensics.msi)
rather than flat. `cp dir/*` only copies top-level entries and silently
no-ops on directories (errors were swallowed by `2>/dev/null || true`), so
the MSI and Bootstrapper .exe never made it into the release - this is why
v0.1.0's GitHub Release only had the Debian package attached even after the
403 permissions fix. Manually uploaded the missing Windows assets to v0.1.0
to fix it immediately; this change fixes future runs.
Replaced with `find ... -type f -exec cp {} release-files/ \;`, which
copies every file regardless of nesting depth.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Fix publish-testing.yml: publish-dev-release job needs full clone for NBGV (#40)
The publish-dev-release job's dotnet pack steps use Nerdbank.GitVersioning,
which requires full commit history to calculate version height. Its
checkout step used actions/checkout@v4's default shallow clone
(fetch-depth: 1), unlike every other job in this workflow, causing:
Nerdbank.GitVersioning.GitException: Shallow clone lacks the objects
required to calculate version height.
This aborted the job before it ever reached the "Create/Update testing
release" step, so the dev branch's rolling testing release was never
actually being updated with new builds despite build-debian-dev and
build-windows-dev succeeding.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Document Testing channel usage for bootstrap installer scripts (#41)
Add example commands for install.ps1/install.sh Testing channel invocation
to README.md and a new Bootstrap Installer Scripts section to deploy/README.md.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Fix broken iex/-- parameter-passing syntax in installer one-liners (#42)
irm|iex piped through '--' doesn't forward args to the downloaded script
in PowerShell; use the scriptblock-wrap pattern instead, and fix iwr's
usage to reference .Content since it returns a response object, not a string.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Switch license to MIT with a Syncfusion carve-out
Syncfusion's Blazor/MAUI components are proprietary and commercially
licensed, consumed only via NuGet reference and never vendored, so
the MIT grant explicitly excludes them and points to CREDITS.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Replace WiX installer with Inno Setup, add first-run setup wizard and configurable storage paths
Removes the WiX-based bootstrapper/MSI (licensing risk under FireGiant's Open
Source Maintenance Fee for proprietary commercial use) in favor of Inno Setup,
which is free for any use. The new installer supports independent Server/
Desktop component selection, bundled FFmpeg, shortcuts, per-category data
directory configuration with resolved default paths, proper upgrade/uninstall
service lifecycle management, and an uninstall-time prompt to keep, back up,
or delete existing data.
Also adds a first-run /setup wizard so the installing user picks their own
admin username/password instead of a fixed admin/ChangeMe123! seed (which
remains available behind VIDEOFORENSICS_ENABLE_DEFAULT_ADMIN for headless
deployments), a DbSetup CLI tool for provisioning the database ahead of
service start, and a hot-swap script for iterating on the installed service
during development without a full installer round-trip.
Fixes two real bugs found along the way: VideoForensics.Hosting.csproj had a
stale PackageReference to Microsoft.AspNetCore.Http.Abstractions that shadowed
the real shared-framework DLL in self-contained publishes, crashing the
service under the SYSTEM account; and ConfigurationLoader.LoadAndApplyAsync
silently dropped 4 of 6 storage location settings when loading persisted
config.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Add installer admin/network setup, DB repair tooling, and fix a real auth race
Installer additions: optional in-installer SuperAdmin creation (skippable,
falls back to the browser /setup wizard), a 2-way network binding picker
(localhost/LAN) with an explicit warning when LAN access is chosen without
setting up an admin account here, per-path NTFS ACL grants and auto-create
for custom data directories, an opt-in network share for Reports/Media
(off by default, seeds the current installing user into a new
VideoForensicsSuperUser group), a Windows Firewall rule for LAN access, an
Optional Tools Start Menu group, and a 9-language installer-chrome picker.
Fixes a real security gap found during testing: the admin-creation and
network-tier DbSetup invocations were gated behind the separate "Optional
Tools" component instead of "server", so filling in the installer's admin
page silently did nothing unless Optional Tools was also checked - the
browser's /setup wizard then became the only path to claim SuperAdmin,
which is a race any device on the LAN could win once local network access
is enabled. DbSetup is now always bundled with the server, its DB-init/
admin-creation/network-tier calls moved from declarative [Run] entries into
[Code] so failures can be detected and surfaced instead of silently
swallowed, and a proactive warning fires when LAN access is chosen without
an admin account being set up here.
Also fixes a second real bug: ReadConfiguredNetworkTierBeforeHostBuilds
hardcoded the default database path, ignoring any registry-configured
custom Database location - extracted into a testable
NetworkTierConfigReader that correctly uses StorageLocationProvider.
New tooling: VideoForensics.Diagnostics library (extracted from
DbDiagnostics, TDD-covered) backing a new DbRepair CLI that fixes exact
duplicate rows and orphaned records - dry-run by default, requires --apply
plus a typed confirmation, transaction-wrapped. DbSetup gains
--set-network-tier and env-var-based (not CLI-arg) admin account creation.
Registers DbSetup/DbDiagnostics/DbRepair/VideoForensics.Diagnostics(.Tests)
in VideoForensics.sln - they were never added, so prior solution-wide
build/test runs silently skipped them. Bumps Radzen.Blazor, Syncfusion.Blazor.*,
and the MAUI package set to their latest patch versions (full gate).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Add media access tickets and client URL provider for authenticated media
Browser img/video tags can't send a bearer token, so media content will be
served via short-lived (10 min) signed tickets scoped to one media item and
one operator. Adds IMediaAccessTicketService (Data Protection based, mirrors
StepUpAuthService), MediaTicket DTOs and route helper, the client-side
IMediaContentUrlProvider contract, and RemoteMediaContentUrlProvider for
MAUI (batched POST /api/v1/media/tickets, absolute URLs). Server endpoints
follow in the next commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Require auth on media API and serve content via per-item tickets
/api/v1/devices, /media-items and /integrity-records now require bearer
auth. New POST /api/v1/media/tickets issues 10-minute tickets per media
item; GET /api/v1/media/{id}/content accepts either a bearer token or a
?ticket= (for img/video tags), re-checks the ticket's operator is still
active and approved, and records each initial view in the access audit
log (continuation Range requests skipped; audit failure returns 500).
Adds LocalMediaContentUrlProvider for the WebApp's Blazor Server UI,
which attributes tickets to the validated session principal rather than
the browser-stored operator id.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Load event media through ticketed URLs and add Ui.Shared bUnit tests
Events.razor and EventDetailsDialog hardcoded an unversioned
/api/media/{id}/content URL that no server route matched. They now get
ticketed URLs from IMediaContentUrlProvider: thumbnails are resolved in
one batch after events load, and the details dialog fetches a fresh URL
on open since tickets expire. The dialog shows "Media preview
unavailable" when no URL can be issued. Image/video format detection is
consolidated into MediaFormatHelper. Adds VideoForensics.Ui.Shared.Tests
(xUnit v3 + bUnit) as the first test project for the shared UI.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Add LinqPad-style DumpView for drilling into raw forensic data
DumpNodeBuilder turns JSON or any object into a navigable tree (paths,
tabular detection for arrays of objects, expansion of JSON embedded in
string fields such as MetadataJson, depth guard). DumpView renders it
with collapsible nodes, tables for record arrays, search that filters
and auto-expands matches, and copy-as-JSON. Manual expand state is kept
per node path so it survives search filtering.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Phase 0.5: Security event audit logging and visibility (#45)
* Add Phase 0 security hardening: break-glass SuperAdmin, lockout, geo/threat-intel blocking, configurable 2FA
Groundwork for upcoming external auth provider support (Entra/Google/AD)
and a domestic-violence/high-value-target threat model, both of which
demand hardening the existing password-only login path before any new
login surface is added:
- Break-glass primary SuperAdmin: the bootstrap /setup account is now
restricted to loopback-only login, permanently, so it stays reachable
even if every other credential or provider is compromised or misconfigured.
- Account lockout: per-operator failed-attempt tracking with configurable
threshold/duration, timing-safe dummy password verification to prevent
username enumeration, and generic failure responses across every cause.
- Layered IP/geo blocking: admin-managed banned-CIDR list and MaxMind
GeoLite2 country blocking, both fail-open by default (configurable to
fail-closed) so a lookup outage can't itself become a denial of service.
- Configurable two-factor policy: per-role defaults plus per-operator
overrides, enforced by reusing the existing WebAuthn passkey sign-in
endpoints as a second step (via a short-lived correlation token) rather
than inventing a new auth mechanism. Operators with no passkey yet get
a bootstrap grace path instead of being locked out by the new policy.
- SuperAdmin settings UI for lockout policy, 2FA role/operator requirements,
and manual unlock.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Add Phase 0.5: Security event audit logging infrastructure
- Add SecurityEvent entity (Id, OperatorId, EventType enum, Success, IpAddress, OccurredAtUtc, Reason)
- Add SecurityEventConfiguration with indexes (OperatorId+OccurredAtUtc, OccurredAtUtc)
- Add migration 20260924000000_AddPhase05SecurityEvents
- Add ISecurityAuditService interface with Record* and Get* methods for self-service and SuperAdmin queries
- Add SecurityAuditService implementation with write-once logging and async enumerable results
- Integrate audit logging into OperatorAuthEndpoints.LoginPasswordAsync:
- Record login attempt success/failure with IP
- Record account lockout when threshold crossed
- Add test file with 4 xUnit tests (TDD-style stubs for: successful login, failed password, lockout, unlock)
- Register ISecurityAuditService in DI
This phase establishes the security event log that feeds the MCP visibility tool (Phase 0.5)
and the admin settings page (Phase 1+).
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Add SecurityEventTools MCP endpoint for security visibility (Phase 0.5)
- Add SecurityEventTools class extending ForensicsToolBase
- Register get-security-events MCP tool with parameters: limit, offset, operatorId
- Self-service queries return caller's own events; cross-account queries require SuperAdminLocal
- Tool uses ISecurityAuditService.GetOperatorEventsAsync() to stream results
- Register tool in Program.cs MCP Tools section
Note: Stub implementation - self-service HTTP endpoint to follow.
Full authorization and self-service path require HTTP context integration.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Add /api/v1/security-events HTTP endpoint for Phase 0.5 visibility
- POST /api/v1/security-events with limit (1-1000), offset, and optional operatorId
- Self-service: caller queries own events (ReadOnly+ policy)
- Cross-account: SuperAdminLocal policy required (SuperAdmin + Local tier)
- Returns SecurityEventDto array (id, operatorId, eventType, success, ipAddress, occurredAtUtc, reason)
- Error handling: 400 invalid params, 401 unauthenticated, 403 policy failure, 500 service error
- Integrated into Program.cs endpoint registration
This enables both self-service visibility and SuperAdmin audit access to the security event log.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Refine SecurityEventTools MCP tool with proper authorization (Phase 0.5)
- Add IHttpContextAccessor to extract caller identity and role from claims
- Implement both self-service and cross-account authorization paths in tool
- Self-service: any authenticated caller queries their own events
- Cross-account: requires SuperAdmin role; Local tier check deferred to HTTP endpoint
- Proper error handling for missing context, invalid operatorId format, authorization failure
- Log authorization decisions and authorization denials for audit trail
- Register IHttpContextAccessor in Program.cs
This enables the MCP tool to enforce caller-based authorization rules
without requiring explicit tier resolution (which is HTTP-specific).
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Add SecurityEvents.razor component for Phase 0.5 security event UI
Implements two-tab Blazor component for operator security event visibility:
- Self-service tab: operators view their own login/breach/lockout events
- Admin audit tab: SuperAdmins query cross-account events with filtering
Features:
- Syncfusion SfGrid, SfAutoComplete, SfDatePicker integration
- Client-side event type and date range filtering
- CSV export capability
- Toast notifications for user feedback
- Role-based UI visibility (admin tab shown only to SuperAdmins)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Implement Phase 0.5 security audit integration tests
Adds three test methods for ISecurityAuditService integration:
- LoginPasswordAsync_SuccessfulLogin_RecordsAuditEvent: verifies successful login records success event
- LoginPasswordAsync_FailedPassword_RecordsFailureEvent: verifies failed password attempt records failure event with reason
- LoginPasswordAsync_LockedOutAfterThreshold_RecordsLockoutEvent: verifies account lockout after max attempts records lockout event
Includes OperatorAuthEndpointsInvoker helper class using reflection to invoke private endpoint method for testing.
All tests use Moq for mocking and verify audit service calls with expected parameters.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix SecurityEvents.razor compilation errors
Replace non-existent ReadAsAsync<T>() with ReadFromJsonAsync<T>() from System.Net.Http.Json.
The method ReadAsAsync<T>() does not exist in the standard HttpContent API.
Correct method is ReadFromJsonAsync<T>() from System.Net.Http.Json namespace.
- Add @using System.Net.Http.Json directive
- Replace ReadAsAsync calls on lines 389 and 454
Fixes build failure in PR #45.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix build errors: HasName does not exist on IndexBuilder
- SecurityEventConfiguration.cs: replace IndexBuilder.HasName() (removed/never existed in
EF Core's fluent API) with HasDatabaseName(), the correct method for naming an index.
- SecurityAuditService.cs: add [EnumeratorCancellation] to the CancellationToken parameter
on both async-iterator methods (GetOperatorEventsAsync, GetAllEventsAsync) so the
compiler-generated GetAsyncEnumerator forwards the caller's token instead of discarding it.
Fixes build-and-test and CodeQL Analyze failures on PR #45.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix SecurityEventConfiguration: remove explicit index naming
HasDatabaseName/HasName are extension methods defined in
Microsoft.EntityFrameworkCore.Relational, which VideoForensics.Data.Database.csproj
does not reference (it only references the core Microsoft.EntityFrameworkCore
package) - hence CS1061. No other entity configuration in this codebase names
indexes explicitly; EF Core's default naming convention already produces
IX_SecurityEvents_OperatorId_OccurredAtUtc and IX_SecurityEvents_OccurredAtUtc,
which is exactly what the migration expects, so the explicit calls are dropped
rather than adding a new package reference for no behavioral change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix migration compile error and add missing Designer.cs snapshot
- 20260924000000_AddPhase05SecurityEvents.cs: CreateIndex's single-column overload
takes descending as bool[]? not bool; wrap the literal in an array (CS1503).
- Add the migration's missing .Designer.cs file (every other migration has one).
Without it, the migration lacks the [DbContext]/[Migration] attributes and its
point-in-time BuildTargetModel snapshot, which EF Core's migration history and
scaffolding rely on - the hand-written migration was missing this pairing.
- VideoForensicsDbContextModelSnapshot.cs was also missing the SecurityEvent
entity entirely; add it so the running model matches the migration instead of
reporting a phantom pending model change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix SecurityEventsEndpoints: missing using and invalid logger generic arg
- INetworkTierResolver lives in VideoForensics.Hosting, not VideoForensics.WebApp.Auth (CS0246).
- ILogger<SecurityEventsEndpoints> fails to compile because the class is static and static
types cannot be used as type arguments (CS0718). Match the established pattern in this
codebase (see MediaApiEndpoints.cs) and use ILogger<Program> instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix missing using directives in security audit integration tests
Add VideoForensics.WebApp.Api (LoginPasswordRequest), VideoForensics.WebApp.Services
(IBannedIpMatchService, IThreatIntelBlocklistService, IGeoIpLookupService), and
VideoForensics.Providers.Common.Contracts (INotificationDispatcher) - all CS0246
in the previous push. Also drop the now-unused System.Security.Claims and
VideoForensics.WebApp.Auth usings and the CreateAuthenticatedHttpContext helper,
leftover from a fourth test method dropped earlier since DeviceManagementEndpoints.UnlockAsync
doesn't take an ISecurityAuditService parameter.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix pre-existing OperatorAuthEndpointsTests: add ISecurityAuditService mock
LoginPasswordAsync's private-method reflection lookup in this file's own
OperatorAuthEndpointsInvoker used a fixed parameter-type array that no longer
matched the real method after the Phase 0.5 auth integration added an
ISecurityAuditService parameter - GetMethod silently returned null, and all 5
tests calling it threw "Could not find LoginPasswordAsync method" at runtime
(a MethodInfo lookup failure, not a compile error, so the prior CI failures
never surfaced it until the WebApp/WebApp.Tests projects finally compiled).
Add the missing mock and thread it through every call site and the invoker's
reflection signature.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
---------
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Add inspector panel and ForensicGrid for row drill-down
Selecting a row in any ForensicGrid opens it in a right-panel inspector
with Fields (DumpView of the entity), Raw (provider JSON), Related
(links) and Provenance tabs; the inspector clears on navigation.
ForensicGrid standardises paging, sorting, checkbox filtering, column
chooser, search and CSV export across forensic tables. InspectorState is
registered in both the WebApp and MAUI hosts. EventDetailsDialogTests
moves off the obsolete bUnit TestContext.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Add UI forensic workflow plan with phase status
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Add global forensic scope with left-side ScopeRail synced to the URL
ScopeState holds the investigation scope (devices, UTC date window,
search) and serialises it to/from the query string; ScopeUrl merges it
into the current URL while keeping unrelated keys. ScopeRail renders in
the left pane for signed-in operators with device checkboxes, from/to
dates, search, and 24h/7d/30d/Reset quick ranges driven by TimeProvider,
so links, reloads and back/forward preserve the scope. Registered in
both hosts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Move Events page onto global scope, ForensicGrid and inspector
Events now reads devices/date window/search from the left ScopeRail and
reloads when the scope changes (multi-device fan-out, merged newest
first, per-device failures reported without losing other rows). Loading
moves to a testable EventRowLoader; selecting a row opens the inspector
with the event and media entities, raw provider JSON and provenance
(hash, integrity, legal hold). ForensicGrid gains context-menu
passthrough so hold/release/verify/export keep working per row.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Move Query API page onto ForensicGrid, inspector and global scope
Every live provider result (locations, devices, device events) is now a
ForensicGrid whose rows open in the inspector with source call,
parameters and retrieval time as provenance, plus a collapsible
DumpView of the whole response. The device-events window comes from the
ScopeRail; selecting a location or device fills the id inputs for the
next query; device config renders via DumpView. Marks phase 2 complete
in the UI workflow plan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Support media lookup by device/date range and id over the API
GET /api/v1/media-items accepts from/to (with deviceId; 400 otherwise
or when reversed) and GET /api/v1/media-items/{id} returns one item.
RemoteMediaItemRepository implements GetByDeviceAndDateRangeAsync and
GetAsync against them, so MAUI can browse all media in a scope (not
only event-linked media) for the Evidence gallery.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Add unified evidence stream and timeline grouping
EvidenceLoader merges events (via EventRowLoader) with every stored
media file in scope, de-duplicating media already attached to events,
skipping purged media, classifying media-only items as snapshot, video
or file, and keeping other devices' items when one device fails.
EvidenceTimeline groups items by local day then device and computes
prev/next neighbours for the upcoming media viewer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Add full-pane MediaViewer for snapshots and event video
Images zoom (buttons, wheel, keys; 1-8x) and drag-pan when zoomed;
video gets playback speed, frame-accurate stepping from the media's
frame rate (30 fps fallback) and play/pause via a small JS module.
Arrow keys move prev/next, Esc closes. Header shows device, UTC time
and the file's SHA-256. Zoom/pan math and key mapping are pure,
unit-tested helpers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Add Evidence page with timeline, grid and gallery over the scope
/evidence shows every event and stored media file in the current scope
as a day/device timeline, a ForensicGrid, or a gallery. Selecting an
item opens the MediaViewer with the inspector docked, fetching a fresh
ticketed URL per item and ignoring stale responses; view and open item
are kept in the URL for deep links. Evidence is the first nav item.
Marks phase 3 complete in the UI workflow plan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Add mobile-friendly layout for MAUI/narrow viewports (#46)
* Fix WebApp startup crash from missing DI registrations
IBannedIpRangeRepository, ILockoutPolicySettingsRepository, and
ITwoFactorRoleRequirementRepository were never registered in
AddVideoForensicsDatabase(), even though BannedIpMatchService and
OperatorAuthEndpoints consume them directly. This crashed the host at
startup (DI validation failure / "Failure to infer one or more
parameters"), since BackgroundServiceExceptionBehavior is StopHost.
Add the missing TryAddScoped registrations and a test that builds the
full service collection with BuildServiceProvider(validateScopes: true)
so a future missing registration fails a test instead of only failing
at runtime.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Add mobile-friendly layout for MAUI/narrow viewports
MAUI mobile (iOS/Android) and narrow browser windows previously got the
same desktop-first SfSplitter layout as full-size desktop/web, with no
touch-friendly alternative. Add IViewportService (matchMedia-backed,
600px breakpoint) and a MobileLayout with a slide-in nav drawer and a
bottom-sheet settings panel, and swap between MobileLayout/MainLayout
via a new ResponsiveLayout wrapper at the Routes.razor level.
MainLayout itself is unchanged, per CLAUDE.md's layout guidance.
Verified live in-browser: the mobile drawer/settings sheet and the
desktop MainLayout both render correctly at their respective viewport
widths.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Update outdated NuGet packages solution-wide
Moq 4.20.72 -> 4.21.0 (all test projects), Radzen.Blazor 11.4.2 ->
11.4.3 (Ui.Shared), Microsoft.Extensions.Logging.Console 10.0.11 ->
10.0.12 (DbSetup/DbRepair/MigrateMediaPaths). Part of the full gate
required before opening a PR.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Remove stale CI test exclusions (#47)
The three --filter-not-* exclusions in ci.yml's Test step were added
before these tests were made hermetic. Verified against current main
that all three now pass without any special environment:
PathUtilitiesTests (uses a fixed OS-independent sanitization set),
ReportGenerationServiceTests.WriteReportAsync_WithJsonFormat_WritesJsonFile
(mocks IStorageLocationProvider to an isolated temp dir), and the SQLite
ServiceCollectionExtensionsTests test (renamed to
AddVideoForensicsSqlite_ExplicitPath_ResolvesFactory and now uses an
isolated temp path instead of the real /var/lib/videoforensics default).
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Fix full-gate build warnings
Make the RemoteMediaContentUrlProvider cancellation test assert the
forwarded token (CS0219), set Range headers via Append in media endpoint
tests (ASP0019), and suppress CS8002 in the four legacy strong-named
Ring projects that reference unsigned assemblies (strong names are not
validated on .NET).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
---------
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Allow PRs into main only from dev (#49)
* Promote dev to main: testing-channel install docs + publish-testing/release fixes (#43)
* Fix publish-testing.yml: WiX DefineConstants collapses ProductVersion (#38)
publish-testing.yml's build-windows-dev job used
-p:DefineConstants="PublishDir=...;ProductVersion=..." (a single
semicolon-joined value), which collapses to one -d argument to wix.exe
and silently drops ProductVersion, surfacing as WIX0150 (undefined
preprocessor variable). release-installers.yml already avoids this by
passing PublishDir and ProductVersion as separate -p: properties;
apply the same fix here.
Reproduced locally: the broken pattern fails with WIX0150 building
deploy/windows/VideoForensics.Bootstrapper.Wix; the fixed pattern
builds both VideoForensics.msi and VideoForensicsBootstrapper.exe
successfully.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Fix release/testing organize-release-files: nested artifact dirs silently dropped (#39)
Both release-installers.yml's create-release job and publish-testing.yml's
publish-dev-release job used a flat `cp dir/*` to gather downloaded artifacts
into release-files/. actions/upload-artifact preserves subdirectories below
the common ancestor of the paths given to it, and the Windows installer
artifacts' paths (VideoForensics.Installer.Wix/... and
VideoForensics.Bootstrapper.Wix/...) only share deploy/windows/ as an
ancestor, so they land nested (e.g.
windows-installer/VideoForensics.Installer.Wix/bin/Release/VideoForensics.msi)
rather than flat. `cp dir/*` only copies top-level entries and silently
no-ops on directories (errors were swallowed by `2>/dev/null || true`), so
the MSI and Bootstrapper .exe never made it into the release - this is why
v0.1.0's GitHub Release only had the Debian package attached even after the
403 permissions fix. Manually uploaded the missing Windows assets to v0.1.0
to fix it immediately; this change fixes future runs.
Replaced with `find ... -type f -exec cp {} release-files/ \;`, which
copies every file regardless of nesting depth.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Fix publish-testing.yml: publish-dev-release job needs full clone for NBGV (#40)
The publish-dev-release job's dotnet pack steps use Nerdbank.GitVersioning,
which requires full commit history to calculate version height. Its
checkout step used actions/checkout@v4's default shallow clone
(fetch-depth: 1), unlike every other job in this workflow, causing:
Nerdbank.GitVersioning.GitException: Shallow clone lacks the objects
required to calculate version height.
This aborted the job before it ever reached the "Create/Update testing
release" step, so the dev branch's rolling testing release was never
actually being updated with new builds despite build-debian-dev and
build-windows-dev succeeding.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Document Testing channel usage for bootstrap installer scripts (#41)
Add example commands for install.ps1/install.sh Testing channel invocation
to README.md and a new Bootstrap Installer Scripts section to deploy/README.md.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Fix broken iex/-- parameter-passing syntax in installer one-liners (#42)
irm|iex piped through '--' doesn't forward args to the downloaded script
in PowerShell; use the scriptblock-wrap pattern instead, and fix iwr's
usage to reference .Content since it returns a response object, not a string.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Switch license to MIT with a Syncfusion carve-out
Syncfusion's Blazor/MAUI components are proprietary and commercially
licensed, consumed only via NuGet reference and never vendored, so
the MIT grant explicitly excludes them and points to CREDITS.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Replace WiX installer with Inno Setup, add first-run setup wizard and configurable storage paths
Removes the WiX-based bootstrapper/MSI (licensing risk under FireGiant's Open
Source Maintenance Fee for proprietary commercial use) in favor of Inno Setup,
which is free for any use. The new installer supports independent Server/
Desktop component selection, bundled FFmpeg, shortcuts, per-category data
directory configuration with resolved default paths, proper upgrade/uninstall
service lifecycle management, and an uninstall-time prompt to keep, back up,
or delete existing data.
Also adds a first-run /setup wizard so the installing user picks their own
admin username/password instead of a fixed admin/ChangeMe123! seed (which
remains available behind VIDEOFORENSICS_ENABLE_DEFAULT_ADMIN for headless
deployments), a DbSetup CLI tool for provisioning the database ahead of
service start, and a hot-swap script for iterating on the installed service
during development without a full installer round-trip.
Fixes two real bugs found along the way: VideoForensics.Hosting.csproj had a
stale PackageReference to Microsoft.AspNetCore.Http.Abstractions that shadowed
the real shared-framework DLL in self-contained publishes, crashing the
service under the SYSTEM account; and ConfigurationLoader.LoadAndApplyAsync
silently dropped 4 of 6 storage location settings when loading persisted
config.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Add installer admin/network setup, DB repair tooling, and fix a real auth race
Installer additions: optional in-installer SuperAdmin creation (skippable,
falls back to the browser /setup wizard), a 2-way network binding picker
(localhost/LAN) with an explicit warning when LAN access is chosen without
setting up an admin account here, per-path NTFS ACL grants and auto-create
for custom data directories, an opt-in network share for Reports/Media
(off by default, seeds the current installing user into a new
VideoForensicsSuperUser group), a Windows Firewall rule for LAN access, an
Optional Tools Start Menu group, and a 9-language installer-chrome picker.
Fixes a real security gap found during testing: the admin-creation and
network-tier DbSetup invocations were gated behind the separate "Optional
Tools" component instead of "server", so filling in the installer's admin
page silently did nothing unless Optional Tools was also checked - the
browser's /setup wizard then became the only path to claim SuperAdmin,
which is a race any device on the LAN could win once local network access
is enabled. DbSetup is now always bundled with the server, its DB-init/
admin-creation/network-tier calls moved from declarative [Run] entries into
[Code] so failures can be detected and surfaced instead of silently
swallowed, and a proactive warning fires when LAN access is chosen without
an admin account being set up here.
Also fixes a second real bug: ReadConfiguredNetworkTierBeforeHostBuilds
hardcoded the default database path, ignoring any registry-configured
custom Database location - extracted into a testable
NetworkTierConfigReader that correctly uses StorageLocationProvider.
New tooling: VideoForensics.Diagnostics library (extracted from
DbDiagnostics, TDD-covered) backing a new DbRepair CLI that fixes exact
duplicate rows and orphaned records - dry-run by default, requires --apply
plus a typed confirmation, transaction-wrapped. DbSetup gains
--set-network-tier and env-var-based (not CLI-arg) admin account creation.
Registers DbSetup/DbDiagnostics/DbRepair/VideoForensics.Diagnostics(.Tests)
in VideoForensics.sln - they were never added, so prior solution-wide
build/test runs silently skipped them. Bumps Radzen.Blazor, Syncfusion.Blazor.*,
and the MAUI package set to their latest patch versions (full gate).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Phase 0.5: Security event audit logging and visibility (#45)
* Add Phase 0 security hardening: break-glass SuperAdmin, lockout, geo/threat-intel blocking, configurable 2FA
Groundwork for upcoming external auth provider support (Entra/Google/AD)
and a domestic-violence/high-value-target threat model, both of which
demand hardening the existing password-only login path before any new
login surface is added:
- Break-glass primary SuperAdmin: the bootstrap /setup account is now
restricted to loopback-only login, permanently, so it stays reachable
even if every other credential or provider is compromised or misconfigured.
- Account lockout: per-operator failed-attempt tracking with configurable
threshold/duration, timing-safe dummy password verification to prevent
username enumeration, and generic failure responses across every cause.
- Layered IP/geo blocking: admin-managed banned-CIDR list and MaxMind
GeoLite2 country blocking, both fail-open by default (configurable to
fail-closed) so a lookup outage can't itself become a denial of service.
- Configurable two-factor policy: per-role defaults plus per-operator
overrides, enforced by reusing the existing WebAuthn passkey sign-in
endpoints as a second step (via a short-lived correlation token) rather
than inventing a new auth mechanism. Operators with no passkey yet get
a bootstrap grace path instead of being locked out by the new policy.
- SuperAdmin settings UI for lockout policy, 2FA role/operator requirements,
and manual unlock.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Add Phase 0.5: Security event audit logging infrastructure
- Add SecurityEvent entity (Id, OperatorId, EventType enum, Success, IpAddress, OccurredAtUtc, Reason)
- Add SecurityEventConfiguration with indexes (OperatorId+OccurredAtUtc, OccurredAtUtc)
- Add migration 20260924000000_AddPhase05SecurityEvents
- Add ISecurityAuditService interface with Record* and Get* methods for self-service and SuperAdmin queries
- Add SecurityAuditService implementation with write-once logging and async enumerable results
- Integrate audit logging into OperatorAuthEndpoints.LoginPasswordAsync:
- Record login attempt success/failure with IP
- Record account lockout when threshold crossed
- Add test file with 4 xUnit tests (TDD-style stubs for: successful login, failed password, lockout, unlock)
- Register ISecurityAuditService in DI
This phase establishes the security event log that feeds the MCP visibility tool (Phase 0.5)
and the admin settings page (Phase 1+).
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Add SecurityEventTools MCP endpoint for security visibility (Phase 0.5)
- Add SecurityEventTools class extending ForensicsToolBase
- Register get-security-events MCP tool with parameters: limit, offset, operatorId
- Self-service queries return caller's own events; cross-account queries require SuperAdminLocal
- Tool uses ISecurityAuditService.GetOperatorEventsAsync() to stream results
- Register tool in Program.cs MCP Tools section
Note: Stub implementation - self-service HTTP endpoint to follow.
Full authorization and self-service path require HTTP context integration.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Add /api/v1/security-events HTTP endpoint for Phase 0.5 visibility
- POST /api/v1/security-events with limit (1-1000), offset, and optional operatorId
- Self-service: caller queries own events (ReadOnly+ policy)
- Cross-account: SuperAdminLocal policy required (SuperAdmin + Local tier)
- Returns SecurityEventDto array (id, operatorId, eventType, success, ipAddress, occurredAtUtc, reason)
- Error handling: 400 invalid params, 401 unauthenticated, 403 policy failure, 500 service error
- Integrated into Program.cs endpoint registration
This enables both self-service visibility and SuperAdmin audit access to the security event log.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Refine SecurityEventTools MCP tool with proper authorization (Phase 0.5)
- Add IHttpContextAccessor to extract caller identity and role from claims
- Implement both self-service and cross-account authorization paths in tool
- Self-service: any authenticated caller queries their own events
- Cross-account: requires SuperAdmin role; Local tier check deferred to HTTP endpoint
- Proper error handling for missing context, invalid operatorId format, authorization failure
- Log authorization decisions and authorization denials for audit trail
- Register IHttpContextAccessor in Program.cs
This enables the MCP tool to enforce caller-based authorization rules
without requiring explicit tier resolution (which is HTTP-specific).
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Add SecurityEvents.razor component for Phase 0.5 security event UI
Implements two-tab Blazor component for operator security event visibility:
- Self-service tab: operators view their own login/breach/lockout events
- Admin audit tab: SuperAdmins query cross-account events with filtering
Features:
- Syncfusion SfGrid, SfAutoComplete, SfDatePicker integration
- Client-side event type and date range filtering
- CSV export capability
- Toast notifications for user feedback
- Role-based UI visibility (admin tab shown only to SuperAdmins)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Implement Phase 0.5 security audit integration tests
Adds three test methods for ISecurityAuditService integration:
- LoginPasswordAsync_SuccessfulLogin_RecordsAuditEvent: verifies successful login records success event
- LoginPasswordAsync_FailedPassword_RecordsFailureEvent: verifies failed password attempt records failure event with reason
- LoginPasswordAsync_LockedOutAfterThreshold_RecordsLockoutEvent: verifies account lockout after max attempts records lockout event
Includes OperatorAuthEndpointsInvoker helper class using reflection to invoke private endpoint method for testing.
All tests use Moq for mocking and verify audit service calls with expected parameters.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix SecurityEvents.razor compilation errors
Replace non-existent ReadAsAsync<T>() with ReadFromJsonAsync<T>() from System.Net.Http.Json.
The method ReadAsAsync<T>() does not exist in the standard HttpContent API.
Correct method is ReadFromJsonAsync<T>() from System.Net.Http.Json namespace.
- Add @using System.Net.Http.Json directive
- Replace ReadAsAsync calls on lines 389 and 454
Fixes build failure in PR #45.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix build errors: HasName does not exist on IndexBuilder
- SecurityEventConfiguration.cs: replace IndexBuilder.HasName() (removed/never existed in
EF Core's fluent API) with HasDatabaseName(), the correct method for naming an index.
- SecurityAuditService.cs: add [EnumeratorCancellation] to the CancellationToken parameter
on both async-iterator methods (GetOperatorEventsAsync, GetAllEventsAsync) so the
compiler-generated GetAsyncEnumerator forwards the caller's token instead of discarding it.
Fixes build-and-test and CodeQL Analyze failures on PR #45.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix SecurityEventConfiguration: remove explicit index naming
HasDatabaseName/HasName are extension methods defined in
Microsoft.EntityFrameworkCore.Relational, which VideoForensics.Data.Database.csproj
does not reference (it only references the core Microsoft.EntityFrameworkCore
package) - hence CS1061. No other entity configuration in this codebase names
indexes explicitly; EF Core's default naming convention already produces
IX_SecurityEvents_OperatorId_OccurredAtUtc and IX_SecurityEvents_OccurredAtUtc,
which is exactly what the migration expects, so the explicit calls are dropped
rather than adding a new package reference for no behavioral change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix migration compile error and add missing Designer.cs snapshot
- 20260924000000_AddPhase05SecurityEvents.cs: CreateIndex's single-column overload
takes descending as bool[]? not bool; wrap the literal in an array (CS1503).
- Add the migration's missing .Designer.cs file (every other migration has one).
Without it, the migration lacks the [DbContext]/[Migration] attributes and its
point-in-time BuildTargetModel snapshot, which EF Core's migration history and
scaffolding rely on - the hand-written migration was missing this pairing.
- VideoForensicsDbContextModelSnapshot.cs was also missing the SecurityEvent
entity entirely; add it so the running model matches the migration instead of
reporting a phantom pending model change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix SecurityEventsEndpoints: missing using and invalid logger generic arg
- INetworkTierResolver lives in VideoForensics.Hosting, not VideoForensics.WebApp.Auth (CS0246).
- ILogger<SecurityEventsEndpoints> fails to compile because the class is static and static
types cannot be used as type arguments (CS0718). Match the established pattern in this
codebase (see MediaApiEndpoints.cs) and use ILogger<Program> instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix missing using directives in security audit integration tests
Add VideoForensics.WebApp.Api (LoginPasswordRequest), VideoForensics.WebApp.Services
(IBannedIpMatchService, IThreatIntelBlocklistService, IGeoIpLookupService), and
VideoForensics.Providers.Common.Contracts (INotificationDispatcher) - all CS0246
in the previous push. Also drop the now-unused System.Security.Claims and
VideoForensics.WebApp.Auth usings and the CreateAuthenticatedHttpContext helper,
leftover from a fourth test method dropped earlier since DeviceManagementEndpoints.UnlockAsync
doesn't take an ISecurityAuditService parameter.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
* Fix pre-existing OperatorAuthEndpointsTests: add ISecurityAuditService mock
LoginPasswordAsync's private-method reflection lookup in this file's own
OperatorAuthEndpointsInvoker used a fixed parameter-type array that no longer
matched the real method after the Phase 0.5 auth integration added an
ISecurityAuditService parameter - GetMethod silently returned null, and all 5
tests calling it threw "Could not find LoginPasswordAsync method" at runtime
(a MethodInfo lookup failure, not a compile error, so the prior CI failures
never surfaced it until the WebApp/WebApp.Tests projects finally compiled).
Add the missing mock and thread it through every call site and the invoker's
reflection signature.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRo463wjcSuyypR8erqTHX
---------
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Add mobile-friendly layout for MAUI/narrow viewports (#46)
* Fix WebApp startup crash from missing DI registrations
IBannedIpRangeRepository, ILockoutPolicySettingsRepository, and
ITwoFactorRoleRequirementRepository were never registered in
AddVideoForensicsDatabase(), even though BannedIpMatchService and
OperatorAuthEndpoints consume them directly. This crashed the host at
startup (DI validation failure / "Failure to infer one or more
parameters"), since BackgroundServiceExceptionBehavior is StopHost.
Add the missing TryAddScoped registrations and a test that builds the
full service collection with BuildServiceProvider(validateScopes: true)
so a future missing registration fails a test instead of only failing
at runtime.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Add mobile-friendly layout for MAUI/narrow viewports
MAUI mobile (iOS/Android) and narrow browser windows previously got the
same desktop-first SfSplitter layout as full-size desktop/web, with no
touch-friendly alternative. Add IViewportService (matchMedia-backed,
600px breakpoint) and a MobileLayout with a slide-in nav drawer and a
bottom-sheet settings panel, and swap between MobileLayout/MainLayout
via a new ResponsiveLayout wrapper at the Routes.razor level.
MainLayout itself is unchanged, per CLAUDE.md's layout guidance.
Verified live in-browser: the mobile drawer/settings sheet and the
desktop MainLayout both render correctly at their respective viewport
widths.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Update outdated NuGet packages solution-wide
Moq 4.20.72 -> 4.21.0 (all test projects), Radzen.Blazor 11.4.2 ->
11.4.3 (Ui.Shared), Microsoft.Extensions.Logging.Console 10.0.11 ->
10.0.12 (DbSetup/DbRepair/MigrateMediaPaths). Part of the full gate
required before opening a PR.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Remove stale CI test exclusions (#47)
The three --filter-not-* exclusions in ci.yml's Test step were added
before these tests were made hermetic. Verified against current main
that all three now pass without any special environment:
PathUtilitiesTests (uses a fixed OS-independent sanitization set),
ReportGenerationServiceTests.WriteReportAsync_WithJsonFormat_WritesJsonFile
(mocks IStorageLocationProvider to an isolated temp dir), and the SQLite
ServiceCollectionExtensionsTests test (renamed to
AddVideoForensicsSqlite_ExplicitPath_ResolvesFactory and now uses an
isolated temp path instead of the real /var/lib/videoforensics default).
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Allow PRs into main only from dev
Adds a main-source-guard workflow that fails any pull request into main
whose head is not this repository's dev branch, and documents the
feature → dev → main flow in CLAUDE.md. The only-from-dev check must be
made a required status check on main to enforce it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
---------
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* UI forensic workflow phase 4: investigation Cases (#51)
* Promote dev to main: testing-channel install docs + publish-testing/release fixes (#43)
* Fix publish-testing.yml: WiX DefineConstants collapses ProductVersion (#38)
publish-testing.yml's build-windows-dev job used
-p:DefineConstants="PublishDir=...;ProductVersion=..." (a single
semicolon-joined value), which collapses to one -d argument to wix.exe
and silently drops ProductVersion, surfacing as WIX0150 (undefined
preprocessor variable). release-installers.yml already avoids this by
passing PublishDir and ProductVersion as separate -p: properties;
apply the same fix here.
Reproduced locally: the broken pattern fails with WIX0150 building
deploy/windows/VideoForensics.Bootstrapper.Wix; the fixed pattern
builds both VideoForensics.msi and VideoForensicsBootstrapper.exe
successfully.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Fix release/testing organize-release-files: nested artifact dirs silently dropped (#39)
Both release-installers.yml's create-release job and publish-testing.yml's
publish-dev-release job used a flat `cp dir/*` to gather downloaded artifacts
into release-files/. actions/upload-artifact preserves subdirectories below
the common ancestor of the paths given to it, and the Windows installer
artifacts' paths (VideoForensics.Installer.Wix/... and
VideoForensics.Bootstrapper.Wix/...) only share deploy/windows/ as an
ancestor, so they land nested (e.g.
windows-installer/VideoForensics.Installer.Wix/bin/Release/VideoForensics.msi)
rather than flat. `cp dir/*` only copies top-level entries and silently
no-ops on directories (errors were swallowed by `2>/dev/null || true`), so
the MSI and Bootstrapper .exe never made it into the release - this is why
v0.1.0's GitHub Release only had the Debian package attached even after the
403 permissions fix. Manually uploaded the missing Windows assets to v0.1.0
to fix it immediately; this change fixes future runs.
Replaced with `find ... -type f -exec cp {} release-files/ \;`, which
copies every file regardless of nesting depth.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Fix publish-testing.yml: publish-dev-release job needs full clone for NBGV (#40)
The publish-dev-release job's dotnet pack steps use Nerdbank.GitVersioning,
which requires full commit history to calculate version height. Its
checkout step used actions/checkout@v4's default shallow clone
(fetch-depth: 1), unlike every other job in this workflow, causing:
Nerdbank.GitVersioning.GitException: Shallow clone lacks the objects
required to calculate version height.
This aborted the job before it ever reached the "Create/Update testing
release" step, so the dev branch's rolling testing release was never
actually being updated with new builds despite build-debian-dev and
build-windows-dev succeeding.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Document Testing channel usage for bootstrap installer scripts (#41)
Add example commands for install.ps1/install.sh Testing channel invocation
to README.md and a new Bootstrap Installer Scripts section to deploy/README.md.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Fix broken iex/-- parameter-passing syntax in installer one-liners (#42)
irm|iex piped through '--' doesn't forward args to the downloaded script
in PowerShell; use the scriptblock-wrap pattern instead, and fix iwr's
usage to reference .Content since it returns a response object, not a string.
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: rsperry79 <rsperry79@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Switch license to MIT with a Syncfusion carve-out
Syncfusion's Blazor/MAUI components are proprietary and commercially
licensed, consumed only via NuGet reference and never vendored, so
the MIT grant explicitly excludes them and points to CREDITS.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Replace WiX installer with Inno Setup, add first-run setup wizard and configurable storage paths
Removes the WiX-based bootstrapper/MSI (licensing risk under FireGiant's Open
Source Maintenance Fee for proprietary commercial use) in favor of Inno Setup,
which is free for any use. The new installer supports independent Server/
Desktop component selection, bundled FFmpeg, shortcuts, per-category data
directory configuration with resolved default paths, proper upgrade/uninstall
service lifecycle management, and an uninstall-time prompt to keep, back up,
or delete existing data.
Also adds a first-run /setup wizard so the installing user picks their own
admin username/password instead of a fixed admin/ChangeMe123! seed (which
remains available behind VIDEOFORENSICS_ENABLE_DEFAULT_ADMIN for headless
deployments), a DbSetup CLI tool for provisioning the database ahead of
service start, and a hot-swap script for iterating on the installed service
during development without a full installer round-trip.
Fixes two real bugs found along the way: VideoForensics.Hosting.csproj had a
stale PackageReference to Microsoft.AspNetCore.Http.Abstractions that shadowed
the real shared-framework DLL in self-contained publishes, crashing the
service under the SYSTEM account; and ConfigurationLoader.LoadAndApplyAsync
silently dropped 4 of 6 storage location settings when loading persisted
config.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Add installer admin/network setup, DB repair tooling, and fix a real auth race
Installer additions: optional in-installer SuperAdmin creation (skippable,
falls back to the browser /setup wizard), a 2-way network binding picker
(localhost/LAN) with an explicit warning when LAN access is chosen without
setting up an admin account here, per-path NTFS ACL grants and auto-create
for custom data directories, an opt-in network share for Reports/Media
(off by default, seeds the current installing user into a new
VideoForensicsSuperUser group), a Windows Firewall rule for LAN access, an
Optional Tools Start Menu group, and a 9-language installer-chrome picker.
Fixes a real security gap found during testing: the admin-creation and
network-tier DbSetup invocations were gated behind the separate "Optional
Tools" component instead of "server", so filling in the installer's admin
page silently did nothing unless Optional Tools was also checked - the
browser's /setup wizard then became the only path to claim SuperAdmin,
which is a race any device on the LAN could win once local network access
is enabled. DbSetup is now always bundled with the server, its DB-init/
admin-creation/network-tier calls moved from declarative [Run] entries into
[Code] so failures can be detected and surfaced instead of silently
swallowed, and a proactive warning fires when LAN access is chosen without
an admin account being set up here.
Also fixes a second real bug: ReadConfiguredNetworkTierBeforeHostBuilds
hardcoded the default database path, ignoring any registry-configured
custom Database location - extracted into a testable
NetworkTierConfigReader that correctly uses StorageLocationProvider.
New tooling: VideoForensics.Diagnostics library (extracted from
DbDiagnostics, TDD-covered) backing a new DbRepair CLI that fixes exact
duplicate rows and orphaned records - dry-run by default, requires --apply
plus a typed confirmation, transaction-wrapped. DbSetup gains
--set-network-tier and env-var-based (not CLI-arg) admin account creation.
Registers DbSetup/DbDiagnostics/DbRepair/VideoForensics.Diagnostics(.Tests)
in VideoForensics.sln - they were never added, so prior solution-wide
build/test runs silently skipped them. Bumps Radzen.Blazor, Syncfusion.Blazor.*,
and the MAUI package set to their latest patch versions (full gate).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Add media access tickets and client URL provider for authenticated media
Browser img/video tags can't send a bearer token, so media content will be
served via short-lived (10 min) signed tickets scoped to one media item and
one operator. Adds IMediaAccessTicketService (Data Protection based, mirrors
StepUpAuthService), MediaTicket DTOs and route helper, the client-side
IMediaContentUrlProvider contract, and RemoteMediaContentUrlProvider for
MAUI (batched POST /api/v1/media/tickets, absolute URLs). Server endpoints
follow in the next commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Require auth on media API and serve content via per-item tickets
/api/v1/devices, /media-items and /integrity-records now require bearer
auth. New POST /api/v1/media/tickets issues 10-minute tickets per media
item; GET /api/v1/media/{id}/content accepts either a bearer token or a
?ticket= (for img/video tags), re-checks the ticket's operator is still
active and approved, and records each initial view in the access audit
log (continuation Range requests skipped; audit failure returns 500).
Adds LocalMediaContentUrlProvider for the WebApp's Blazor Server UI,
which attributes tickets to the validated session principal rather than
the browser-stored operator id.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Load event media through ticketed URLs and add Ui.Shared bUnit tests
Events.razor and EventDetailsDialog hardcoded an unversioned
/api/media/{id}/content URL that no server route matched. They now get
ticketed URLs from IMediaContentUrlProvider: thumbnails are resolved in
one batch after events load, and the details dialog fetches a fresh URL
on open since tickets expire. The dialog shows "Media preview
unavailable" when no URL can be issued. Image/video format detection is
consolidated into MediaFormatHelper. Adds VideoForensics.Ui.Shared.Tests
(xUnit v3 + bUnit) as the first test project for the shared UI.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Add LinqPad-style DumpView for drilling into raw forensic data
DumpNodeBuilder turns JSON or any object into a navigable tree (paths,
tabular detection for arrays of objects, expansion of JSON embedded in
string fields such as MetadataJson, depth guard). DumpView renders it
with collapsible nodes, tables for record arrays, search that filters
and auto-expands matches, and copy-as-JSON. Manual expand state is kept
per node path so it survives search filtering.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrxQDyvvoJcdZeNd9FMQ8c
* Phase 0.5: Security event audit logging and visibility (#45)
* Add Phase 0 security hardening: break-glass SuperAdmin, lockout, geo/threat-intel blocking, configurable 2FA
Groundwork for upcoming external auth provider support (Entra/Google/AD)
and a domestic-violence/high-value-target threat model, both of which
demand hardening the existing password-only login path before any new
login surface is added:
- Break-glass primary SuperAdmin: the bootstrap /setup account is now
restricted to loopback-only login, permanently, so it stays reachable
even if every other c…
rsperry79
enabled auto-merge (squash)
October 4, 2026 18:28
Keep dev's structure (Phase 3 reorganization with src/utils/* paths).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bring workflow and solution file fixes from main back to dev (real merge commit).
Per branching model: main→dev reconciliation must use real merge, not squash.