146 lines
5.4 KiB
C#
146 lines
5.4 KiB
C#
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;
|
|
}
|
|
}
|