fleet/pkg/file/pe.go
Ian Littman 5a30b477c6
Fall back to FileVersion when an EXE installer has FileVersion but not ProductVersion (#25070)
For #23541

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files)
for more information.
- [x] Manual QA for all new/changed functionality
2024-12-31 14:28:15 -06:00

122 lines
3.8 KiB
Go

package file
import (
"crypto/sha256"
"fmt"
"io"
"strings"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/saferwall/pe"
)
// ExtractPEMetadata extracts the name and version metadata from a .exe file in
// the Portable Executable (PE) format.
func ExtractPEMetadata(tfr *fleet.TempFileReader) (*InstallerMetadata, error) {
// compute its hash
h := sha256.New()
_, _ = io.Copy(h, tfr) // writes to a hash cannot fail
if err := tfr.Rewind(); err != nil {
return nil, err
}
// cannot use the "Fast" option, we need the data directories for the
// resources to be available.
pep, err := pe.New(tfr.Name(), &pe.Options{
OmitExportDirectory: true,
OmitImportDirectory: true,
OmitExceptionDirectory: true,
OmitSecurityDirectory: true,
OmitRelocDirectory: true,
OmitDebugDirectory: true,
OmitArchitectureDirectory: true,
OmitGlobalPtrDirectory: true,
OmitTLSDirectory: true,
OmitLoadConfigDirectory: true,
OmitBoundImportDirectory: true,
OmitIATDirectory: true,
OmitDelayImportDirectory: true,
OmitCLRHeaderDirectory: true,
})
if err != nil {
return nil, fmt.Errorf("error creating PE file: %w", err)
}
defer pep.Close()
if err := pep.Parse(); err != nil {
return nil, fmt.Errorf("error parsing PE file: %w", err)
}
resources, err := pep.ParseVersionResourcesForEntries()
if err != nil {
return nil, fmt.Errorf("error parsing PE version resources: %w", err)
}
var name, version, sfxName, sfxVersion string
for _, e := range resources {
productName, ok := e["ProductName"]
if !ok {
productName = e["productname"] // used by Opera SFX (self-extracting archive)
}
productVersion := strings.TrimSpace(e["ProductVersion"])
if productName != "" {
productName = strings.TrimSpace(productName)
if productName == "7-Zip" {
// This may be a 7-Zip self-extracting archive.
sfxName = productName
sfxVersion = productVersion
continue
}
name = productName
}
if productVersion != "" {
version = productVersion
} else if strings.TrimSpace(e["FileVersion"]) != "" {
version = strings.TrimSpace(e["FileVersion"])
}
}
if name == "" && sfxName != "" {
// If we didn't find a ProductName, we may be
// dealing with an archive executable (e.g., if we're dealing with the 7-Zip executable itself rather than Opera)
name = sfxName
if sfxVersion != "" {
version = sfxVersion
}
}
return applySpecialCases(&InstallerMetadata{
Name: name,
Version: version,
PackageIDs: []string{name},
SHASum: h.Sum(nil),
}, resources), nil
}
var exeSpecialCases = map[string]func(*InstallerMetadata, []map[string]string) *InstallerMetadata{
"Notion": func(meta *InstallerMetadata, _ []map[string]string) *InstallerMetadata {
if meta.Version != "" {
meta.Name = meta.Name + " " + meta.Version
}
return meta
},
}
// Unlike .exe files that are the software itself (and just need to be copied
// over to the host), and unlike standard installer formats like .msi where the
// metadata defines the name under which the software will be installed, .exe
// installers may do pretty much anything they want when installing the
// software, regardless of what the .exe metadata contains.
//
// For example, the Notion .exe installer installs the app under a name like
// "Notion 3.11.1", and not just "Notion". There's no way to detect that by
// parsing the installer's metadata, so we need to apply some special cases at
// least for the most popular apps that use unusual naming.
//
// See https://github.com/fleetdm/fleet/issues/20440#issuecomment-2260500661
func applySpecialCases(meta *InstallerMetadata, resources []map[string]string) *InstallerMetadata {
if fn := exeSpecialCases[meta.Name]; fn != nil {
return fn(meta, resources)
}
return meta
}