Skip to content

Development

Rod Christiansen edited this page Oct 8, 2026 · 3 revisions

Development

Getting the agent building, tested and running on a development machine.

Setup

git clone https://github.com/reportmate/reportmate-client-win.git
cd reportmate-client-win

You need PowerShell 7+, the .NET 10 SDK, and Windows. osquery is only needed if you want a module to return real rows.

Build once through the script — this is not optional:

.\build.ps1 -NoSign -SkipMSI -SkipNUPKG -SkipZIP -Configuration Release

build.ps1 generates src/Generated/ModuleVersions.g.cs from git history before compiling. A bare dotnet build on a clean checkout fails because that file does not exist yet. Once it has been generated, incremental dotnet build src/ReportMate.WindowsClient.csproj works normally.

Tests

dotnet test tests\ReportMate.WindowsClient.Tests\ReportMate.WindowsClient.Tests.csproj --configuration Release

The suite is unit-level and covers the parts where a silent wrong answer is expensive: settings precedence, the trusted-settings-file check, credential migration into the protected store, Prefs elevation and which settings a save writes, idempotency key derivation, unified payload bundling, transmission log retention, bounded child processes, hardware serial selection, storage volume letters and the storage-analysis cache, DNS-correlated management addresses, install event messages, usage launch counting, tracker merging and app-name resolution, monitor EDID parsing, USB serial matching for graphics tablets, policy registry grouping, managed log survey parsing, run-log location for the app, source-OS markers, and TPM manufacturer NUL stripping.

Fixtures live in tests/ReportMate.WindowsClient.Tests/Fixtures/ and are hand-authored. Do not add captured endpoint data as a fixture — provenance from a real machine is not a virtue here, and it is not publishable.

The test project uses InternalsVisibleTo from the client, so internal helpers are directly testable.

Repository layout

Path What is in it
src/Program.cs Entry point, CLI, configuration layering, DI
src/Configuration/ Settings layering (SettingsLoader), trusted-file check, typed settings
src/Shared/ Code compiled into both the runner and the app: registry key paths, credential store, legacy-key migration, Prefs rules, log reading
src/Models/Modules/ The payload shape, one file per module
src/Models/JsonContext.cs Source-generated System.Text.Json context
src/Services/ osquery, API, WMI, PowerShell, EDID, usage, logging
src/Services/Modules/ One processor per collection module
src/App/ The Managed Reports Runner App (WinUI 3). Built by build.ps1, not in the solution
usagetracker/ Per-user session tracker
build/resources/osquery/ Query packs and enabled-modules.json
build/resources/ module-schedules.json, task install and uninstall scripts, the Cimian postflight, Speedtest CLI
build/pkg/, build/nupkg/ cimipkg package projects and install scripts
tests/ xUnit tests and fixtures

Running locally

Collect a single module and print it without touching the network:

dotnet run --project src/ReportMate.WindowsClient.csproj -- --run-module system --collect-only -vvv

Or run the published binary directly:

.\.publish\managedreportsrunner.exe --run-module network --collect-only -vvv

Run from an elevated shell. Credentials live in a registry key only an elevated process can read, so an unelevated run sends no passphrase or API key. Modules that read the registry under HKLM, query WMI, or read the Security event log need elevation too. Several also behave differently as SYSTEM than as an interactive user — the displays code exists precisely because HKLM\SYSTEM\CurrentControlSet\Enum\DISPLAY returns nothing to osquery running as SYSTEM. If a result looks wrong, check which identity you are running under before assuming a bug.

The --collect-only output is also written to C:\ProgramData\ManagedReports\cache\<timestamp>\<module>.json, which is usually easier to read than terminal output.

Adding a module

There are four coupled pieces:

  1. build/resources/osquery/modules/<id>.json — the query pack.
  2. src/Models/Modules/<Name>Models.cs — the model, deriving from BaseModuleData.
  3. src/Services/Modules/<Name>ModuleProcessor.cs — deriving from BaseModuleProcessor<T>, implementing ProcessModuleAsync and optionally GenerateEventsAsync and ValidateModuleDataAsync.
  4. Registration in three places: ConfigureServices() in Program.cs, both dictionaries in ModuleProcessorFactory, and the moduleFileMap in build.ps1 so the module gets a version.

Add the id to enabled-modules.json, to module-schedules.json if it should run on a schedule, and to ReportMateConstants.AllModules in src/App/ so the app's Run tab lists it. Add the property to UnifiedDevicePayload and to the JSON source-generation context.

JSON names are mostly not written by hand. Apart from the peripherals model, the Cimian session records in InstallsModels.cs and two usage fields in ApplicationsModels.cs, the module models carry no [JsonPropertyName] attributes — the serializer's camelCase naming policy derives the name from the C# property, so TotalPhysical becomes totalPhysical and ScopeID becomes scopeID. Renaming a property renames a payload field.

The project is published trimmed, so reflection-heavy code needs care. Serilog and System.Management are already rooted against the trimmer.

Conventions

  • Branch from origin/main as <type>/<slug>, type being feature, fix, chore, ci or docs. Never push main directly.
  • Plain imperative commit subjects. No emoji, no bracketed tags, no Co-Authored-By trailers.
  • Track work with GitHub issues in this repo; close from a pull request with Closes #<n>.
  • Never commit collected endpoint data, .env files, keys or certificates.

Clone this wiki locally