Files
Jonathan Katz 2c383d7b8d Differentiate between ipa and other zip file types in ExtractInstallerMetadata (#48802)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #48102

Changes:
- Renames `ExtractIPAMetadata` to `ExtractZIPMetadata` because the magic
bytes for zip based installers (.ipa, .msix, .zip, etc) are the same so
any zip file reaches it. If the zip does not contain an `Info.plist`
file it will now fail with `ErrInvalidType`.
- Did **NOT** make typeFromBytes return "zip" instead of "ipa" because
meta.Extension is set from that which has downstream effects.
- Added test files 
The actual error message is still just "invalid file type". 

# 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.

- [ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.
- [ ] Timeouts are implemented and retries are limited to avoid infinite
loops
- [ ] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes

## Testing

- [x] 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)

- [x] QA'd all new/changed functionality manually
- Tested adding a valid `.ipa`, a macos FMA that uses a .zip file
(alt-tab/darwin), and a windows FMA that uses a .zip file
(vnc-server/windows).
- Tested an msix file (renamed or not) cannot be uploaded or edited for
an existing msi installer
  - Also tested the same things via GitOps
  

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved installer type detection so ZIP-based packages are less
likely to be misidentified.
* Fixed an error message that incorrectly referred to the wrong file
type when detection fails.
* MSIX packages are now reported more accurately when they don’t match
IPA parsing rules.
* **Refactor**
  * Cleaned up installer metadata handling for ZIP-based archives.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-06 18:18:16 -04:00

104 lines
3.3 KiB
Go

package file_test
import (
"archive/zip"
"os"
"path/filepath"
"testing"
"github.com/fleetdm/fleet/v4/pkg/file"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/stretchr/testify/require"
)
func infoPlist(bundleID string, iOS bool) string {
requiresIPhoneOS := ""
if iOS {
requiresIPhoneOS = "<key>LSRequiresIPhoneOS</key><true/>"
}
return `<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0"><dict>
<key>CFBundleIdentifier</key><string>` + bundleID + `</string>
<key>CFBundleName</key><string>Test</string>
<key>CFBundleShortVersionString</key><string>1.0</string>
` + requiresIPhoneOS + `
</dict></plist>`
}
// writeZip builds a zip at a temp path with the given entries in order.
func writeZip(t *testing.T, entries [][2]string) string {
t.Helper()
path := filepath.Join(t.TempDir(), "pkg.zip")
f, err := os.Create(path)
require.NoError(t, err)
defer f.Close()
zw := zip.NewWriter(f)
for _, e := range entries {
w, err := zw.Create(e[0])
require.NoError(t, err)
_, err = w.Write([]byte(e[1]))
require.NoError(t, err)
}
require.NoError(t, zw.Close())
return path
}
func TestExtractZIPMetadata(t *testing.T) {
// a valid ipa returns metadata without error
tfr, err := fleet.NewKeepFileReader(filepath.Join("testdata", "software-installers", "ipa_test.ipa"))
require.NoError(t, err)
defer tfr.Close()
meta, err := file.ExtractZIPMetadata(tfr)
require.NoError(t, err)
require.NotNil(t, meta)
// a zip-based package with no Info.plist at all is not an ipa. This has the
// same magic bytes as a Windows .zip installer and likewise has no Info.plist,
// so it covers that case too; we use a real .msix here.
msixTfr, err := fleet.NewKeepFileReader(filepath.Join("testdata", "software-installers", "msix_test.msix"))
require.NoError(t, err)
defer msixTfr.Close()
meta, err = file.ExtractZIPMetadata(msixTfr)
require.ErrorIs(t, err, file.ErrInvalidType)
require.Nil(t, meta)
// the same msix renamed to a .msi extension still returns invalid type
obfuscatedPath := filepath.Join(t.TempDir(), "not-really-an.msi")
require.NoError(t, file.Copy(filepath.Join("testdata", "software-installers", "msix_test.msix"), obfuscatedPath, 0o644))
obfuscatedTfr, err := fleet.NewKeepFileReader(obfuscatedPath)
require.NoError(t, err)
defer obfuscatedTfr.Close()
meta, err = file.ExtractZIPMetadata(obfuscatedTfr)
require.ErrorIs(t, err, file.ErrInvalidType)
require.Nil(t, meta)
// a macOS .app zip has an Info.plist but no LSRequiresIPhoneOS, so it is not an ipa
macTfr, err := fleet.NewKeepFileReader(writeZip(t, [][2]string{
{"MacApp.app/Contents/Info.plist", infoPlist("com.example.mac", false)},
}))
require.NoError(t, err)
defer macTfr.Close()
meta, err = file.ExtractZIPMetadata(macTfr)
require.ErrorIs(t, err, file.ErrInvalidType)
require.Nil(t, meta)
// once LSRequiresIPhoneOS is seen it stays set, so a framework plist without
// the key coming after the app plist doesn't undo ipa detection
latchTfr, err := fleet.NewKeepFileReader(writeZip(t, [][2]string{
{"Payload/App.app/Info.plist", infoPlist("com.example.ios", true)},
{"Payload/App.app/Frameworks/Bar.framework/Info.plist", infoPlist("com.example.framework", false)},
}))
require.NoError(t, err)
defer latchTfr.Close()
meta, err = file.ExtractZIPMetadata(latchTfr)
require.NoError(t, err)
require.NotNil(t, meta)
}