-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Extractor to retrieve the list of installed software on Windows (incl…
…uding [googet](https://github.com/google/googet) packages). Retrieves the list of installed software using the `Microsoft\Windows\CurrentVersion\Uninstall` in the following three roots: - `HKLM\SOFTWARE`: the default one, should be present on all system; - `HKLM\SOFTWARE\Wow6432Node`: WoW64, should be present on 64bits system with 32bits applications - `HKU\{SID}` for each `SID` associated to users; This method is not 100% comprehensive and depend on software declaring themselves. But it should provide a view that is very similar (but not identical) to the control panel uninstall screen. PiperOrigin-RevId: 641247869
- Loading branch information
1 parent
81c01e8
commit 9610f8f
Showing
3 changed files
with
242 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
53 changes: 53 additions & 0 deletions
53
extractor/standalone/windows/ospackages/extractor_linux.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
// Copyright 2024 Google LLC | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
//go:build linux | ||
|
||
package ospackages | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
"github.com/google/osv-scalibr/extractor" | ||
"github.com/google/osv-scalibr/extractor/standalone" | ||
"github.com/google/osv-scalibr/purl" | ||
) | ||
|
||
// Name of the extractor | ||
const Name = "windows/ospackages" | ||
|
||
// Extractor implements the ospackages extractor. | ||
type Extractor struct{} | ||
|
||
// Name of the extractor. | ||
func (e Extractor) Name() string { return Name } | ||
|
||
// Version of the extractor. | ||
func (e Extractor) Version() int { return 0 } | ||
|
||
// Extract is a no-op for Linux. | ||
func (e *Extractor) Extract(ctx context.Context, input *standalone.ScanInput) ([]*extractor.Inventory, error) { | ||
return nil, fmt.Errorf("only supported on Windows") | ||
} | ||
|
||
// ToPURL converts an inventory created by this extractor into a PURL. | ||
func (e *Extractor) ToPURL(i *extractor.Inventory) (*purl.PackageURL, error) { | ||
return nil, fmt.Errorf("only supported on Windows") | ||
} | ||
|
||
// ToCPEs converts an inventory created by this extractor into CPEs, if supported. | ||
func (e *Extractor) ToCPEs(i *extractor.Inventory) ([]string, error) { | ||
return nil, fmt.Errorf("only supported on Windows") | ||
} |
187 changes: 187 additions & 0 deletions
187
extractor/standalone/windows/ospackages/extractor_windows.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
// Copyright 2024 Google LLC | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
//go:build windows | ||
|
||
// Package ospackages extracts installed softwares on Windows. | ||
package ospackages | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"strings" | ||
|
||
"golang.org/x/sys/windows/registry" | ||
"github.com/google/osv-scalibr/extractor" | ||
"github.com/google/osv-scalibr/extractor/standalone" | ||
"github.com/google/osv-scalibr/purl" | ||
) | ||
|
||
const ( | ||
// regUninstallRootWow64 is the registry key for 32-bit software on 64-bit Windows. | ||
regUninstallRootWow64 = `SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall` | ||
regUninstallRootDefault = `SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall` | ||
regUninstallRelativeUsers = `Software\Microsoft\Windows\CurrentVersion\Uninstall` | ||
|
||
// googetPrefix identifies GooGet packages. | ||
googetPrefix = "GooGet -" | ||
) | ||
|
||
// Name of the extractor | ||
const Name = "windows/ospackages" | ||
|
||
// Extractor implements the ospackages extractor. | ||
type Extractor struct{} | ||
|
||
// Name of the extractor. | ||
func (e Extractor) Name() string { return Name } | ||
|
||
// Version of the extractor. | ||
func (e Extractor) Version() int { return 0 } | ||
|
||
// Extract retrieves the patch level from the Windows registry. | ||
func (e *Extractor) Extract(ctx context.Context, input *standalone.ScanInput) ([]*extractor.Inventory, error) { | ||
// First extract the system-level installed software, both for x64 and x86. | ||
sysKeys, err := e.installedSystemSoftware() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
inventory := e.allSoftwaresInfo(registry.LOCAL_MACHINE, sysKeys) | ||
|
||
// Then we extract user-level installed software. | ||
userKeys, err := e.installedUserSoftware() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
inv := e.allSoftwaresInfo(registry.USERS, userKeys) | ||
return append(inventory, inv...), nil | ||
} | ||
|
||
// allSoftwaresInfo builds the inventory of name/version for installed software from the given registry | ||
// keys. This function cannot return an error. | ||
func (e *Extractor) allSoftwaresInfo(key registry.Key, paths []string) []*extractor.Inventory { | ||
var inventory []*extractor.Inventory | ||
|
||
for _, p := range paths { | ||
// Silently swallow errors as some software might not have a name or version. | ||
// For example, paint will be a subkey of the registry key, but it does not have a version. | ||
if inv, err := e.softwareInfo(key, p); err == nil { | ||
inventory = append(inventory, inv) | ||
} | ||
} | ||
|
||
return inventory | ||
} | ||
|
||
func (e *Extractor) softwareInfo(key registry.Key, path string) (*extractor.Inventory, error) { | ||
key, err := registry.OpenKey(key, path, registry.QUERY_VALUE) | ||
if err != nil { | ||
return nil, err | ||
} | ||
defer key.Close() | ||
|
||
name, _, err := key.GetStringValue("DisplayName") | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
version, _, err := key.GetStringValue("DisplayVersion") | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &extractor.Inventory{ | ||
Name: name, | ||
Version: version, | ||
}, nil | ||
} | ||
|
||
func (e *Extractor) installedSystemSoftware() ([]string, error) { | ||
keys, err := e.enumerateSubkeys(registry.LOCAL_MACHINE, regUninstallRootDefault) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
k, err := e.enumerateSubkeys(registry.LOCAL_MACHINE, regUninstallRootWow64) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return append(keys, k...), nil | ||
} | ||
|
||
func (e *Extractor) installedUserSoftware() ([]string, error) { | ||
var keys []string | ||
|
||
userHives, err := e.enumerateSubkeys(registry.USERS, "") | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
for _, userHive := range userHives { | ||
regPath := fmt.Sprintf(`%s\%s`, userHive, regUninstallRelativeUsers) | ||
regPath = strings.TrimPrefix(regPath, `\`) | ||
|
||
// Note that the key might not exist or be accessible for all users, so we silently ignore | ||
// errors here. | ||
if k, err := e.enumerateSubkeys(registry.USERS, regPath); err == nil { | ||
keys = append(keys, k...) | ||
} | ||
} | ||
|
||
return keys, nil | ||
} | ||
|
||
func (e *Extractor) enumerateSubkeys(key registry.Key, path string) ([]string, error) { | ||
key, err := registry.OpenKey(key, path, registry.ENUMERATE_SUB_KEYS) | ||
if err != nil { | ||
return nil, err | ||
} | ||
defer key.Close() | ||
|
||
subkeys, err := key.ReadSubKeyNames(0) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
var paths []string | ||
for _, subkey := range subkeys { | ||
paths = append(paths, fmt.Sprintf(`%s\%s`, path, subkey)) | ||
} | ||
|
||
return paths, nil | ||
} | ||
|
||
// ToPURL converts an inventory created by this extractor into a PURL. | ||
func (e Extractor) ToPURL(i *extractor.Inventory) (*purl.PackageURL, error) { | ||
if strings.HasPrefix(i.Name, googetPrefix) { | ||
return &purl.PackageURL{ | ||
Type: purl.TypeGooget, | ||
Name: i.Name, | ||
Version: i.Version, | ||
}, nil | ||
} | ||
|
||
return &purl.PackageURL{ | ||
Type: purl.TypeGeneric, | ||
Namespace: "microsoft", | ||
Name: i.Name, | ||
Version: i.Version, | ||
}, nil | ||
} | ||
|
||
// ToCPEs is not applicable as this extractor does not infer CPEs from the Inventory. | ||
func (e Extractor) ToCPEs(i *extractor.Inventory) ([]string, error) { return []string{}, nil } |