mirror of
https://github.com/fleetdm/fleet
synced 2026-05-06 06:48:54 +00:00
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
122 lines
3.8 KiB
Go
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
|
|
}
|