From a9cc1bfc35f22866c8686ce4cfc6991da50ed701 Mon Sep 17 00:00:00 2001 From: Preslav Gerchev Date: Fri, 1 Nov 2024 18:10:27 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Read=20windows=20appx=20packages=20?= =?UTF-8?q?on=20a=20FS=20scan.=20(#4804)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docker/{filesytem.go => filesystem.go} | 0 providers/os/fs/find_files.go | 5 +- providers/os/fs/find_files_test.go | 4 + .../packages/windows_appx_manifest.go | 69 ++++++ .../packages/windows_appx_manifest_test.go | 100 ++++++++ .../os/resources/packages/windows_packages.go | 214 +++++++++++++----- .../packages/windows_packages_test.go | 25 ++ 7 files changed, 354 insertions(+), 63 deletions(-) rename providers/os/connection/docker/{filesytem.go => filesystem.go} (100%) create mode 100644 providers/os/resources/packages/windows_appx_manifest.go create mode 100644 providers/os/resources/packages/windows_appx_manifest_test.go diff --git a/providers/os/connection/docker/filesytem.go b/providers/os/connection/docker/filesystem.go similarity index 100% rename from providers/os/connection/docker/filesytem.go rename to providers/os/connection/docker/filesystem.go diff --git a/providers/os/fs/find_files.go b/providers/os/fs/find_files.go index 34e3cb3ae9..e55210f0c1 100644 --- a/providers/os/fs/find_files.go +++ b/providers/os/fs/find_files.go @@ -17,10 +17,10 @@ func FindFiles(iofs fs.FS, from string, r *regexp.Regexp, typ string, perm *uint if err != nil { return err } + if skip { return nil } - if matcher.Match(p, d.Type()) { matchedPaths = append(matchedPaths, p) } @@ -54,7 +54,8 @@ func (m findFilesMatcher) matchesRegex(path string) bool { // We don't use r.Match because we need the entire path to match // if we want to be compatible with find. It would probably be // more efficient add anchors to the regular expression - return m.r.FindString(path) == path + match := m.r.FindString(path) + return match == path } func (m findFilesMatcher) matchesType(entryType fs.FileMode) bool { diff --git a/providers/os/fs/find_files_test.go b/providers/os/fs/find_files_test.go index 3499eb4175..cbefedc669 100644 --- a/providers/os/fs/find_files_test.go +++ b/providers/os/fs/find_files_test.go @@ -165,6 +165,10 @@ func TestFindFiles(t *testing.T) { require.NoError(t, err) assert.ElementsMatch(t, rootBFiles, []string{"root/b/file1"}) + file1Files, err := FindFiles(afero.NewIOFS(fs), "root", regexp.MustCompile(".*/file1"), "f", nil) + require.NoError(t, err) + assert.ElementsMatch(t, file1Files, []string{"root/b/file1", "root/a/file1"}) + perm := uint32(0o002) permFiles, err := FindFiles(afero.NewIOFS(fs), "root", nil, "f", &perm) require.NoError(t, err) diff --git a/providers/os/resources/packages/windows_appx_manifest.go b/providers/os/resources/packages/windows_appx_manifest.go new file mode 100644 index 0000000000..1e86e593a2 --- /dev/null +++ b/providers/os/resources/packages/windows_appx_manifest.go @@ -0,0 +1,69 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package packages + +import "encoding/xml" + +// AppxManifest represents the structure of an AppxManifest.xml file +type AppxManifest struct { + XMLName xml.Name `xml:"Package"` + Text string `xml:",chardata"` + Xmlns string `xml:"xmlns,attr"` + Uap string `xml:"uap,attr"` + Build string `xml:"build,attr"` + Mp string `xml:"mp,attr"` + IgnorableNamespaces string `xml:"IgnorableNamespaces,attr"` + Identity struct { + Text string `xml:",chardata"` + Name string `xml:"Name,attr"` + ProcessorArchitecture string `xml:"ProcessorArchitecture,attr"` + Publisher string `xml:"Publisher,attr"` + Version string `xml:"Version,attr"` + } `xml:"Identity"` + Properties struct { + Text string `xml:",chardata"` + Framework string `xml:"Framework"` + DisplayName string `xml:"DisplayName"` + PublisherDisplayName string `xml:"PublisherDisplayName"` + Description string `xml:"Description"` + Logo string `xml:"Logo"` + } `xml:"Properties"` + Resources struct { + Text string `xml:",chardata"` + Resource struct { + Text string `xml:",chardata"` + Language string `xml:"Language,attr"` + } `xml:"Resource"` + } `xml:"Resources"` + Dependencies struct { + Text string `xml:",chardata"` + TargetDeviceFamily struct { + Text string `xml:",chardata"` + Name string `xml:"Name,attr"` + MinVersion string `xml:"MinVersion,attr"` + MaxVersionTested string `xml:"MaxVersionTested,attr"` + } `xml:"TargetDeviceFamily"` + } `xml:"Dependencies"` + PhoneIdentity struct { + Text string `xml:",chardata"` + PhoneProductId string `xml:"PhoneProductId,attr"` + PhonePublisherId string `xml:"PhonePublisherId,attr"` + } `xml:"PhoneIdentity"` + Extensions struct { + Text string `xml:",chardata"` + Extension []struct { + Text string `xml:",chardata"` + Category string `xml:"Category,attr"` + InProcessServer struct { + Text string `xml:",chardata"` + Path string `xml:"Path"` + ActivatableClass []struct { + Text string `xml:",chardata"` + ActivatableClassId string `xml:"ActivatableClassId,attr"` + ThreadingModel string `xml:"ThreadingModel,attr"` + } `xml:"ActivatableClass"` + } `xml:"InProcessServer"` + } `xml:"Extension"` + } `xml:"Extensions"` +} diff --git a/providers/os/resources/packages/windows_appx_manifest_test.go b/providers/os/resources/packages/windows_appx_manifest_test.go new file mode 100644 index 0000000000..0fbf86ed04 --- /dev/null +++ b/providers/os/resources/packages/windows_appx_manifest_test.go @@ -0,0 +1,100 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package packages + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParseWindowsAppxManifest(t *testing.T) { + manifest := ` + + + + + + + ms-resource:PackageDisplayName + ms-resource:PublisherDisplayName + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + ms-resource:PackageDisplayName + + + + + + + + + + + + + + + + + + + + + + + + + AAD.Core.dll + + + + + + + + +` + + man, err := parseAppxManifest([]byte(manifest)) + require.NoError(t, err) + + require.Equal(t, "neutral", man.arch) + require.Equal(t, "Microsoft.AAD.BrokerPlugin", man.Name) + require.Equal(t, "CN=Microsoft Windows, O=Microsoft Corporation, L=Redmond, S=Washington, C=US", man.Publisher) + require.Equal(t, "1000.19580.1000.0", man.Version) +} diff --git a/providers/os/resources/packages/windows_packages.go b/providers/os/resources/packages/windows_packages.go index f727d4568c..94331c877f 100644 --- a/providers/os/resources/packages/windows_packages.go +++ b/providers/os/resources/packages/windows_packages.go @@ -5,13 +5,16 @@ package packages import ( "encoding/json" + "encoding/xml" "fmt" "io" + "regexp" "runtime" "time" "github.com/cockroachdb/errors" "github.com/rs/zerolog/log" + "github.com/spf13/afero" "go.mondoo.com/cnquery/v11/providers-sdk/v1/inventory" "go.mondoo.com/cnquery/v11/providers/os/connection/shared" "go.mondoo.com/cnquery/v11/providers/os/detector/windows" @@ -38,20 +41,20 @@ const ( // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ff357803(v=vs.85) var ( wsusClassificationGUID = map[string]WSUSClassification{ - "5c9376ab-8ce6-464a-b136-22113dd69801 ": Application, - "434de588-ed14-48f5-8eed-a15e09a991f6": Connectors, - "e6cf1350-c01b-414d-a61f-263d14d133b4": CriticalUpdates, - "e0789628-ce08-4437-be74-2495b842f43b": DefinitionUpdates, - "e140075d-8433-45c3-ad87-e72345b3607": DeveloperKits, - "b54e7d24-7add-428f-8b75-90a396fa584f ": FeaturePacks, - "9511D615-35B2-47BB-927F-F73D8E9260BB": Guidance, - "0fa1201d-4330-4fa8-8ae9-b877473b6441": SecurityUpdates, - "68c5b0a3-d1a6-4553-ae49-01d3a7827828": ServicePacks, - "b4832bd8-e735-4761-8daf-37f882276dab": Tools, - "28bc880e-0592-4cbf-8f95-c79b17911d5f": UpdateRollups, - "cd5ffd1e-e932-4e3a-bf74-18bf0b1bbd83": Updates, - "ebfc1fc5-71a4-4f7b-9aca-3b9a503104a0": Drivers, - "8c3fcc84-7410-4a95-8b89-a166a0190486": Defender, + "5c9376ab-8ce6-464a-b136-22113dd69801": Application, + "434de588-ed14-48f5-8eed-a15e09a991f6": Connectors, + "e6cf1350-c01b-414d-a61f-263d14d133b4": CriticalUpdates, + "e0789628-ce08-4437-be74-2495b842f43b": DefinitionUpdates, + "e140075d-8433-45c3-ad87-e72345b3607": DeveloperKits, + "b54e7d24-7add-428f-8b75-90a396fa584f": FeaturePacks, + "9511D615-35B2-47BB-927F-F73D8E9260BB": Guidance, + "0fa1201d-4330-4fa8-8ae9-b877473b6441": SecurityUpdates, + "68c5b0a3-d1a6-4553-ae49-01d3a7827828": ServicePacks, + "b4832bd8-e735-4761-8daf-37f882276dab": Tools, + "28bc880e-0592-4cbf-8f95-c79b17911d5f": UpdateRollups, + "cd5ffd1e-e932-4e3a-bf74-18bf0b1bbd83": Updates, + "ebfc1fc5-71a4-4f7b-9aca-3b9a503104a0": Drivers, + "8c3fcc84-7410-4a95-8b89-a166a0190486": Defender, } appxArchitecture = map[int]string{ @@ -98,12 +101,46 @@ var ( WINDOWS_QUERY_APPX_PACKAGES = `Get-AppxPackage -AllUsers | Select Name, PackageFullName, Architecture, Version, Publisher | ConvertTo-Json` ) -type powershellWinAppxPackages struct { +type winAppxPackages struct { Name string `json:"Name"` FullName string `json:"PackageFullName"` Architecture int `json:"Architecture"` Version string `json:"Version"` Publisher string `json:"Publisher"` + // can directly set it to the architecture string, the pwsh script returns it as int (Architecture) + arch string `json:"-"` +} + +func (p winAppxPackages) toPackage() Package { + if p.arch == "" { + arch, ok := appxArchitecture[p.Architecture] + if !ok { + log.Warn().Int("arch", p.Architecture).Msg("unknown architecture value for windows appx package") + arch = "unknown" + } + p.arch = arch + } + + pkg := Package{ + Name: p.Name, + Version: p.Version, + Arch: p.arch, + Format: "windows/appx", + Vendor: p.Publisher, + } + + if p.Name != "" && p.Version != "" { + cpeWfns, err := cpe.NewPackage2Cpe(p.Publisher, p.Name, p.Version, "", "") + if err != nil { + log.Debug().Err(err).Str("name", p.Name).Str("version", p.Version).Msg("could not create cpe for windows appx package") + } else { + pkg.CPEs = cpeWfns + } + } else { + log.Debug().Msg("ignored package since information is missing") + } + + return pkg } // Good read: https://www.wintips.org/view-installed-apps-and-packages-in-windows-10-8-1-8-from-powershell/ @@ -113,7 +150,7 @@ func ParseWindowsAppxPackages(input io.Reader) ([]Package, error) { return nil, err } - var appxPackages []powershellWinAppxPackages + var appxPackages []winAppxPackages // handle case where no packages are installed if len(data) == 0 { @@ -126,31 +163,9 @@ func ParseWindowsAppxPackages(input io.Reader) ([]Package, error) { } pkgs := make([]Package, len(appxPackages)) - for i := range appxPackages { - arch, ok := appxArchitecture[appxPackages[i].Architecture] - if !ok { - log.Warn().Int("arch", appxPackages[i].Architecture).Msg("unknown architecture value for windows appx package") - arch = "unknown" - } - - cpeWfns := []string{} - if appxPackages[i].Name != "" && appxPackages[i].Version != "" { - cpeWfns, err = cpe.NewPackage2Cpe(appxPackages[i].Publisher, appxPackages[i].Name, appxPackages[i].Version, "", "") - if err != nil { - log.Debug().Err(err).Str("name", appxPackages[i].Name).Str("version", appxPackages[i].Version).Msg("could not create cpe for windows appx package") - } - } else { - log.Debug().Msg("ignored package since information is missing") - } - - pkgs[i] = Package{ - Name: appxPackages[i].Name, - Version: appxPackages[i].Version, - Arch: arch, - Format: "windows/appx", - CPEs: cpeWfns, - Vendor: appxPackages[i].Publisher, - } + for i, p := range appxPackages { + pkg := p.toPackage() + pkgs[i] = pkg } return pkgs, nil } @@ -268,6 +283,34 @@ func (w *WinPkgManager) getInstalledApps() ([]Package, error) { return ParseWindowsAppPackages(cmd.Stdout) } +func (w *WinPkgManager) getAppxPackages() ([]Package, error) { + canRunCmd := w.conn.Capabilities().Has(shared.Capability_RunCommand) + // we always prefer to use the powershell command to get the appx packages, fallback to filesystem if not possible + if !canRunCmd && (w.conn.Type() == shared.Type_FileSystem || w.conn.Type() == shared.Type_Device) { + return w.getFsAppxPackages() + } + + b, err := windows.Version(w.platform.Version) + if err != nil { + return nil, err + } + + // only win 10+ are compatible with app x packages + if b.Build > 10240 { + return w.getPwshAppxPackages() + } + + return []Package{}, nil +} + +func (w *WinPkgManager) getPwshAppxPackages() ([]Package, error) { + cmd, err := w.conn.RunCommand(powershell.Wrap(WINDOWS_QUERY_APPX_PACKAGES)) + if err != nil { + return nil, fmt.Errorf("could not read appx package list") + } + return ParseWindowsAppxPackages(cmd.Stdout) +} + func (w *WinPkgManager) getFsInstalledApps() ([]Package, error) { rh := registry.NewRegistryHandler() defer func() { @@ -310,6 +353,67 @@ func (w *WinPkgManager) getFsInstalledApps() ([]Package, error) { return packages, nil } +func (w *WinPkgManager) getFsAppxPackages() ([]Package, error) { + if !w.conn.Capabilities().Has(shared.Capability_FindFile) { + return nil, errors.New("find file is not supported for your platform") + } + fs := w.conn.FileSystem() + fsSearch, ok := fs.(shared.FileSearch) + if !ok { + return nil, errors.New("find file is not supported for your platform") + } + + paths := []string{ + `Windows\SystemApps`, + `Program Files\WindowsApps`, + } + appxPaths := map[string]struct{}{} + for _, p := range paths { + res, err := fsSearch.Find(p, regexp.MustCompile(".*/[Aa]ppx[Mm]anifest.xml"), "f", nil) + if err != nil { + continue + } + for _, r := range res { + appxPaths[r] = struct{}{} + } + } + log.Debug().Int("amount", len(appxPaths)).Msg("found appx manifest files") + + pkgs := []Package{} + afs := &afero.Afero{Fs: fs} + for p := range appxPaths { + res, err := afs.ReadFile(p) + if err != nil { + log.Debug().Err(err).Str("path", p).Msg("could not read appx manifest") + continue + } + winAppxPkg, err := parseAppxManifest(res) + if err != nil { + log.Debug().Err(err).Str("path", p).Msg("could not parse appx manifest") + continue + } + pkg := winAppxPkg.toPackage() + pkgs = append(pkgs, pkg) + + } + return pkgs, nil +} + +func parseAppxManifest(input []byte) (winAppxPackages, error) { + manifest := &AppxManifest{} + err := xml.Unmarshal(input, manifest) + if err != nil { + return winAppxPackages{}, err + } + pkg := winAppxPackages{ + Name: manifest.Identity.Name, + Version: manifest.Identity.Version, + Publisher: manifest.Identity.Publisher, + arch: manifest.Identity.ProcessorArchitecture, + } + return pkg, nil +} + func getPackageFromRegistryKey(key registry.RegistryKeyChild) (*Package, error) { items, err := registry.GetNativeRegistryKeyItems(key.Path + "\\" + key.Name) if err != nil { @@ -364,37 +468,25 @@ func getPackageFromRegistryKeyItems(children []registry.RegistryKeyItem) *Packag // returns installed appx packages as well as hot fixes func (w *WinPkgManager) List() ([]Package, error) { - b, err := windows.Version(w.platform.Version) - if err != nil { - return nil, err - } - pkgs := []Package{} appPkgs, err := w.getInstalledApps() if err != nil { - return nil, fmt.Errorf("could not read app package list") + return nil, errors.Wrap(err, "could not read app package list") } pkgs = append(pkgs, appPkgs...) + appxPackages, err := w.getAppxPackages() + if err != nil { + return nil, errors.Wrap(err, "could not read appx package list") + } + pkgs = append(pkgs, appxPackages...) + canRunCmd := w.conn.Capabilities().Has(shared.Capability_RunCommand) if !canRunCmd { - log.Debug().Msg("cannot run command on windows, skipping appx package and hotfixes list") + log.Debug().Msg("cannot run command on windows, skipping hotfixes list") return pkgs, nil } - if b.Build > 10240 { - // only win 10+ are compatible with app x packages - cmd, err := w.conn.RunCommand(powershell.Wrap(WINDOWS_QUERY_APPX_PACKAGES)) - if err != nil { - return nil, fmt.Errorf("could not read appx package list") - } - appxPkgs, err := ParseWindowsAppxPackages(cmd.Stdout) - if err != nil { - return nil, fmt.Errorf("could not read appx package list") - } - pkgs = append(pkgs, appxPkgs...) - } - // hotfixes cmd, err := w.conn.RunCommand(powershell.Wrap(WINDOWS_QUERY_HOTFIXES)) if err != nil { diff --git a/providers/os/resources/packages/windows_packages_test.go b/providers/os/resources/packages/windows_packages_test.go index 3c56d065c7..3feb0f4414 100644 --- a/providers/os/resources/packages/windows_packages_test.go +++ b/providers/os/resources/packages/windows_packages_test.go @@ -194,3 +194,28 @@ func TestGetPackageFromRegistryKeyItems(t *testing.T) { assert.Equal(t, expected, p) }) } + +func TestToPackage(t *testing.T) { + winAppxPkg := winAppxPackages{ + Name: "Microsoft.Windows.Cortana", + Version: "1.11.5.17763", + Publisher: "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US", + Architecture: 0, + } + + pkg := winAppxPkg.toPackage() + + expected := Package{ + Name: "Microsoft.Windows.Cortana", + Version: "1.11.5.17763", + Arch: "x86", + Format: "windows/appx", + Vendor: "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US", + CPEs: []string{ + "cpe:2.3:a:cn\\=microsoft_corporation\\,_o\\=microsoft_corporation\\,_l\\=redmond\\,_s\\=washington\\,_c\\=us:microsoft.windows.cortana:1.11.5.17763:*:*:*:*:*:*:*", + "cpe:2.3:a:cn\\=microsoft_corporation\\,_o\\=microsoft_corporation\\,_l\\=redmond\\,_s\\=washington\\,_c\\=us:microsoft.windows.cortana:1.11.5:*:*:*:*:*:*:*", + }, + } + + assert.Equal(t, expected, pkg) +}