Initial Commit
This commit is contained in:
145
PatchProbe.Cli/Collectors/WindowsUpdateCollector.cs
Normal file
145
PatchProbe.Cli/Collectors/WindowsUpdateCollector.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PatchProbe.Shared.Contracts;
|
||||
using PatchProbe.Shared.Models;
|
||||
|
||||
namespace PatchProbe.Cli.Collectors;
|
||||
|
||||
public sealed class WindowsUpdateCollector(ILogger<WindowsUpdateCollector> logger) : ICollector<WindowsUpdateInfo>
|
||||
{
|
||||
public Task<WindowsUpdateInfo> CollectAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
logger.LogInformation("Collecting Windows Update information via WUA COM");
|
||||
|
||||
var applicable = new List<ApplicableUpdate>();
|
||||
var history = new List<UpdateHistoryEntry>();
|
||||
string? searchError = null;
|
||||
|
||||
try
|
||||
{
|
||||
var sessionType = Type.GetTypeFromProgID("Microsoft.Update.Session")
|
||||
?? throw new InvalidOperationException("Microsoft.Update.Session ProgID not found");
|
||||
dynamic session = Activator.CreateInstance(sessionType)!;
|
||||
dynamic searcher = session.CreateUpdateSearcher();
|
||||
|
||||
logger.LogInformation("Searching for applicable updates (IsInstalled=0 and IsHidden=0)");
|
||||
dynamic result = searcher.Search("IsInstalled=0 and IsHidden=0");
|
||||
|
||||
for (int i = 0; i < result.Updates.Count; i++)
|
||||
{
|
||||
dynamic u = result.Updates.Item(i);
|
||||
string kbId = ExtractKbId(u);
|
||||
string category = ExtractFirstCategory(u);
|
||||
|
||||
applicable.Add(new ApplicableUpdate
|
||||
{
|
||||
UpdateId = TryGet<string>(() => u.Identity.UpdateID),
|
||||
Title = TryGet<string>(() => u.Title),
|
||||
KbArticleId = kbId,
|
||||
Category = category,
|
||||
Severity = TryGet<string>(() => u.MsrcSeverity),
|
||||
RebootRequired = TryGet<bool>(() => u.RebootRequired),
|
||||
IsDownloaded = TryGet<bool>(() => u.IsDownloaded),
|
||||
Description = TryGet<string>(() => u.Description),
|
||||
SupportUrl = TryGet<string>(() => u.SupportUrl),
|
||||
});
|
||||
}
|
||||
|
||||
logger.LogInformation("Found {Count} applicable updates", applicable.Count);
|
||||
|
||||
int totalHistoryCount = searcher.GetTotalHistoryCount();
|
||||
int historyLimit = Math.Min(totalHistoryCount, 100);
|
||||
|
||||
if (historyLimit > 0)
|
||||
{
|
||||
dynamic historyEntries = searcher.QueryHistory(0, historyLimit);
|
||||
for (int i = 0; i < historyEntries.Count; i++)
|
||||
{
|
||||
dynamic e = historyEntries.Item(i);
|
||||
history.Add(new UpdateHistoryEntry
|
||||
{
|
||||
UpdateId = TryGet<string>(() => e.UpdateIdentity.UpdateID),
|
||||
Title = TryGet<string>(() => e.Title),
|
||||
KbArticleId = ExtractKbIdFromTitle(TryGet<string>(() => e.Title)),
|
||||
ResultCode = TryGet<int>(() => (int)e.ResultCode),
|
||||
HResult = TryGet<int>(() => e.HResult),
|
||||
Date = TryGetDate(() => e.Date),
|
||||
Operation = TryGet<int>(() => (int)e.Operation) switch
|
||||
{
|
||||
1 => "Installation",
|
||||
2 => "Uninstallation",
|
||||
_ => "Unknown"
|
||||
},
|
||||
ServerSelection = TryGet<int>(() => (int)e.ServerSelection) switch
|
||||
{
|
||||
0 => "Default",
|
||||
1 => "ManagedServer",
|
||||
2 => "WindowsUpdate",
|
||||
3 => "Others",
|
||||
_ => "Unknown"
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "WUA search failed");
|
||||
searchError = ex.Message;
|
||||
}
|
||||
|
||||
return Task.FromResult(new WindowsUpdateInfo
|
||||
{
|
||||
ApplicableUpdates = applicable,
|
||||
History = history,
|
||||
SearchError = searchError,
|
||||
});
|
||||
}
|
||||
|
||||
private static string ExtractKbId(dynamic update)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (update.KBArticleIDs.Count > 0)
|
||||
return "KB" + update.KBArticleIDs.Item(0);
|
||||
}
|
||||
catch { /* fall through to title parse */ }
|
||||
return ExtractKbIdFromTitle(TryGet<string>(() => update.Title));
|
||||
}
|
||||
|
||||
private static string ExtractFirstCategory(dynamic update)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (update.Categories.Count > 0)
|
||||
return update.Categories.Item(0).Name;
|
||||
}
|
||||
catch { }
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static string ExtractKbIdFromTitle(string? title)
|
||||
{
|
||||
if (string.IsNullOrEmpty(title)) return string.Empty;
|
||||
var match = System.Text.RegularExpressions.Regex.Match(title, @"KB\d+", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||||
return match.Success ? match.Value.ToUpperInvariant() : string.Empty;
|
||||
}
|
||||
|
||||
private static T TryGet<T>(Func<T> getter)
|
||||
{
|
||||
try { return getter(); }
|
||||
catch { return default!; }
|
||||
}
|
||||
|
||||
private static DateTimeOffset? TryGetDate(Func<object> getter)
|
||||
{
|
||||
try
|
||||
{
|
||||
var raw = getter();
|
||||
if (raw is DateTime dt) return new DateTimeOffset(dt, TimeSpan.Zero);
|
||||
if (raw is DateTimeOffset dto) return dto;
|
||||
if (DateTime.TryParse(raw?.ToString(), out var parsed)) return new DateTimeOffset(parsed, TimeSpan.Zero);
|
||||
}
|
||||
catch { }
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user