diff --git a/.github/workflows/build-orbit.yaml b/.github/workflows/build-orbit.yaml new file mode 100644 index 0000000000..2099d3f7ad --- /dev/null +++ b/.github/workflows/build-orbit.yaml @@ -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 diff --git a/ee/cis/macos-13/cis-policy-queries.yml b/ee/cis/macos-13/cis-policy-queries.yml index 91edff7aa1..b928fec070 100644 --- a/ee/cis/macos-13/cis-policy-queries.yml +++ b/ee/cis/macos-13/cis-policy-queries.yml @@ -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 diff --git a/ee/cis/macos-13/test/scripts/CIS_2.9.1.sh b/ee/cis/macos-13/test/scripts/CIS_2.9.1.sh new file mode 100644 index 0000000000..993ddd2af1 --- /dev/null +++ b/ee/cis/macos-13/test/scripts/CIS_2.9.1.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +/usr/bin/sudo /usr/bin/pmset -a powernap 0 \ No newline at end of file diff --git a/ee/cis/macos-13/test/scripts/CIS_2.9.2.sh b/ee/cis/macos-13/test/scripts/CIS_2.9.2.sh new file mode 100644 index 0000000000..2ecab92e99 --- /dev/null +++ b/ee/cis/macos-13/test/scripts/CIS_2.9.2.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +/usr/bin/sudo /usr/bin/pmset -a womp 0 diff --git a/ee/cis/macos-13/test/scripts/CIS_2.9.3.sh b/ee/cis/macos-13/test/scripts/CIS_2.9.3.sh new file mode 100755 index 0000000000..cc877132f3 --- /dev/null +++ b/ee/cis/macos-13/test/scripts/CIS_2.9.3.sh @@ -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 diff --git a/orbit/changes/9253-add-fleetd-table-pmset b/orbit/changes/9253-add-fleetd-table-pmset new file mode 100644 index 0000000000..6b676cf32d --- /dev/null +++ b/orbit/changes/9253-add-fleetd-table-pmset @@ -0,0 +1 @@ +* Add `pmset` table extension to fleed for CIS check 2.9.1. diff --git a/orbit/cmd/orbit/orbit.go b/orbit/cmd/orbit/orbit.go index 9cecfa97cd..328e2e2f07 100644 --- a/orbit/cmd/orbit/orbit.go +++ b/orbit/cmd/orbit/orbit.go @@ -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" diff --git a/orbit/pkg/table/common/common.go b/orbit/pkg/table/common/common_darwin.go similarity index 100% rename from orbit/pkg/table/common/common.go rename to orbit/pkg/table/common/common_darwin.go diff --git a/orbit/pkg/table/common/common_test.go b/orbit/pkg/table/common/common_darwin_test.go similarity index 100% rename from orbit/pkg/table/common/common_test.go rename to orbit/pkg/table/common/common_darwin_test.go diff --git a/orbit/pkg/table/extension_darwin.go b/orbit/pkg/table/extension_darwin.go index 4916a5359b..26035ee9d8 100644 --- a/orbit/pkg/table/extension_darwin.go +++ b/orbit/pkg/table/extension_darwin.go @@ -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), diff --git a/orbit/pkg/table/pmset/pmset_darwin.go b/orbit/pkg/table/pmset/pmset_darwin.go new file mode 100644 index 0000000000..ac43d28600 --- /dev/null +++ b/orbit/pkg/table/pmset/pmset_darwin.go @@ -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 +} diff --git a/orbit/pkg/table/pmset/pmset_darwin_test.go b/orbit/pkg/table/pmset/pmset_darwin_test.go new file mode 100644 index 0000000000..c514bafa86 --- /dev/null +++ b/orbit/pkg/table/pmset/pmset_darwin_test.go @@ -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") +} diff --git a/orbit/pkg/table/privaterelay/privaterelay.go b/orbit/pkg/table/privaterelay/privaterelay_darwin.go similarity index 100% rename from orbit/pkg/table/privaterelay/privaterelay.go rename to orbit/pkg/table/privaterelay/privaterelay_darwin.go index c53e50969e..c7d365a1e5 100644 --- a/orbit/pkg/table/privaterelay/privaterelay.go +++ b/orbit/pkg/table/privaterelay/privaterelay_darwin.go @@ -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" ) diff --git a/orbit/tools/build/build.go b/orbit/tools/build/build.go new file mode 100644 index 0000000000..4332cd38a0 --- /dev/null +++ b/orbit/tools/build/build.go @@ -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 +} diff --git a/pkg/buildpkg/buildpkg.go b/pkg/buildpkg/buildpkg.go new file mode 100644 index 0000000000..ee9ab4cd1b --- /dev/null +++ b/pkg/buildpkg/buildpkg.go @@ -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 +} diff --git a/tools/desktop/desktop.go b/tools/desktop/desktop.go index 4115f05dc1..cc15e720ef 100644 --- a/tools/desktop/desktop.go +++ b/tools/desktop/desktop.go @@ -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 -}