github.com/google/osv-scalibr@v0.4.1/extractor/standalone/windows/regpatchlevel/regpatchlevel_windows.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 //go:build windows 16 17 // Package regpatchlevel extract patch level from the Windows registry. 18 package regpatchlevel 19 20 import ( 21 "context" 22 "errors" 23 "fmt" 24 "regexp" 25 26 "github.com/google/osv-scalibr/common/windows/registry" 27 "github.com/google/osv-scalibr/extractor" 28 "github.com/google/osv-scalibr/extractor/standalone" 29 "github.com/google/osv-scalibr/inventory" 30 "github.com/google/osv-scalibr/plugin" 31 ) 32 33 const ( 34 // Registry path to the Windows component based servicing packages. 35 regPackagesRoot = `SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\Packages` 36 ) 37 38 var ( 39 dismVersionRegexp = regexp.MustCompile(`~([^~]+)$`) 40 errSkipEntry = errors.New("entry was skipped") 41 ) 42 43 // Name of the extractor 44 const Name = "windows/regpatchlevel" 45 46 // Configuration for the extractor. 47 type Configuration struct { 48 // Opener is the registry engine to use (offline, live or mock). 49 Opener registry.Opener 50 } 51 52 // DefaultConfiguration for the extractor. It uses the live registry of the running system. 53 func DefaultConfiguration() Configuration { 54 return Configuration{ 55 Opener: registry.NewLiveOpener(), 56 } 57 } 58 59 // Extractor implements the regpatchlevel extractor. 60 type Extractor struct { 61 opener registry.Opener 62 } 63 64 // New creates a new Extractor from a given configuration. 65 func New(config Configuration) standalone.Extractor { 66 return &Extractor{ 67 opener: config.Opener, 68 } 69 } 70 71 // NewDefault returns an extractor with the default config settings. 72 func NewDefault() standalone.Extractor { 73 return New(DefaultConfiguration()) 74 } 75 76 // Name of the extractor. 77 func (e Extractor) Name() string { return Name } 78 79 // Version of the extractor. 80 func (e Extractor) Version() int { return 0 } 81 82 // Requirements of the extractor. 83 func (e Extractor) Requirements() *plugin.Capabilities { 84 return &plugin.Capabilities{OS: plugin.OSWindows, RunningSystem: true} 85 } 86 87 // Extract retrieves the patch level from the Windows registry. 88 func (e *Extractor) Extract(ctx context.Context, input *standalone.ScanInput) (inventory.Inventory, error) { 89 reg, err := e.opener.Open() 90 if err != nil { 91 return inventory.Inventory{}, err 92 } 93 defer reg.Close() 94 95 key, err := reg.OpenKey("HKLM", regPackagesRoot) 96 if err != nil { 97 return inventory.Inventory{}, err 98 } 99 defer key.Close() 100 101 subkeys, err := key.SubkeyNames() 102 if err != nil { 103 return inventory.Inventory{}, err 104 } 105 106 var pkgs []*extractor.Package 107 108 for _, subkey := range subkeys { 109 entry, err := e.handleKey(reg, regPackagesRoot, subkey) 110 if err != nil { 111 if errors.Is(err, errSkipEntry) { 112 continue 113 } 114 115 return inventory.Inventory{}, err 116 } 117 118 pkgs = append(pkgs, entry) 119 } 120 121 return inventory.Inventory{Packages: pkgs}, nil 122 } 123 124 func (e *Extractor) handleKey(reg registry.Registry, registryPath, keyName string) (*extractor.Package, error) { 125 keyPath := fmt.Sprintf("%s\\%s", registryPath, keyName) 126 key, err := reg.OpenKey("HKLM", keyPath) 127 if err != nil { 128 return nil, err 129 } 130 defer key.Close() 131 132 currentState, err := key.ValueString("CurrentState") 133 if err != nil { 134 return nil, err 135 } 136 137 visibility, err := key.ValueString("Visibility") 138 if err != nil { 139 return nil, err 140 } 141 142 // Is installed and visible 143 if (currentState != "112" && currentState != "80") || visibility != "1" { 144 return nil, errSkipEntry 145 } 146 147 submatch := dismVersionRegexp.FindStringSubmatch(keyName) 148 if len(submatch) < 2 { 149 return nil, errSkipEntry 150 } 151 152 return &extractor.Package{ 153 Name: keyName, 154 Version: submatch[1], 155 PURLType: "windows", 156 }, nil 157 }