Commit Graph

22 Commits

Author SHA1 Message Date
Oliver Kieselbach 192a1e939b refactor(sessions): unify SessionSummary mapper + SessionsIndex projection; fix ImeAgentVersion/AdminMarkedAction drift
Audit findings #4 (SessionSummary mapper x3) and #3 dual-write Phase 1.

- Collapse the two ~99%-identical SessionSummary mappers into one core mapper
  taking the resolved sessionId (index vs primary differ only in that source).
- BuildSessionIndexEntity is now the single SessionsIndex projection manifest and
  a SUPERSET of every field the merge call sites write, so a StartedAt-shift full
  upsert can no longer drop a merged field (the recurring drift class, cf ab90423b).
- Fix two real bugs surfaced while grounding the design: ImeAgentVersion was never
  written to the index although search/MCP push an OData filter against it (filter
  matched nothing); UpdateSessionImeAgentVersionAsync now mirrors it. AdminMarkedAction
  (dashboard list badge) was merged but missing from the projection -> now included.
- SyncSessionIndexAsync funnels the upsert-vs-merge + IndexRowKey-resolution decision
  that was duplicated across the 3 StartedAt-shift branch sites.
- Correct the DeletionState doc-comment (it is primary-only, not index-surfaced).

The SessionsIndex is a full denormalized mirror of Sessions (different RowKey sort)
serving list/search/stats/raw directly; lean-drop would break /api/raw/sessions.

Backend build 0 warnings; full xUnit suite 2172/2172 (incl. new index-projection +
ImeAgentVersion-mirror tests). BuildSessionIndexEntity made internal for the test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 20:54:16 +02:00
Oliver Kieselbach bc3cefa790 feat(backend/observability): SignalR free-quota watcher with tiered OpsEvents
Hourly TimerFunction queries Azure Monitor for SignalR ConnectionCount (max
over last hour) and MessageCount (sum since UTC midnight) and emits Warning
(>=80%) / Error (>=95%) OpsEvents when the free-tier limits (20 connections,
20k messages/day) are approached. Mirror of the cert-expiry pattern - global
TenantId=null events, dedup'd to one emission per EventType per UTC day.

Fail-soft: skips silently when SignalRResourceId app setting is unset, logs
warning on Azure Monitor auth/query errors. Function App's managed identity
needs 'Monitoring Reader' on the SignalR resource.

Also called from the manual maintenance trigger so 'Run Maintenance Now'
exercises every health check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 18:06:56 +02:00
Oliver Kieselbach 9af3de15d4 fix(backend/security): pin client cert chain trust to embedded Intune root (v2)
Re-applies 7e21591b after the chain-validation outage and adds the missing
trust anchors. The first attempt embedded only the 2030 root and the 2026
intermediate; current production device certs chain via the 2026 root +
the 2028 intermediate, so chain.Build() returned "unable to get local
issuer certificate" for every legitimate agent and broke enrolment.

What is different in v2:
- Bundle expanded with both Intune roots (exp 2026-08-12 and 2030-09-15)
  and both currently-issuing MDM Device CA intermediates (exp 2026-05-05
  and 2028-09-15). Real device certs chain via 2028-intermediate -> 2030-
  root today; the 2026 generation is kept for any older device certs
  still in flight.
- Loader now globs intune-root-*.pem and intune-intermediate-*.pem from
  embedded resources and dedupes by thumbprint, so a future Intune root
  rotation is a drop-in PEM with no code change. Legacy generic-named
  files (intune-root-ca.pem, intune-intermediate-ca.pem) are duplicates
  of the new ones and harmlessly de-dup at load.
- Adds a happy-path integration test against a real Intune-issued device
  cert (public-key only, copied from a test VM). This is the regression
  test that would have caught the v1 outage and gates any future bundle
  drift in CI.
- Fixes a pre-existing TZ bug in the expiry check: X509Certificate2's
  NotBefore/NotAfter return Local time, were being compared raw against
  DateTime.UtcNow. Surfaced by the integration test on a CEST dev box;
  unobservable in prod because Linux Flex Consumption runs in UTC.

Trust model is unchanged from v1: X509ChainTrustMode.CustomRootTrust
pinned to the embedded Intune roots, OS trust store ignored, fail-closed
when no roots are loaded. Self-signed-with-Intune-DN bypass remains shut.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 15:28:21 +02:00
Oliver Kieselbach 617d63a9c2 chore(backend): security patches + minor bumps on net8.0
Backport security and hygiene fixes that were originally part of the
.NET 10 upgrade (reverted in 7abbd593) but apply equally to net8.0.

Security pins (override vulnerable transitive resolutions):
- System.Formats.Asn1 -> 8.0.2 (GHSA-447r-wph3-92pm, HIGH; was 8.0.0)
- System.Security.Cryptography.Xml -> 8.0.3 (GHSA-37gx-xxp4-5rgx +
  GHSA-w3x6-4m5h-cxqf, HIGH; was 8.0.1)

Minor/patch bumps (TFM-agnostic, surfaced by 'list package --outdated'):
- Azure.Identity 1.19.0 -> 1.21.0
- Azure.Data.Tables 12.9.1 -> 12.11.0
- Azure.Storage.Blobs 12.22.2 -> 12.27.0
- Azure.Storage.Queues 12.23.0 -> 12.25.0
- Microsoft.Azure.SignalR.Management 1.29.0 -> 1.33.0
- Newtonsoft.Json 13.0.3 -> 13.0.4

Skipped (require .NET 10 — defer until Flex stamp picks up runtime):
- Microsoft.Identity.Web 4.3 -> 4.8 (transitively needs ASP.NET Core 10)
- JwtBearer / Http.Polly bumps to 10.0.7 (ASP.NET Core 10 only)
- DataProtection 10.0.7 pin (CVE only affects 10.0.0, not 8.x)
- SYSLIB0057 X509CertificateLoader migration (obsoletion not flagged
  on net8.0 reference assemblies)

'dotnet list package --vulnerable --include-transitive' on net8.0:
  no vulnerable packages reported.
Tests: 781/781 passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 07:46:05 +02:00
Oliver Kieselbach 7abbd59333 revert: roll back .NET 10 upgrade — Flex stamp missing runtime
Reverts:
- 8dacce5c "chore(backend): bump optional NuGet patches + fix SYSLIB0057"
- 2f68fe69 "chore(backend): upgrade Functions from .NET 8 to .NET 10 isolated"

After deploy succeeded and the Flex Consumption stack was switched to
.NET 10 Isolated in the portal, the dotnet-isolated worker died on every
boot with: dotnet exited with code 150 (FrameworkMissingFailure) and
"Exceeded language worker restart retry count for runtime:dotnet-isolated".
The deployed package was correct (TFM net10.0, FrameworkReference 10.0.0,
all package bumps applied) and the runtime stack property was set to
dotnet-isolated 10.0, but the .NET 10 runtime is not present on the
Flex Consumption stamp in West Europe even though Microsoft lists
DOTNET-ISOLATED|10.0 as supported. Restart and stop+start did not help.

Recovery sequence:
1. Stack switched back to .NET 8 Isolated (manual portal action).
2. Re-deployed last known-good net8.0 commit 637bd9a1 via temp branch
   rollback-net8 (now safe to delete).
3. /api/health back to 200.

Follow-ups:
- Re-apply security fixes (DataProtection 10.0.7 pin GHSA-9mv3-2cwr-p262,
  SYSLIB0057 X509CertificateLoader migration) on net8.0 in a separate PR.
- Open Microsoft support: ".NET 10 Isolated worker on Flex Consumption
  WestEurope fails with FrameworkMissingFailure despite stack listed as
  supported."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 07:42:56 +02:00
Oliver Kieselbach 8dacce5ce9 chore(backend): bump optional NuGet patches + fix SYSLIB0057
Follow-up to .NET 10 upgrade (2f68fe69). Picks up safe minor/patch bumps
flagged by 'dotnet list package --outdated' and replaces the obsolete
X509Certificate2(byte[]) ctor with X509CertificateLoader.LoadCertificate.

Package bumps (all minor/patch, no breaking changes):
- Azure.Identity 1.19.0 -> 1.21.0
- Azure.Data.Tables 12.9.1 -> 12.11.0
- Azure.Storage.Blobs 12.22.2 -> 12.27.0
- Azure.Storage.Queues 12.23.0 -> 12.25.0
- Microsoft.Azure.SignalR.Management 1.29.0 -> 1.33.0
- Newtonsoft.Json 13.0.3 -> 13.0.4

Code:
- CertificateValidator.cs:69 swap to X509CertificateLoader.LoadCertificate
  (.NET 9+ replacement for the obsolete byte[] ctor; same behavior, no
  flags/keystore changes — just unblocks SYSLIB0057).

Tests: 781/781 passing on net10.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 07:12:13 +02:00
Oliver Kieselbach 2f68fe69c0 chore(backend): upgrade Functions from .NET 8 to .NET 10 isolated
- Target framework: net8.0 -> net10.0 (Functions + Tests)
- Microsoft.Identity.Web 4.3.0 -> 4.8.0 (transitively pulls AspNetCore 10.x)
- Microsoft.AspNetCore.Authentication.JwtBearer 8.0.11 -> 10.0.7
- Microsoft.Extensions.Http.Polly 8.0.25 -> 10.0.7
- Pin Microsoft.AspNetCore.DataProtection 10.0.7 to override transitive
  10.0.0 (GHSA-9mv3-2cwr-p262 critical, GHSA-37gx-xxp4-5rgx +
  GHSA-w3x6-4m5h-cxqf high in Cryptography.Xml)
- Add global.json pinning SDK 10.0.x latestFeature, allowPrerelease=false
- CI deploy-backend workflow: setup-dotnet 8.0.x -> 10.0.x ga

Tests: 781/781 passing on net10.0.

Runtime stack switch on the Function App ("Stack settings" in portal:
.NET 8 Isolated -> .NET 10 Isolated) must be done manually after deploy.

Out of scope (carry as follow-ups):
- SYSLIB0057 (X509Certificate2 ctor obsolete) in CertificateValidator.cs:69
- Optional bumps surfaced by 'list package --outdated':
  Azure.Identity 1.19->1.21, Azure.Storage.Blobs 12.22->12.27,
  Azure.Data.Tables 12.9->12.11, SignalR.Management 1.29->1.33,
  Newtonsoft.Json 13.0.3->13.0.4. Major bumps deferred:
  ApplicationInsights.WorkerService 2.x->3.x, Resend 0.2->0.5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 07:08:28 +02:00
Oliver Kieselbach a523192dc7 backend fix buildtime 2026-04-25 12:26:34 +02:00
Oliver Kieselbach 42ff4284d8 added version info to backend 2026-04-22 19:36:19 +02:00
Oliver Kieselbach 8e39299da5 feat(backend): M5.d.3 queue-triggered index reconcile consumer
Consumes the telemetry-index-reconcile queue (M5.d.2) and fans each
envelope out into 0–3 index-table rows via IIndexTableRepository
(M5.d.1). Closes the producer→consumer loop; once the
EnableIndexDualWrite flag is flipped, primary commits drive
eventual-consistent index upserts asynchronously.

Components:
- Microsoft.Azure.Functions.Worker.Extensions.Storage.Queues 5.5.4
  package for the isolated-worker QueueTrigger binding.
- IndexReconcileHandler (plain class — not interface-abstracted per
  Plan §M5.d design: DAL abstraction lives at IIndexTableRepository,
  not here). Routing:
    Signal → SignalsByKind (always, 1 row).
    DecisionTransition:
      IsTerminal=true          → SessionsByTerminal
      Taken=true               → SessionsByStage
      Taken=false + reason     → DeadEndsByReason
      ClassifierVerdictId!=null → ClassifierVerdictsByIdLevel
  Up to 3 rows per DecisionTransition envelope (a Taken+terminal+
  classifier transition fans to SessionsByTerminal + SessionsByStage
  + ClassifierVerdictsByIdLevel).
- IndexReconcileFunction thin QueueTrigger wrapper
  (Connection = AzureTableStorageConnectionString). Malformed JSON
  logged + dropped to avoid poison-queue retry loops; transient
  repo errors propagate so the platform retries.
- DI: handler registered as Singleton in Program.cs.

Failure semantics:
- Required-field-missing envelopes (Signal without SessionSignalOrdinal
  or SignalKind; DecisionTransition without StepIndex) → log warn,
  drop. These won't become valid on retry — avoiding the retry loop.
- Transient Table Storage errors → rethrow. QueueTrigger platform
  retries up to 5 times by default, then moves to
  telemetry-index-reconcile-poison for manual investigation.

Deferred to M5.d.4:
- SessionsByStage stale-row pruning: when a session moves from
  Stage-X→Stage-Y, the old Stage-X row stays. Either query-side
  dedupe (per SessionId, max-Ticks RK) or reconcile-timer pruning
  handles this. Deliberately not solved in the handler to avoid
  a query+delete round-trip per Taken transition.
- End-to-end dual-write verification tests (Azurite harness) are
  the M5.d.4 release gate before the flag is flipped.

Tests (+10 in Functions) via capture-only FakeIndexTableRepository:
- Signal happy path (1 SignalsByKind row).
- Signal missing SessionSignalOrdinal / SignalKind → 0 rows (x2).
- DecisionTransition Terminal+Taken+Classifier → 3 rows.
- DecisionTransition Taken non-terminal no-classifier → 1 row
  (SessionsByStage only).
- DecisionTransition dead-end with classifier → 2 rows
  (DeadEndsByReason + ClassifierVerdictsByIdLevel).
- DecisionTransition dead-end without reason → 0 rows (PK-collision
  defense).
- DecisionTransition missing StepIndex → 0 rows (all paths gated).
- Unknown SourceKind → 0 rows.
- Null envelope → 0 rows.

Full Functions suite: 682 green (prev 672 + 10 new). Full solution
build green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 12:03:06 +02:00
Oliver Kieselbach c3ee0cad55 feat(backend): M5.d.2 index-reconcile queue producer + feature flag
Adds the async fan-out producer path from Plan §2.8 / §M5.d so primary
Signals + DecisionTransitions rows can be projected onto their index
tables without blocking the hot ingest loop.

Components:
- Constants.QueueNames.TelemetryIndexReconcile (azure-queue-name-safe,
  all lowercase + hyphens).
- IndexReconcileEnvelope (Shared DTO, flat shape with nullable fields
  per SourceKind="Signal"/"DecisionTransition"). Avoids polymorphic
  Newtonsoft deserialization — see M5.c.3 finding.
- IndexReconcileEnvelopeFactory pure static helper (FromSignal /
  FromDecisionTransition / BuildBatch) — state-less projection.
- IIndexReconcileProducer (Shared interface) contract: no-op when
  the feature flag is off; never rethrows underlying-queue failures.
- AzureQueueIndexReconcileProducer (Functions) — Base64-encoded JSON
  messages, lazy CreateIfNotExistsAsync guarded by Interlocked flag,
  Managed-Identity-or-connection-string construction mirroring
  BlobStorageService / TableStorageService.
- IngestTelemetryFunction ctor gains IIndexReconcileProducer; after
  primary Signals + DecisionTransitions commit it builds envelopes
  and enqueues them. Event path is untouched (events don't feed the
  index tables per §2.8).
- Program.cs registers the producer as a singleton.

Feature gate (AdminConfiguration.EnableIndexDualWrite, default false):
- New bool field in AdminConfiguration.
- TableConfigRepository Store + Map extension (per
  feedback_table_storage_serialization).
- Producer checks config each call via AdminConfigurationService's
  5-min memory cache. Flag off → primary behaviour is bit-exact.

Fault tolerance: queue-side exceptions (network, throttling, missing
queue before first CreateIfNotExists) are logged per envelope and
swallowed. The primary row is the source of truth; the 2h reconcile
timer (M5.d.4) is the safety-net recovery path.

Tests (+11 in Functions):
- 5 IndexReconcileEnvelopeFactoryTests: Signal/DecisionTransition
  projection, dead-end preservation, batch ordering, null/empty.
- 3 IndexReconcileEnvelopeSerializationTests: Newtonsoft round-trip
  (Signal, full DecisionTransition, minimal-envelope tolerance).
- 3 AdminConfigurationEnableIndexDualWriteTests: default-false on
  new config + CreateDefault, persists when set.

Deferred to later sub-milestones:
- M5.d.3: queue-triggered consumer that maps envelopes to 0–3 index
  rows using M5.d.1 TableIndexRepository.
- M5.d.4: 2h reconcile timer + release-gate dual-write verification
  tests, then flip EnableIndexDualWrite=true in prod admin config.

Full Functions suite: 672 green (prev 661 + 11 new). Full solution
build green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 11:48:27 +02:00
Oliver Kieselbach 830a27bb70 refactor(decisioncore): add DecisionCore shared library scaffolding (M1)
New netstandard2.0 project src/Shared/AutopilotMonitor.DecisionCore/ with
14 scaffolding types across Signals/, State/, Classifiers/, Engine/.
Agent.Core and Functions reference the library without call-sites; the
full solution builds 0 errors, 0 warnings. DecisionEngine.Reduce is an
intentional NotImplementedException stub — M3 fills in the partial-class
reducer.

Invariants wired into the scaffolding:
 - Immutable DTOs (sealed + get-only + With-methods) per L.3
 - Pflicht-Evidence validation in Evidence ctor per L.5
 - KindSchemaVersion enforcement per L.6
 - AssemblyVersion 2.0.0.0 as ReducerVersion source per §2.10

Architecture principles L.1–L.18 extracted to
.claude/architecture-principles.md as persistent Claude-session context.

Plan: plans/REFACTOR_AGENT_V2.md §4 M1. Zero runtime risk.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 13:29:19 +02:00
Oliver Kieselbach 68818ce4d1 added column select picker 2026-04-07 16:25:58 +02:00
Oliver Kieselbach a6f552a877 added MCP capabilities 2026-03-28 04:20:50 +01:00
Oliver Kieselbach 769beaa14a Refactorings for many reusables, API URL builder, CollectorBaseClass, CorrelationID Middleware, added Polly for external APIs 2026-03-27 11:14:38 +01:00
Oliver Kieselbach 8e0bf00079 simplified seeding 2026-03-21 00:16:25 +01:00
Oliver Kieselbach f1d653e60e feat: enhanced software inventory and vulnerability report 2026-03-20 19:01:16 +01:00
Oliver Kieselbach 26e5925672 feat: Software Inventory Analyzer with Vulnerability Correlation 2026-03-20 14:43:58 +01:00
Oliver Kieselbach eb1e5c6041 added preview email notification service 2026-03-09 19:29:11 +01:00
Oliver Kieselbach 1f796bf6e3 feat: externalize rules to GitHub + API consolidation + preview mode
## Rules as GitHub single source of truth
- Add 75 individual JSON rule files (2 gather, 18 analyze, 55 IME patterns)
  under rules/gather/, rules/analyze/, rules/ime-log-patterns/
- Add JSON Schemas for all three rule types with VS Code IntelliSense support
- Add CI combine script (rules/scripts/combine.js) + GitHub Action that
  validates schemas with ajv-cli and generates rules/dist/ combined files
- Convert BuiltIn*.cs from hardcoded C# objects to embedded JSON resource loaders
- Add GitHubRuleRepository service to fetch dist/ files from raw.githubusercontent.com
- Add ReseedFromGitHubFunction: POST /api/rules/reseed-from-github?type=all|gather|analyze|ime

## IME Log Patterns
- Extend ImeLogPattern model with Description and IsBuiltIn properties
- Add ImeLogPatterns table + API constants
- Add TableStorageService CRUD methods for IME log patterns
- Add ImeLogPatternService (CRUD, merge, auto-seed from embedded JSON)
- Add ImeLogPatternsFunction: GET/PUT /api/rules/ime-log-patterns (Phase 1: read-only
  for tenants, Galactic Admin can enable/disable/edit globally)
- Add IME Log Patterns management page (/ime-log-patterns)
- Wire GetAgentConfigFunction to use ImeLogPatternService instead of static class

## API route consolidation
- All rule endpoints now under /api/rules/* for single certificate exclusion path:
  /api/rules/gather, /api/rules/analyze, /api/rules/ime-log-patterns,
  /api/rules/reseed-from-github

## Export buttons
- Add per-rule and export-all JSON download buttons to gather-rules,
  analyze-rules, and ime-log-patterns pages

## Admin configuration
- Replace old individual reseed buttons with unified "Fetch & Reseed from GitHub"
  (calls new GitHub endpoint) plus compact individual-type reseed buttons
- Add CDN cache hint: GitHub raw.githubusercontent.com caches up to 5 minutes

## Preview mode
- ?preview=1 URL param seeds localStorage.previewMode (persists across navigation)
- ?preview=0 clears it; toggle in Navbar settings dropdown (indigo, eye icon)
- IME Log Patterns nav link gated behind galacticAdminMode OR previewMode
- Preview badge shown on nav link when active for normal users

## Tests
- Add xUnit test project (net8.0) with 5 tests for embedded resource loading,
  known IME actions validation, and valid categories check

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 13:12:09 +01:00
Oliver Kieselbach 4591d05e65 embedded Intune root + intermediate certs for chain validation 2026-02-11 11:51:30 +01:00
Oliver Kieselbach 5690620058 Initial commit 2026-02-05 06:14:41 +01:00