Add CIS checks for 2.9.X and add pmset table to fleetd (#9470)

#9253

- ~[ ] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.~
- ~[ ] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)~
- ~[ ] Documented any permissions changes~
- ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)~
- ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.~
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
  - For Orbit and Fleet Desktop changes:
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- ~[ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~

---------

Co-authored-by: Sharon Katz <121527325+sharon-fdm@users.noreply.github.com>
This commit is contained in:
Lucas Manuel Rodriguez 2023-02-08 13:08:17 -03:00 committed by GitHub
parent 3cdea3c896
commit d4a1b4d218
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 675 additions and 127 deletions

64
.github/workflows/build-orbit.yaml vendored Normal file
View File

@ -0,0 +1,64 @@
name: Build, Sign and Notarize Orbit
on:
pull_request:
paths:
- 'orbit/**.go'
# This allows a subsequently queued workflow run to interrupt previous runs
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id}}
cancel-in-progress: true
defaults:
run:
# fail-fast using bash -eo pipefail. See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference
shell: bash
permissions:
contents: read
jobs:
build:
runs-on: macos-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2
- name: Import signing keys
env:
APPLE_APPLICATION_CERTIFICATE: ${{ secrets.APPLE_APPLICATION_CERTIFICATE }}
APPLE_APPLICATION_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_APPLICATION_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
echo "$APPLE_APPLICATION_CERTIFICATE" | base64 --decode > certificate.p12
security create-keychain -p $KEYCHAIN_PASSWORD build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p $KEYCHAIN_PASSWORD build.keychain
security import certificate.p12 -k build.keychain -P $APPLE_APPLICATION_CERTIFICATE_PASSWORD -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain
security find-identity -vv
rm certificate.p12
- name: Set up Go
uses: actions/setup-go@268d8c0ca0432bb2cf416faae41297df9d262d7f # v2
with:
go-version: 1.19.4
- name: Build, codesign and notarize orbit
run: go run ./orbit/tools/build/build.go
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AC_USERNAME: ${{ secrets.APPLE_USERNAME }}
AC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
AC_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
CODESIGN_IDENTITY: 51049B247B25B3119FAE7E9C0CC4375A43E47237
- name: Upload orbit
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v2
with:
name: orbit
path: |
orbit-darwin

View File

@ -656,6 +656,145 @@ spec:
---
apiVersion: v1
kind: policy
spec:
name: CIS - Ensure Power Nap Is Disabled for Intel Macs (Fleetd Required)
platforms: macOS
platform: darwin
description: |
Power Nap allows the system to stay in low power mode, especially while on battery power, and periodically
connect to previously known networks with stored credentials for user applications to phone home and get updates.
This capability requires FileVault to remain unlocked and the use of previously joined networks to be risk accepted
based on the SSID without user input.
Disabling this feature mitigates the risk of an attacker remotely waking the system and gaining access.
resolution: |
Automated method:
Ask your system administrator to deploy a script that runs the following command to turn off
Power Nap on the device:
/usr/bin/sudo /usr/bin/pmset -a powernap 0
query: |
SELECT 1 FROM (SELECT
COALESCE(JSON_EXTRACT(
JSON_EXTRACT(json_result, '$.AC Power:'),
'$.powernap'
), '') AS powernap_ac,
COALESCE(JSON_EXTRACT(
JSON_EXTRACT(json_result, '$.Battery Power:'),
'$.powernap'
), '') AS powernap_battery
FROM pmset WHERE getting = 'custom' AND powernap_battery != '1' AND powernap_ac != '1');
purpose: Informational
tags: compliance, CIS, CIS_Level1, CIS2.9.1
contributors: lucasmrod
---
apiVersion: v1
kind: policy
spec:
name: CIS - Ensure Wake for Network Access Is Disabled (Fleetd Required)
platforms: macOS
platform: darwin
description: |
Wake for Network Access allows the computer to take action when the user is not present and the computer
is in energy saving mode. These tools require FileVault to remain unlocked and fully rejoin known networks.
Disabling this feature mitigates the risk of an attacker remotely waking the system and gaining access.
resolution: |
Automated method:
Ask your system administrator to deploy a script that runs the following command to turn off
Wake on on the device:
/usr/bin/sudo /usr/bin/pmset -a womp 0
query: |
SELECT 1 FROM (SELECT
COALESCE(JSON_EXTRACT(
JSON_EXTRACT(json_result, '$.AC Power:'),
'$.womp'
), '') AS womp_ac,
COALESCE(JSON_EXTRACT(
JSON_EXTRACT(json_result, '$.Battery Power:'),
'$.womp'
), '') AS womp_battery
FROM pmset WHERE getting = 'custom' AND womp_battery != '1' AND womp_ac != '1');
purpose: Informational
tags: compliance, CIS, CIS_Level1, CIS2.9.2
contributors: lucasmrod
---
apiVersion: v1
kind: policy
spec:
name: CIS - Ensure the OS is not Activate When Resuming from Sleep
platforms: macOS
platform: darwin
description: |
In order to use a computer with Full Disk Encryption (FDE), macOS must keep encryption keys in memory to allow
the use of the disk that has been FileVault protected. When the system is not in use, the volume is protected
through encryption. When the system is sleeping and available to quickly resume, the encryption keys remain in memory.
If an unauthorized party has possession of the computer and the computer is only slept, there are known attack vectors
that can be attempted against the RAM that has the encryption keys or the running operating system protected
by a login screen.
Mac systems should be set to hibernate after sleeping for a risk-acceptable time period.
MacBooks should be set so that the `standbydelay` is 15 minutes (900 seconds) or less.
resolution: |
Ask your system administrator to deploy the following script to Macbook devices:
if [[ $(uname -m) == 'arm64' ]]; then
# Apple silicon
/usr/bin/sudo /usr/bin/pmset -a standby 900
/usr/bin/sudo /usr/bin/pmset -a destroyfvkeyonstandby 1
/usr/bin/sudo /usr/bin/pmset -a hibernatemode 25
else
# Intel
/usr/bin/sudo /usr/bin/pmset -a standbydelaylow 900
/usr/bin/sudo /usr/bin/pmset -a standbydelayhigh 900
/usr/bin/sudo /usr/bin/pmset -a highstandbythreshold 90
/usr/bin/sudo /usr/bin/pmset -a destroyfvkeyonstandby 1
/usr/bin/sudo /usr/bin/pmset -a hibernatemode 25
fi
query: |
SELECT 1 WHERE EXISTS(
SELECT 1 FROM system_info WHERE
regex_match(hardware_model, '^Mac[0-9,]+$', 0) != '' OR regex_match(hardware_model, '^MacBook', 0) != 0
)
AND EXISTS(
SELECT JSON_EXTRACT(system_wide_power_settings, '$.DestroyFVKeyOnStandby') AS destroy_fv_key_on_standby
FROM (
SELECT JSON_EXTRACT(json_result, '$.System-wide power settings:') AS system_wide_power_settings FROM pmset
)
WHERE destroy_fv_key_on_standby = '1'
)
AND EXISTS(
SELECT 1 WHERE EXISTS(
SELECT 1 WHERE EXISTS(
SELECT 1 FROM system_info WHERE cpu_type = 'x86_64h' OR cpu_type = 'x86_64'
) AND EXISTS(
SELECT
CAST(JSON_EXTRACT(battery, '$.standbydelaylow') AS INTEGER) AS standbydelaylow,
CAST(JSON_EXTRACT(battery, '$.standbydelayhigh') AS INTEGER) AS standbydelayhigh,
CAST(JSON_EXTRACT(battery, '$.highstandbythreshold') AS INTEGER) AS highstandbythreshold,
CAST(JSON_EXTRACT(battery, '$.hibernatemode') AS INTEGER) AS hibernatemode
FROM (
SELECT JSON_EXTRACT(json_result, '$.Battery Power:') as battery FROM pmset WHERE getting = 'custom'
)
WHERE standbydelaylow <= 900 AND standbydelayhigh <= 900 AND highstandbythreshold >= 90 AND hibernatemode = 25
)
) OR EXISTS(
SELECT 1 WHERE EXISTS(
SELECT 1 FROM system_info WHERE cpu_type = 'arm64e'
) AND EXISTS (
SELECT
CAST(JSON_EXTRACT(battery, '$.standby') AS INTEGER) AS standby,
CAST(JSON_EXTRACT(battery, '$.hibernatemode') AS INTEGER) AS hibernatemode
FROM (
SELECT JSON_EXTRACT(json_result, '$.Battery Power:') AS battery FROM pmset WHERE getting = 'custom'
)
WHERE standby <= 900 AND hibernatemode = 25
)
)
);
purpose: Informational
tags: compliance, CIS, CIS_Level2, CIS2.9.3
contributors: lucasmrod
---
apiVersion: v1
kind: policy
spec:
name: CIS - Ensure a Password is Required to Wake the Computer From Sleep or Screen Saver Is Enabled (MDM Required)
platforms: macOS

View File

@ -0,0 +1,3 @@
#!/bin/bash
/usr/bin/sudo /usr/bin/pmset -a powernap 0

View File

@ -0,0 +1,3 @@
#!/bin/bash
/usr/bin/sudo /usr/bin/pmset -a womp 0

View File

@ -0,0 +1,15 @@
#!/bin/bash
if [[ $(uname -m) == 'arm64' ]]; then
# Apple silicon
/usr/bin/sudo /usr/bin/pmset -a standby 900
/usr/bin/sudo /usr/bin/pmset -a destroyfvkeyonstandby 1
/usr/bin/sudo /usr/bin/pmset -a hibernatemode 25
else
# Intel
/usr/bin/sudo /usr/bin/pmset -a standbydelaylow 900
/usr/bin/sudo /usr/bin/pmset -a standbydelayhigh 900
/usr/bin/sudo /usr/bin/pmset -a highstandbythreshold 90
/usr/bin/sudo /usr/bin/pmset -a destroyfvkeyonstandby 1
/usr/bin/sudo /usr/bin/pmset -a hibernatemode 25
fi

View File

@ -0,0 +1 @@
* Add `pmset` table extension to fleed for CIS check 2.9.1.

View File

@ -16,9 +16,6 @@ import (
"strings"
"time"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/service"
"github.com/fleetdm/fleet/v4/orbit/pkg/augeas"
"github.com/fleetdm/fleet/v4/orbit/pkg/build"
"github.com/fleetdm/fleet/v4/orbit/pkg/constant"
@ -34,6 +31,8 @@ import (
"github.com/fleetdm/fleet/v4/pkg/certificate"
"github.com/fleetdm/fleet/v4/pkg/file"
"github.com/fleetdm/fleet/v4/pkg/secure"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/service"
"github.com/oklog/run"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"

View File

@ -6,6 +6,7 @@ import (
"github.com/fleetdm/fleet/v4/orbit/pkg/table/authdb"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/csrutil_info"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/nvram_info"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/pmset"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/privaterelay"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/pwd_policy"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/user_login_settings"
@ -27,6 +28,7 @@ func platformTables() []osquery.OsqueryPlugin {
table.NewPlugin("csrutil_info", csrutil_info.Columns(), csrutil_info.Generate),
table.NewPlugin("nvram_info", nvram_info.Columns(), nvram_info.Generate),
table.NewPlugin("authdb", authdb.Columns(), authdb.Generate),
table.NewPlugin("pmset", pmset.Columns(), pmset.Generate),
// Macadmins extension tables
table.NewPlugin("filevault_users", filevaultusers.FileVaultUsersColumns(), filevaultusers.FileVaultUsersGenerate),

View File

@ -0,0 +1,90 @@
//go:build darwin
// +build darwin
// Package pmset implements the table for getting macOS power settings
// with the `pmset -g` command
package pmset
import (
"bufio"
"bytes"
"context"
"encoding/json"
"os/exec"
"regexp"
"strings"
"github.com/osquery/osquery-go/plugin/table"
"github.com/rs/zerolog/log"
)
// Columns is the schema of the table.
func Columns() []table.ColumnDefinition {
return []table.ColumnDefinition{
table.TextColumn("getting"), // pmset -g (aka GETTING option)
table.TextColumn("json_result"),
}
}
var linePattern = regexp.MustCompile(`^(\w+)[\t\f\r ]+(\S[\S\t\f\r ]*)$`)
// Generate is called to return the results for the table at query time.
//
// Constraints for generating can be retrieved from the queryContext.
func Generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) {
getting := ""
if constraints, ok := queryContext.Constraints["getting"]; ok {
for _, constraint := range constraints.Constraints {
if constraint.Operator == table.OperatorEquals {
getting = constraint.Expression
}
}
}
output, err := exec.CommandContext(ctx, "pmset", "-g", getting).CombinedOutput()
if err != nil {
return nil, err
}
result := parsePMSetOutput(output)
jsonResult, err := json.Marshal(result)
if err != nil {
return nil, err
}
return []map[string]string{{
"getting": getting,
"json_result": string(jsonResult),
}}, nil
}
func parsePMSetOutput(output []byte) map[string]interface{} {
scanner := bufio.NewScanner(bytes.NewReader(output))
result := make(map[string]interface{})
curKey := ""
for scanner.Scan() {
line := scanner.Text()
if line[0] != ' ' {
curKey = strings.TrimSpace(line)
result[curKey] = make(map[string]string)
continue
}
line = strings.TrimSpace(line)
loc := linePattern.FindStringSubmatch(line)
if loc == nil {
log.Debug().Str("line", line).Msg("failed to match line, ignoring")
continue
}
if len(loc) != 3 {
log.Debug().Str("line", line).Msg("invalid number of submatches")
continue
}
m := result[curKey].(map[string]string)
m[loc[1]] = loc[2]
}
return result
}

View File

@ -0,0 +1,110 @@
//go:build darwin
// +build darwin
package pmset
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestParsePMSetOutput(t *testing.T) {
// pmset -g
sampleOutput := []byte(`System-wide power settings:
SleepDisabled 0
Currently in use:
lidwake 1
lowpowermode 0
standbydelayhigh 86400
proximitywake 1
standby 1
standbydelaylow 0
ttyskeepawake 1
hibernatemode 3
powernap 0
gpuswitch 2
hibernatefile /var/vm/sleepimage
highstandbythreshold 50
displaysleep 10
womp 1
networkoversleep 0
sleep 1 (sleep prevented by bluetoothd, coreaudiod)
acwake 0
halfdim 1
tcpkeepalive 1
disksleep 10`)
result := parsePMSetOutput(sampleOutput)
systemWide := result["System-wide power settings:"].(map[string]string)
require.NotNil(t, systemWide)
require.Equal(t, systemWide["SleepDisabled"], "0")
currInUse := result["Currently in use:"].(map[string]string)
require.Equal(t, currInUse["powernap"], "0")
require.Equal(t, currInUse["hibernatefile"], "/var/vm/sleepimage")
require.Equal(t, currInUse["highstandbythreshold"], "50")
require.Equal(t, currInUse["sleep"], "1 (sleep prevented by bluetoothd, coreaudiod)")
// pmset -g custom
sampleOutput = []byte(`Battery Power:
lidwake 1
lowpowermode 1
standbydelayhigh 86400
proximitywake 0
standby 1
standbydelaylow 10800
ttyskeepawake 1
hibernatemode 3
gpuswitch 2
powernap 0
hibernatefile /var/vm/sleepimage
highstandbythreshold 50
displaysleep 2
womp 0
networkoversleep 0
sleep 1
lessbright 1
halfdim 1
tcpkeepalive 1
acwake 0
disksleep 10
AC Power:
lidwake 1
lowpowermode 0
standbydelayhigh 86400
proximitywake 1
standby 1
standbydelaylow 0
ttyskeepawake 1
hibernatemode 3
powernap 0
gpuswitch 2
hibernatefile /var/vm/sleepimage
highstandbythreshold 50
displaysleep 10
womp 1
networkoversleep 0
sleep 1
acwake 0
halfdim 1
tcpkeepalive 1
disksleep 10
`)
result = parsePMSetOutput(sampleOutput)
batteryPower := result["Battery Power:"].(map[string]string)
require.NotNil(t, batteryPower)
require.Equal(t, batteryPower["powernap"], "0")
require.Equal(t, batteryPower["displaysleep"], "2")
require.Equal(t, batteryPower["highstandbythreshold"], "50")
require.Equal(t, batteryPower["hibernatefile"], "/var/vm/sleepimage")
require.Equal(t, batteryPower["disksleep"], "10")
acPower := result["AC Power:"].(map[string]string)
require.Equal(t, acPower["powernap"], "0")
require.Equal(t, acPower["displaysleep"], "10")
require.Equal(t, acPower["highstandbythreshold"], "50")
require.Equal(t, acPower["hibernatefile"], "/var/vm/sleepimage")
require.Equal(t, acPower["disksleep"], "10")
}

View File

@ -6,12 +6,12 @@ package privaterelay
import (
"context"
"fmt"
tbl_common "github.com/fleetdm/fleet/v4/orbit/pkg/table/common"
"os/exec"
"strings"
"syscall"
"time"
tbl_common "github.com/fleetdm/fleet/v4/orbit/pkg/table/common"
"github.com/osquery/osquery-go/plugin/table"
)

111
orbit/tools/build/build.go Normal file
View File

@ -0,0 +1,111 @@
//go:build ignore
// +build ignore
package main
// This tool builds Orbit as macOS Universal Binary, codesigns it and notarizes it.
// It currently doesn't support stapling of the binary.
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"github.com/fleetdm/fleet/v4/orbit/pkg/packaging"
"github.com/fleetdm/fleet/v4/pkg/buildpkg"
"github.com/mitchellh/gon/package/zip"
zlog "github.com/rs/zerolog/log"
)
func main() {
// Codesigning configuration
codesignIdentity := os.Getenv("CODESIGN_IDENTITY")
// Notarization configuration
acUsername := os.Getenv("AC_USERNAME")
acPassword := os.Getenv("AC_PASSWORD")
acTeamID := os.Getenv("AC_TEAM_ID")
codesign := false
if codesignIdentity != "" {
codesign = true
} else {
zlog.Info().Msg("skipping running codesign: CODESIGN_IDENTITY not set")
}
notarize := false
if acUsername != "" && acPassword != "" && acTeamID != "" {
notarize = true
} else {
zlog.Info().Msg("skipping running notarization: AC_USERNAME, AC_PASSWORD, AC_TEAM_ID not all set")
}
const (
amdBinaryPath = "orbit-darwin-amd64"
armBinaryPath = "orbit-darwin-arm64"
binaryPath = "orbit-darwin"
bundleIdentifier = "com.fleetdm.orbit"
)
if err := buildOrbit(amdBinaryPath, "amd64"); err != nil {
panic(err)
}
if err := buildOrbit(armBinaryPath, "arm64"); err != nil {
panic(err)
}
if err := buildpkg.MakeMacOSFatExecutable(binaryPath, amdBinaryPath, armBinaryPath); err != nil {
panic(err)
}
if err := os.Remove(amdBinaryPath); err != nil {
panic(err)
}
if err := os.Remove(armBinaryPath); err != nil {
panic(err)
}
if codesign {
codeSign := exec.Command("codesign", "-s", codesignIdentity, "-i", bundleIdentifier,
"-f", "-v", "--timestamp", "--options", "runtime", binaryPath,
)
zlog.Info().Str("command", codeSign.String()).Msgf("signing %s", binaryPath)
codeSign.Stderr = os.Stderr
codeSign.Stdout = os.Stdout
if err := codeSign.Run(); err != nil {
panic(err)
}
}
if notarize {
const notarizationZip = "orbit.zip"
// NOTE(lucas): The binary needs to be zipped in order to upload to Apple for Notarization.
if err := zip.Zip(context.Background(), &zip.Options{Files: []string{binaryPath}, OutputPath: notarizationZip}); err != nil {
panic(err)
}
defer os.Remove(notarizationZip)
if err := packaging.Notarize(notarizationZip, bundleIdentifier); err != nil {
panic(err)
}
// TODO(lucas): packaging.Staple doesn't work on plain binaries.
}
}
func buildOrbit(binaryPath, arch string) error {
/* #nosec G204 -- arguments are actually well defined */
buildExec := exec.Command("go", "build",
"-o", binaryPath,
"./"+filepath.Join("orbit", "cmd", "orbit"),
)
buildExec.Env = append(os.Environ(), "GOOS=darwin", "GOARCH="+arch)
buildExec.Stderr = os.Stderr
buildExec.Stdout = os.Stdout
zlog.Info().Str("command", buildExec.String()).Str("arch", arch).Msg("build orbit executable")
if err := buildExec.Run(); err != nil {
return fmt.Errorf("compile for %s: %w", arch, err)
}
return nil
}

131
pkg/buildpkg/buildpkg.go Normal file
View File

@ -0,0 +1,131 @@
// Package buildpkg contains utilities to build Fleet components.
package buildpkg
import (
"debug/macho"
"encoding/binary"
"errors"
"fmt"
"io/ioutil"
"os"
)
// Adapted from Unlicensed https://github.com/randall77/makefat/blob/master/makefat.go
const (
magicFat64 = macho.MagicFat + 1 // TODO: add to stdlib (...when it works)
// Alignment wanted for each sub-file.
// amd64 needs 12 bits, arm64 needs 14. We choose the max of all requirements here.
alignBits = 14
align = 1 << alignBits
)
// MakeMacOSFatExecutable makes a macOS fat executable from the given binaries.
func MakeMacOSFatExecutable(outPath string, inPaths ...string) error {
// Read input files.
type input struct {
data []byte
cpu uint32
subcpu uint32
offset int64
}
var inputs []input
offset := int64(align)
for _, i := range inPaths {
data, err := ioutil.ReadFile(i)
if err != nil {
return err
}
if len(data) < 12 {
return fmt.Errorf("file %s too small", i)
}
// All currently supported mac archs (386,amd64,arm,arm64) are little endian.
magic := binary.LittleEndian.Uint32(data[0:4])
if magic != macho.Magic32 && magic != macho.Magic64 {
return fmt.Errorf("input %s is not a macho file, magic=%x", i, magic)
}
cpu := binary.LittleEndian.Uint32(data[4:8])
subcpu := binary.LittleEndian.Uint32(data[8:12])
inputs = append(inputs, input{data: data, cpu: cpu, subcpu: subcpu, offset: offset})
offset += int64(len(data))
offset = (offset + align - 1) / align * align
}
// Decide on whether we're doing fat32 or fat64.
sixtyfour := false
if inputs[len(inputs)-1].offset >= 1<<32 || len(inputs[len(inputs)-1].data) >= 1<<32 {
// fat64 doesn't seem to work:
// - the resulting binary won't run.
// - the resulting binary is parseable by lipo, but reports that the contained files are "hidden".
// - the native OSX lipo can't make a fat64.
return errors.New("files too large to fit into a fat binary")
}
// Make output file.
out, err := os.Create(outPath)
if err != nil {
return err
}
err = out.Chmod(0o755)
if err != nil {
return err
}
// Build a fat_header.
var hdr []uint32
if sixtyfour {
hdr = append(hdr, magicFat64)
} else {
hdr = append(hdr, macho.MagicFat)
}
hdr = append(hdr, uint32(len(inputs)))
// Build a fat_arch for each input file.
for _, i := range inputs {
hdr = append(hdr, i.cpu)
hdr = append(hdr, i.subcpu)
if sixtyfour {
hdr = append(hdr, uint32(i.offset>>32)) // big endian
}
hdr = append(hdr, uint32(i.offset))
if sixtyfour {
hdr = append(hdr, uint32(len(i.data)>>32)) // big endian
}
hdr = append(hdr, uint32(len(i.data)))
hdr = append(hdr, alignBits)
if sixtyfour {
hdr = append(hdr, 0) // reserved
}
}
// Write header.
// Note that the fat binary header is big-endian, regardless of the
// endianness of the contained files.
err = binary.Write(out, binary.BigEndian, hdr)
if err != nil {
return err
}
offset = int64(4 * len(hdr))
// Write each contained file.
for _, i := range inputs {
if offset < i.offset {
_, err = out.Write(make([]byte, i.offset-offset))
if err != nil {
return err
}
offset = i.offset
}
_, err := out.Write(i.data)
if err != nil {
return err
}
offset += int64(len(i.data))
}
err = out.Close()
if err != nil {
return err
}
return nil
}

View File

@ -4,8 +4,6 @@ import (
"archive/tar"
"compress/gzip"
"context"
"debug/macho"
"encoding/binary"
"errors"
"fmt"
"io"
@ -17,6 +15,7 @@ import (
"github.com/fleetdm/fleet/v4/orbit/pkg/constant"
"github.com/fleetdm/fleet/v4/orbit/pkg/packaging"
"github.com/fleetdm/fleet/v4/pkg/buildpkg"
"github.com/fleetdm/fleet/v4/pkg/secure"
"github.com/kolide/kit/version"
"github.com/mitchellh/gon/package/zip"
@ -183,8 +182,8 @@ func createMacOSApp(version, authority string, notarize bool) error {
}
// Make the fat exe and remove the separate binaries
if err := makeFatExecutable(binaryPath, amdBinaryPath, armBinaryPath); err != nil {
return fmt.Errorf("make fat exectuable: %w", err)
if err := buildpkg.MakeMacOSFatExecutable(binaryPath, amdBinaryPath, armBinaryPath); err != nil {
return fmt.Errorf("make fat executable: %w", err)
}
if err := os.Remove(amdBinaryPath); err != nil {
return fmt.Errorf("remove amd64 binary: %w", err)
@ -294,122 +293,3 @@ func compressDir(outPath, dirPath string) error {
return nil
}
// Adapted from Unlicensed https://github.com/randall77/makefat/blob/master/makefat.go
const (
MagicFat64 = macho.MagicFat + 1 // TODO: add to stdlib (...when it works)
// Alignment wanted for each sub-file.
// amd64 needs 12 bits, arm64 needs 14. We choose the max of all requirements here.
alignBits = 14
align = 1 << alignBits
)
func makeFatExecutable(outPath string, inPaths ...string) error {
// Read input files.
type input struct {
data []byte
cpu uint32
subcpu uint32
offset int64
}
var inputs []input
offset := int64(align)
for _, i := range inPaths {
data, err := ioutil.ReadFile(i)
if err != nil {
return err
}
if len(data) < 12 {
return fmt.Errorf("file %s too small", i)
}
// All currently supported mac archs (386,amd64,arm,arm64) are little endian.
magic := binary.LittleEndian.Uint32(data[0:4])
if magic != macho.Magic32 && magic != macho.Magic64 {
return fmt.Errorf("input %s is not a macho file, magic=%x", i, magic)
}
cpu := binary.LittleEndian.Uint32(data[4:8])
subcpu := binary.LittleEndian.Uint32(data[8:12])
inputs = append(inputs, input{data: data, cpu: cpu, subcpu: subcpu, offset: offset})
offset += int64(len(data))
offset = (offset + align - 1) / align * align
}
// Decide on whether we're doing fat32 or fat64.
sixtyfour := false
if inputs[len(inputs)-1].offset >= 1<<32 || len(inputs[len(inputs)-1].data) >= 1<<32 {
// fat64 doesn't seem to work:
// - the resulting binary won't run.
// - the resulting binary is parseable by lipo, but reports that the contained files are "hidden".
// - the native OSX lipo can't make a fat64.
return errors.New("files too large to fit into a fat binary")
}
// Make output file.
out, err := os.Create(outPath)
if err != nil {
return err
}
err = out.Chmod(0o755)
if err != nil {
return err
}
// Build a fat_header.
var hdr []uint32
if sixtyfour {
hdr = append(hdr, MagicFat64)
} else {
hdr = append(hdr, macho.MagicFat)
}
hdr = append(hdr, uint32(len(inputs)))
// Build a fat_arch for each input file.
for _, i := range inputs {
hdr = append(hdr, i.cpu)
hdr = append(hdr, i.subcpu)
if sixtyfour {
hdr = append(hdr, uint32(i.offset>>32)) // big endian
}
hdr = append(hdr, uint32(i.offset))
if sixtyfour {
hdr = append(hdr, uint32(len(i.data)>>32)) // big endian
}
hdr = append(hdr, uint32(len(i.data)))
hdr = append(hdr, alignBits)
if sixtyfour {
hdr = append(hdr, 0) // reserved
}
}
// Write header.
// Note that the fat binary header is big-endian, regardless of the
// endianness of the contained files.
err = binary.Write(out, binary.BigEndian, hdr)
if err != nil {
return err
}
offset = int64(4 * len(hdr))
// Write each contained file.
for _, i := range inputs {
if offset < i.offset {
_, err = out.Write(make([]byte, i.offset-offset))
if err != nil {
return err
}
offset = i.offset
}
_, err := out.Write(i.data)
if err != nil {
return err
}
offset += int64(len(i.data))
}
err = out.Close()
if err != nil {
return err
}
return nil
}