Initial Commit
This commit is contained in:
244
PatchProbe.Cli/Program.cs
Normal file
244
PatchProbe.Cli/Program.cs
Normal file
@@ -0,0 +1,244 @@
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Invocation;
|
||||
using System.Security.Principal;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using PatchProbe.Cli.Auth;
|
||||
using PatchProbe.Cli.Collectors;
|
||||
using PatchProbe.Cli.Services;
|
||||
using PatchProbe.Shared.Contracts;
|
||||
using PatchProbe.Shared.Models;
|
||||
using PatchProbe.Shared.Serialization;
|
||||
using Serilog;
|
||||
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Information()
|
||||
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
|
||||
.WriteTo.File("logs/patchprobe.log", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7)
|
||||
.CreateLogger();
|
||||
|
||||
try
|
||||
{
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
builder.Services.AddSerilog();
|
||||
builder.Services.AddHttpClient();
|
||||
|
||||
// Collectors
|
||||
builder.Services.AddSingleton<DeviceCollector>();
|
||||
builder.Services.AddSingleton<OsCollector>();
|
||||
builder.Services.AddSingleton<WindowsUpdateCollector>();
|
||||
builder.Services.AddSingleton<PendingRebootCollector>();
|
||||
builder.Services.AddSingleton<DriverCollector>();
|
||||
builder.Services.AddSingleton<CbsDismCollector>();
|
||||
builder.Services.AddSingleton<WindowsUpdatePolicyCollector>();
|
||||
builder.Services.AddSingleton<HotfixCollector>();
|
||||
builder.Services.AddSingleton<EventCollector>();
|
||||
|
||||
// Auth
|
||||
builder.Services.AddSingleton<IDeviceCredentialStore, DpapiCredentialStore>();
|
||||
builder.Services.AddSingleton<IRequestAuthenticator, EcdsaRequestAuthenticator>();
|
||||
|
||||
// Services
|
||||
builder.Services.AddSingleton<EnrollmentService>();
|
||||
builder.Services.AddSingleton<PayloadUploader>();
|
||||
builder.Services.AddSingleton<IPayloadUploader>(sp => sp.GetRequiredService<PayloadUploader>());
|
||||
|
||||
var host = builder.Build();
|
||||
|
||||
var rootCommand = new RootCommand("PatchProbe — Windows patch-management evidence collector");
|
||||
|
||||
// ── scan ──────────────────────────────────────────────────────────────
|
||||
var scanCommand = new Command("scan", "Collect patch-management evidence from this endpoint");
|
||||
var outputOption = new Option<string?>("--output") { Description = "Path to write the JSON payload to disk" };
|
||||
var noUploadOption = new Option<bool>("--no-upload") { Description = "Collect evidence and print JSON to stdout instead of uploading" };
|
||||
var serverUrlOption = new Option<string?>("--server-url") { Description = "PatchProbe server URL to upload to (overrides enrolled server URL)" };
|
||||
scanCommand.Add(outputOption);
|
||||
scanCommand.Add(noUploadOption);
|
||||
scanCommand.Add(serverUrlOption);
|
||||
|
||||
scanCommand.SetAction(async (ParseResult parseResult, CancellationToken ct) =>
|
||||
{
|
||||
var output = parseResult.GetValue(outputOption);
|
||||
var noUpload = parseResult.GetValue(noUploadOption);
|
||||
var serverUrlArg = parseResult.GetValue(serverUrlOption);
|
||||
|
||||
if (!IsAdministrator())
|
||||
Log.Warning("Not running as administrator — some data sources may be unavailable");
|
||||
|
||||
var sp = host.Services;
|
||||
var deviceCollector = sp.GetRequiredService<DeviceCollector>();
|
||||
var osCollector = sp.GetRequiredService<OsCollector>();
|
||||
var wuCollector = sp.GetRequiredService<WindowsUpdateCollector>();
|
||||
var rebootCollector = sp.GetRequiredService<PendingRebootCollector>();
|
||||
var driverCollector = sp.GetRequiredService<DriverCollector>();
|
||||
var cbsCollector = sp.GetRequiredService<CbsDismCollector>();
|
||||
var policyCollector = sp.GetRequiredService<WindowsUpdatePolicyCollector>();
|
||||
var hotfixCollector = sp.GetRequiredService<HotfixCollector>();
|
||||
var eventCollector = sp.GetRequiredService<EventCollector>();
|
||||
var uploader = sp.GetRequiredService<PayloadUploader>();
|
||||
var credStore = sp.GetRequiredService<IDeviceCredentialStore>();
|
||||
|
||||
Log.Information("Starting PatchProbe scan on {Machine}", Environment.MachineName);
|
||||
|
||||
var (device, os, reboot, drivers, cbs, hotfixes, events, policy) = await CollectAllAsync(
|
||||
deviceCollector, osCollector, rebootCollector, driverCollector, cbsCollector, hotfixCollector, eventCollector, policyCollector, ct);
|
||||
|
||||
var wuRaw = await wuCollector.CollectAsync(ct);
|
||||
var wuInfo = new WindowsUpdateInfo
|
||||
{
|
||||
ApplicableUpdates = wuRaw.ApplicableUpdates,
|
||||
History = wuRaw.History,
|
||||
SearchError = wuRaw.SearchError,
|
||||
Policy = policy,
|
||||
};
|
||||
|
||||
var payload = new PatchProbePayload
|
||||
{
|
||||
Collector = new CollectorMeta
|
||||
{
|
||||
CollectedAt = DateTimeOffset.UtcNow,
|
||||
MachineName = Environment.MachineName,
|
||||
RanAsAdministrator = IsAdministrator(),
|
||||
},
|
||||
Device = device,
|
||||
Os = os,
|
||||
PendingReboot = reboot,
|
||||
WindowsUpdate = wuInfo,
|
||||
InstalledHotfixes = hotfixes,
|
||||
CbsPackages = cbs,
|
||||
Drivers = drivers,
|
||||
RecentUpdateEvents = events,
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(output))
|
||||
{
|
||||
await PayloadSerializer.SerializeToFileAsync(payload, output, ct);
|
||||
Log.Information("Payload written to {Path}", output);
|
||||
}
|
||||
else if (!noUpload)
|
||||
{
|
||||
// Resolve server URL: CLI arg → enrolled server URL → local write
|
||||
var effectiveServerUrl = serverUrlArg
|
||||
?? (credStore.IsEnrolled ? credStore.Load().ServerUrl : null);
|
||||
|
||||
if (effectiveServerUrl is null)
|
||||
Log.Warning("No server URL configured — writing locally. Run 'PatchProbe.exe enroll' to register this device.");
|
||||
|
||||
await uploader.UploadAsync(payload, effectiveServerUrl, ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
var json = PayloadSerializer.Serialize(payload);
|
||||
Console.WriteLine(json);
|
||||
}
|
||||
|
||||
Log.Information("Scan complete. Applicable updates: {Count}, Pending reboot: {Reboot}",
|
||||
wuInfo.ApplicableUpdates.Count, reboot?.AnyPending);
|
||||
});
|
||||
|
||||
// ── enroll ────────────────────────────────────────────────────────────
|
||||
var enrollCommand = new Command("enroll", "Register this device with the PatchProbe server");
|
||||
var enrollServerUrlOption = new Option<string?>("--server-url") { Description = "PatchProbe server URL (required)" };
|
||||
var enrollmentKeyOption = new Option<string?>("--enrollment-key") { Description = "Enrollment key provided by your administrator (required)" };
|
||||
var forceOption = new Option<bool>("--force") { Description = "Re-enroll even if this device is already enrolled" };
|
||||
enrollCommand.Add(enrollServerUrlOption);
|
||||
enrollCommand.Add(enrollmentKeyOption);
|
||||
enrollCommand.Add(forceOption);
|
||||
|
||||
enrollCommand.SetAction(async (ParseResult parseResult, CancellationToken ct) =>
|
||||
{
|
||||
var serverUrl = parseResult.GetValue(enrollServerUrlOption);
|
||||
var enrollmentKey = parseResult.GetValue(enrollmentKeyOption);
|
||||
var force = parseResult.GetValue(forceOption);
|
||||
|
||||
if (string.IsNullOrEmpty(serverUrl) || string.IsNullOrEmpty(enrollmentKey))
|
||||
{
|
||||
Log.Error("--server-url and --enrollment-key are required");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsAdministrator())
|
||||
Log.Warning("Not running as administrator — credential storage to %ProgramData% may fail");
|
||||
|
||||
var sp = host.Services;
|
||||
var credStore = sp.GetRequiredService<IDeviceCredentialStore>();
|
||||
|
||||
if (credStore.IsEnrolled && !force)
|
||||
{
|
||||
Log.Warning("Device is already enrolled. Use --force to re-enroll and generate a new keypair.");
|
||||
return;
|
||||
}
|
||||
|
||||
var enrollmentService = sp.GetRequiredService<EnrollmentService>();
|
||||
await enrollmentService.EnrollAsync(serverUrl, enrollmentKey, ct);
|
||||
});
|
||||
|
||||
// ── unenroll ──────────────────────────────────────────────────────────
|
||||
var unenrollCommand = new Command("unenroll", "Remove stored device credentials from this endpoint");
|
||||
unenrollCommand.SetAction((ParseResult _, CancellationToken _) =>
|
||||
{
|
||||
if (!IsAdministrator())
|
||||
Log.Warning("Not running as administrator — credential removal may fail");
|
||||
|
||||
var credStore = host.Services.GetRequiredService<IDeviceCredentialStore>();
|
||||
if (!credStore.IsEnrolled)
|
||||
{
|
||||
Log.Information("Device is not enrolled — nothing to remove");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
credStore.Delete();
|
||||
Log.Information("Device credentials removed from {Path}", DpapiCredentialStore.StorePath);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
rootCommand.Add(scanCommand);
|
||||
rootCommand.Add(enrollCommand);
|
||||
rootCommand.Add(unenrollCommand);
|
||||
|
||||
var parseResult = rootCommand.Parse(args, new ParserConfiguration());
|
||||
return await parseResult.InvokeAsync(new InvocationConfiguration());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Fatal(ex, "Unhandled exception");
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await Log.CloseAndFlushAsync();
|
||||
}
|
||||
|
||||
static async Task<(DeviceInfo, OsInfo, PendingRebootInfo, List<DriverInfo>, List<CbsPackage>, List<InstalledHotfix>, List<UpdateEvent>, WindowsUpdatePolicy)>
|
||||
CollectAllAsync(
|
||||
DeviceCollector deviceCollector,
|
||||
OsCollector osCollector,
|
||||
PendingRebootCollector rebootCollector,
|
||||
DriverCollector driverCollector,
|
||||
CbsDismCollector cbsCollector,
|
||||
HotfixCollector hotfixCollector,
|
||||
EventCollector eventCollector,
|
||||
WindowsUpdatePolicyCollector policyCollector,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var deviceTask = deviceCollector.CollectAsync(ct);
|
||||
var osTask = osCollector.CollectAsync(ct);
|
||||
var rebootTask = rebootCollector.CollectAsync(ct);
|
||||
var driverTask = driverCollector.CollectAsync(ct);
|
||||
var cbsTask = cbsCollector.CollectAsync(ct);
|
||||
var hotfixTask = hotfixCollector.CollectAsync(ct);
|
||||
var eventTask = eventCollector.CollectAsync(ct);
|
||||
var policyTask = policyCollector.CollectAsync(ct);
|
||||
|
||||
await Task.WhenAll(deviceTask, osTask, rebootTask, driverTask, cbsTask, hotfixTask, eventTask, policyTask);
|
||||
|
||||
return (deviceTask.Result, osTask.Result, rebootTask.Result, driverTask.Result,
|
||||
cbsTask.Result, hotfixTask.Result, eventTask.Result, policyTask.Result);
|
||||
}
|
||||
|
||||
static bool IsAdministrator()
|
||||
{
|
||||
using var identity = WindowsIdentity.GetCurrent();
|
||||
var principal = new WindowsPrincipal(identity);
|
||||
return principal.IsInRole(WindowsBuiltInRole.Administrator);
|
||||
}
|
||||
Reference in New Issue
Block a user