fleet/pkg/file/pe.go
Matt Hatcher 369f9070c3
Add InstallAnywhere self extracting archive to metadata extraction (#34874)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #34827

# Checklist for submitter

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

- [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/guides/committing-changes.md#changes-files)
for more information.

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [x] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes

## Testing

- [ ] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)

- [ ] QA'd all new/changed functionality manually

For unreleased bug fixes in a release candidate, one of:

- [ ] Confirmed that the fix is not expected to adversely impact load
test results
- [ ] Alerted the release DRI if additional load testing is needed
2025-11-06 14:25:07 -05: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" || productName == "InstallAnywhere" {
// This may be a 7-Zip or InstallAnywhere 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
}