Files
Fimeg e40cb14945 Fix module paths for GitHub repository structure
- Update go.mod files to use github.com/Fimeg/RedFlag module path
- Fix all import statements across server and agent code
- Resolves build errors when cloning from GitHub
- Utils package (version comparison) is actually needed and working
2025-10-29 11:53:20 -04:00

553 lines
16 KiB
Go

//go:build windows
// +build windows
package scanner
import (
"fmt"
"runtime"
"strings"
"time"
"github.com/Fimeg/RedFlag/aggregator-agent/internal/client"
"github.com/Fimeg/RedFlag/aggregator-agent/pkg/windowsupdate"
"github.com/go-ole/go-ole"
"github.com/scjalliance/comshim"
)
// WindowsUpdateScannerWUA scans for Windows updates using the Windows Update Agent (WUA) API
type WindowsUpdateScannerWUA struct{}
// NewWindowsUpdateScannerWUA creates a new Windows Update scanner using WUA API
func NewWindowsUpdateScannerWUA() *WindowsUpdateScannerWUA {
return &WindowsUpdateScannerWUA{}
}
// IsAvailable checks if WUA scanner is available on this system
func (s *WindowsUpdateScannerWUA) IsAvailable() bool {
// Only available on Windows
return runtime.GOOS == "windows"
}
// Scan scans for available Windows updates using the Windows Update Agent API
func (s *WindowsUpdateScannerWUA) Scan() ([]client.UpdateReportItem, error) {
if !s.IsAvailable() {
return nil, fmt.Errorf("WUA scanner is only available on Windows")
}
// Initialize COM
comshim.Add(1)
defer comshim.Done()
ole.CoInitializeEx(0, ole.COINIT_APARTMENTTHREADED|ole.COINIT_SPEED_OVER_MEMORY)
defer ole.CoUninitialize()
// Create update session
session, err := windowsupdate.NewUpdateSession()
if err != nil {
return nil, fmt.Errorf("failed to create Windows Update session: %w", err)
}
// Create update searcher
searcher, err := session.CreateUpdateSearcher()
if err != nil {
return nil, fmt.Errorf("failed to create update searcher: %w", err)
}
// Search for available updates (IsInstalled=0 means not installed)
searchCriteria := "IsInstalled=0 AND IsHidden=0"
result, err := searcher.Search(searchCriteria)
if err != nil {
return nil, fmt.Errorf("failed to search for updates: %w", err)
}
// Convert results to our format
updates := s.convertWUAResult(result)
return updates, nil
}
// convertWUAResult converts WUA search results to our UpdateReportItem format
func (s *WindowsUpdateScannerWUA) convertWUAResult(result *windowsupdate.ISearchResult) []client.UpdateReportItem {
var updates []client.UpdateReportItem
updatesCollection := result.Updates
if updatesCollection == nil {
return updates
}
for _, update := range updatesCollection {
if update == nil {
continue
}
updateItem := s.convertWUAUpdate(update)
updates = append(updates, *updateItem)
}
return updates
}
// convertWUAUpdate converts a single WUA update to our UpdateReportItem format
func (s *WindowsUpdateScannerWUA) convertWUAUpdate(update *windowsupdate.IUpdate) *client.UpdateReportItem {
// Get update information
title := update.Title
description := update.Description
kbArticles := s.getKBArticles(update)
updateIdentity := update.Identity
// Use MSRC severity if available (more accurate than category-based detection)
severity := s.mapMsrcSeverity(update.MsrcSeverity)
if severity == "" {
severity = s.determineSeverityFromCategories(update)
}
// Get version information with improved parsing
currentVersion, availableVersion := s.parseVersionFromTitle(title)
// Get version information
maxDownloadSize := update.MaxDownloadSize
estimatedSize := s.getEstimatedSize(update)
// Create metadata with WUA-specific information
metadata := map[string]interface{}{
"package_manager": "windows_update",
"detected_via": "wua_api",
"kb_articles": kbArticles,
"update_identity": updateIdentity.UpdateID,
"revision_number": updateIdentity.RevisionNumber,
"search_criteria": "IsInstalled=0 AND IsHidden=0",
"download_size": maxDownloadSize,
"estimated_size": estimatedSize,
"api_source": "windows_update_agent",
"scan_timestamp": time.Now().Format(time.RFC3339),
}
// Add MSRC severity if available
if update.MsrcSeverity != "" {
metadata["msrc_severity"] = update.MsrcSeverity
}
// Add security bulletin IDs (includes CVEs)
if len(update.SecurityBulletinIDs) > 0 {
metadata["security_bulletins"] = update.SecurityBulletinIDs
// Extract CVEs from security bulletins
cveList := make([]string, 0)
for _, bulletin := range update.SecurityBulletinIDs {
if strings.HasPrefix(bulletin, "CVE-") {
cveList = append(cveList, bulletin)
}
}
if len(cveList) > 0 {
metadata["cve_list"] = cveList
}
}
// Add deployment information
if update.LastDeploymentChangeTime != nil {
metadata["last_deployment_change"] = update.LastDeploymentChangeTime.Format(time.RFC3339)
metadata["discovered_at"] = update.LastDeploymentChangeTime.Format(time.RFC3339)
}
// Add deadline if present
if update.Deadline != nil {
metadata["deadline"] = update.Deadline.Format(time.RFC3339)
}
// Add flags
if update.IsMandatory {
metadata["is_mandatory"] = true
}
if update.IsBeta {
metadata["is_beta"] = true
}
if update.IsDownloaded {
metadata["is_downloaded"] = true
}
// Add more info URLs
if len(update.MoreInfoUrls) > 0 {
metadata["more_info_urls"] = update.MoreInfoUrls
}
// Add release notes
if update.ReleaseNotes != "" {
metadata["release_notes"] = update.ReleaseNotes
}
// Add support URL
if update.SupportUrl != "" {
metadata["support_url"] = update.SupportUrl
}
// Add categories if available
categories := s.getCategories(update)
if len(categories) > 0 {
metadata["categories"] = categories
}
updateItem := &client.UpdateReportItem{
PackageType: "windows_update",
PackageName: title,
PackageDescription: description,
CurrentVersion: currentVersion,
AvailableVersion: availableVersion,
Severity: severity,
RepositorySource: "Microsoft Update",
Metadata: metadata,
}
// Add KB articles to CVE list field if present
if len(kbArticles) > 0 {
updateItem.KBID = strings.Join(kbArticles, ", ")
}
// Add size information to description if available
if maxDownloadSize > 0 {
sizeStr := s.formatFileSize(uint64(maxDownloadSize))
updateItem.PackageDescription += fmt.Sprintf(" (Size: %s)", sizeStr)
}
return updateItem
}
// getKBArticles extracts KB article IDs from an update
func (s *WindowsUpdateScannerWUA) getKBArticles(update *windowsupdate.IUpdate) []string {
kbCollection := update.KBArticleIDs
if kbCollection == nil {
return []string{}
}
// kbCollection is already a slice of strings
return kbCollection
}
// getCategories extracts update categories
func (s *WindowsUpdateScannerWUA) getCategories(update *windowsupdate.IUpdate) []string {
var categories []string
categoryCollection := update.Categories
if categoryCollection == nil {
return categories
}
for _, category := range categoryCollection {
if category != nil {
name := category.Name
categories = append(categories, name)
}
}
return categories
}
// determineSeverityFromCategories determines severity based on update categories
func (s *WindowsUpdateScannerWUA) determineSeverityFromCategories(update *windowsupdate.IUpdate) string {
categories := s.getCategories(update)
title := strings.ToUpper(update.Title)
// Critical Security Updates
for _, category := range categories {
categoryUpper := strings.ToUpper(category)
if strings.Contains(categoryUpper, "SECURITY") ||
strings.Contains(categoryUpper, "CRITICAL") ||
strings.Contains(categoryUpper, "IMPORTANT") {
return "critical"
}
}
// Check title for security keywords
if strings.Contains(title, "SECURITY") ||
strings.Contains(title, "CRITICAL") ||
strings.Contains(title, "IMPORTANT") ||
strings.Contains(title, "PATCH TUESDAY") {
return "critical"
}
// Driver Updates
for _, category := range categories {
if strings.Contains(strings.ToUpper(category), "DRIVERS") {
return "moderate"
}
}
// Definition Updates
for _, category := range categories {
if strings.Contains(strings.ToUpper(category), "DEFINITION") ||
strings.Contains(strings.ToUpper(category), "ANTIVIRUS") ||
strings.Contains(strings.ToUpper(category), "ANTIMALWARE") {
return "high"
}
}
return "moderate"
}
// categorizeUpdate determines the type of update
func (s *WindowsUpdateScannerWUA) categorizeUpdate(title string, categories []string) string {
titleUpper := strings.ToUpper(title)
// Security Updates
for _, category := range categories {
if strings.Contains(strings.ToUpper(category), "SECURITY") {
return "security"
}
}
if strings.Contains(titleUpper, "SECURITY") ||
strings.Contains(titleUpper, "PATCH") ||
strings.Contains(titleUpper, "VULNERABILITY") {
return "security"
}
// Driver Updates
for _, category := range categories {
if strings.Contains(strings.ToUpper(category), "DRIVERS") {
return "driver"
}
}
if strings.Contains(titleUpper, "DRIVER") {
return "driver"
}
// Definition Updates
for _, category := range categories {
if strings.Contains(strings.ToUpper(category), "DEFINITION") {
return "definition"
}
}
if strings.Contains(titleUpper, "DEFINITION") ||
strings.Contains(titleUpper, "ANTIVIRUS") ||
strings.Contains(titleUpper, "ANTIMALWARE") {
return "definition"
}
// Feature Updates
if strings.Contains(titleUpper, "FEATURE") ||
strings.Contains(titleUpper, "VERSION") ||
strings.Contains(titleUpper, "UPGRADE") {
return "feature"
}
// Quality Updates
if strings.Contains(titleUpper, "QUALITY") ||
strings.Contains(titleUpper, "CUMULATIVE") {
return "quality"
}
return "system"
}
// getEstimatedSize gets the estimated size of the update
func (s *WindowsUpdateScannerWUA) getEstimatedSize(update *windowsupdate.IUpdate) uint64 {
maxSize := update.MaxDownloadSize
if maxSize > 0 {
return uint64(maxSize)
}
return 0
}
// formatFileSize formats bytes into human readable string
func (s *WindowsUpdateScannerWUA) formatFileSize(bytes uint64) string {
const unit = 1024
if bytes < unit {
return fmt.Sprintf("%d B", bytes)
}
div, exp := uint64(unit), 0
for n := bytes / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
}
// GetUpdateDetails retrieves detailed information about a specific Windows update
func (s *WindowsUpdateScannerWUA) GetUpdateDetails(updateID string) (*client.UpdateReportItem, error) {
// This would require implementing a search by ID functionality
// For now, we don't implement this as it would require additional WUA API calls
return nil, fmt.Errorf("GetUpdateDetails not yet implemented for WUA scanner")
}
// GetUpdateHistory retrieves update history
func (s *WindowsUpdateScannerWUA) GetUpdateHistory() ([]client.UpdateReportItem, error) {
if !s.IsAvailable() {
return nil, fmt.Errorf("WUA scanner is only available on Windows")
}
// Initialize COM
comshim.Add(1)
defer comshim.Done()
ole.CoInitializeEx(0, ole.COINIT_APARTMENTTHREADED|ole.COINIT_SPEED_OVER_MEMORY)
defer ole.CoUninitialize()
// Create update session
session, err := windowsupdate.NewUpdateSession()
if err != nil {
return nil, fmt.Errorf("failed to create Windows Update session: %w", err)
}
// Create update searcher
searcher, err := session.CreateUpdateSearcher()
if err != nil {
return nil, fmt.Errorf("failed to create update searcher: %w", err)
}
// Query update history
historyEntries, err := searcher.QueryHistoryAll()
if err != nil {
return nil, fmt.Errorf("failed to query update history: %w", err)
}
// Convert history to our format
return s.convertHistoryEntries(historyEntries), nil
}
// convertHistoryEntries converts update history entries to our UpdateReportItem format
func (s *WindowsUpdateScannerWUA) convertHistoryEntries(entries []*windowsupdate.IUpdateHistoryEntry) []client.UpdateReportItem {
var updates []client.UpdateReportItem
for _, entry := range entries {
if entry == nil {
continue
}
// Create a basic update report item from history entry
updateItem := &client.UpdateReportItem{
PackageType: "windows_update_history",
PackageName: entry.Title,
PackageDescription: entry.Description,
CurrentVersion: "Installed",
AvailableVersion: "History Entry",
Severity: s.determineSeverityFromHistoryEntry(entry),
RepositorySource: "Microsoft Update",
Metadata: map[string]interface{}{
"detected_via": "wua_history",
"api_source": "windows_update_agent",
"scan_timestamp": time.Now().Format(time.RFC3339),
"history_date": entry.Date,
"operation": entry.Operation,
"result_code": entry.ResultCode,
"hresult": entry.HResult,
},
}
updates = append(updates, *updateItem)
}
return updates
}
// determineSeverityFromHistoryEntry determines severity from history entry
func (s *WindowsUpdateScannerWUA) determineSeverityFromHistoryEntry(entry *windowsupdate.IUpdateHistoryEntry) string {
title := strings.ToUpper(entry.Title)
// Check title for security keywords
if strings.Contains(title, "SECURITY") ||
strings.Contains(title, "CRITICAL") ||
strings.Contains(title, "IMPORTANT") {
return "critical"
}
if strings.Contains(title, "DEFINITION") ||
strings.Contains(title, "ANTIVIRUS") ||
strings.Contains(title, "ANTIMALWARE") {
return "high"
}
return "moderate"
}
// mapMsrcSeverity maps Microsoft's MSRC severity ratings to our severity levels
func (s *WindowsUpdateScannerWUA) mapMsrcSeverity(msrcSeverity string) string {
switch strings.ToLower(strings.TrimSpace(msrcSeverity)) {
case "critical":
return "critical"
case "important":
return "critical"
case "moderate":
return "moderate"
case "low":
return "low"
case "unspecified", "":
return ""
default:
return ""
}
}
// parseVersionFromTitle attempts to extract current and available version from update title
// Examples:
// "Intel Corporation - Display - 26.20.100.7584" -> ("Unknown", "26.20.100.7584")
// "2024-01 Cumulative Update for Windows 11 Version 22H2 (KB5034123)" -> ("Unknown", "KB5034123")
func (s *WindowsUpdateScannerWUA) parseVersionFromTitle(title string) (currentVersion, availableVersion string) {
currentVersion = "Unknown"
availableVersion = "Unknown"
// Pattern 1: Version at the end after last dash (common for drivers)
// Example: "Intel Corporation - Display - 26.20.100.7584"
if strings.Contains(title, " - ") {
parts := strings.Split(title, " - ")
lastPart := strings.TrimSpace(parts[len(parts)-1])
// Check if last part looks like a version (contains dots and digits)
if strings.Contains(lastPart, ".") && s.containsDigits(lastPart) {
availableVersion = lastPart
return
}
}
// Pattern 2: KB article in parentheses
// Example: "2024-01 Cumulative Update (KB5034123)"
if strings.Contains(title, "(KB") && strings.Contains(title, ")") {
start := strings.Index(title, "(KB")
end := strings.Index(title[start:], ")")
if end > 0 {
kbNumber := title[start+1 : start+end]
availableVersion = kbNumber
return
}
}
// Pattern 3: Date-based versioning
// Example: "2024-01 Security Update"
if strings.Contains(title, "202") { // Year pattern
words := strings.Fields(title)
for _, word := range words {
// Look for YYYY-MM pattern
if len(word) == 7 && word[4] == '-' && s.containsDigits(word[:4]) && s.containsDigits(word[5:]) {
availableVersion = word
return
}
}
}
// Pattern 4: Version keyword followed by number
// Example: "Feature Update to Windows 11, version 23H2"
lowerTitle := strings.ToLower(title)
if strings.Contains(lowerTitle, "version ") {
idx := strings.Index(lowerTitle, "version ")
afterVersion := title[idx+8:]
words := strings.Fields(afterVersion)
if len(words) > 0 {
// Take the first word after "version"
versionStr := strings.TrimRight(words[0], ",.")
availableVersion = versionStr
return
}
}
return
}
// containsDigits checks if a string contains any digits
func (s *WindowsUpdateScannerWUA) containsDigits(str string) bool {
for _, char := range str {
if char >= '0' && char <= '9' {
return true
}
}
return false
}