├── Package
├── outset
├── Library
│ ├── LaunchDaemons
│ │ ├── io.macadmins.Outset.boot.plist
│ │ ├── io.macadmins.Outset.cleanup.plist
│ │ ├── io.macadmins.Outset.login-privileged.plist
│ │ └── io.macadmins.Outset.on-demand-privileged.plist
│ └── LaunchAgents
│ │ ├── io.macadmins.Outset.login.plist
│ │ ├── io.macadmins.Outset.login-window.plist
│ │ └── io.macadmins.Outset.on-demand.plist
├── Scripts
│ ├── preinstall
│ └── postinstall
├── generatePackage.zsh
├── outset-pkg
└── outset-payload
├── legacy
├── support-files
│ ├── Outset.png
│ └── com.chilcote.outset.plist
├── custom-ondemand
│ ├── scripts
│ │ └── postinstall
│ ├── pkgroot
│ │ └── usr
│ │ │ └── local
│ │ │ └── outset
│ │ │ ├── login-every
│ │ │ └── login-every_example.py
│ │ │ ├── login-once
│ │ │ └── login-once_example.py
│ │ │ └── on-demand
│ │ │ └── on-demand_example.sh
│ └── Makefile
├── custom-outset
│ ├── pkgroot
│ │ └── usr
│ │ │ └── local
│ │ │ └── outset
│ │ │ ├── boot-every
│ │ │ └── boot-every_example.py
│ │ │ ├── boot-once
│ │ │ └── boot-once_example.py
│ │ │ ├── login-every
│ │ │ └── login-every_example.py
│ │ │ ├── login-once
│ │ │ └── login-once_example.py
│ │ │ ├── login-privileged-every
│ │ │ └── login-privileged-every_example.py
│ │ │ └── login-privileged-once
│ │ │ └── login-privileged-once_example.py
│ └── Makefile
├── Requirements.plist
├── .flake8
├── Makefile
├── scripts
│ └── postinstall
├── README.md
└── pkgroot
│ └── usr
│ └── local
│ └── outset
│ └── outset.py
├── Outset
├── Assets.xcassets
│ ├── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── Outset.png_16x16.png
│ │ ├── Outset.png_32x32.png
│ │ ├── Outset.png_128x128.png
│ │ ├── Outset.png_16x16@2x.png
│ │ ├── Outset.png_256x256.png
│ │ ├── Outset.png_32x32@2x.png
│ │ ├── Outset.png_512x512.png
│ │ ├── Outset.png_128x128@2x.png
│ │ ├── Outset.png_256x256@2x.png
│ │ ├── Outset.png_512x512@2x.png
│ │ └── Contents.json
│ └── AccentColor.colorset
│ │ └── Contents.json
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── Outset.entitlements
├── Extensions
│ ├── URL+additions.swift
│ ├── Data+additions.swift
│ └── String+additions.swift
├── Info.plist
├── RunModes
│ ├── Cleanup.swift
│ ├── Boot.swift
│ ├── OnDemand.swift
│ ├── IgnoredUser.swift
│ ├── Override.swift
│ └── Login.swift
├── Utils
│ ├── FileUtils
│ │ ├── DMG.swift
│ │ ├── Packages.swift
│ │ ├── Shell.swift
│ │ ├── Checksum.swift
│ │ └── CoreFileUtils.swift
│ ├── SystemUtils.swift
│ ├── Network.swift
│ ├── SystemInfo.swift
│ ├── Services.swift
│ ├── Logging.swift
│ └── ItemProcessing.swift
├── UserDefaults
│ ├── Legacy
│ │ └── LegacyPreferences.swift
│ ├── Preferences.swift
│ └── Payloads.swift
├── Globals.swift
└── Outset.swift
├── Outset.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm
│ │ └── Package.resolved
├── xcuserdata
│ └── rea094.xcuserdatad
│ │ ├── xcdebugger
│ │ └── Breakpoints_v2.xcbkptlist
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
└── xcshareddata
│ └── xcschemes
│ ├── Outset Installer Package.xcscheme
│ └── Outset App Bundle.xcscheme
├── OutsetInstaller
└── OutsetInstaller.entitlements
├── .gitignore
├── CHANGELOG.md
├── .swiftlint.yml
├── .github
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
└── workflows
│ ├── build_outset_release_manual.yml
│ └── build_outset_prerelease_manual.yml
├── MDM
└── Jamf Pro
│ ├── service-status-ea.sh
│ └── outset-preferences-schema.json
├── README.md
└── LICENSE.md
/Package/outset:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | /usr/local/outset/Outset.app/Contents/MacOS/Outset ${@}
4 |
--------------------------------------------------------------------------------
/legacy/support-files/Outset.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macadmins/outset/HEAD/legacy/support-files/Outset.png
--------------------------------------------------------------------------------
/Outset/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Outset/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macadmins/outset/HEAD/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_16x16.png
--------------------------------------------------------------------------------
/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macadmins/outset/HEAD/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_32x32.png
--------------------------------------------------------------------------------
/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macadmins/outset/HEAD/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_128x128.png
--------------------------------------------------------------------------------
/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_16x16@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macadmins/outset/HEAD/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_16x16@2x.png
--------------------------------------------------------------------------------
/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macadmins/outset/HEAD/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_256x256.png
--------------------------------------------------------------------------------
/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_32x32@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macadmins/outset/HEAD/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_32x32@2x.png
--------------------------------------------------------------------------------
/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macadmins/outset/HEAD/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_512x512.png
--------------------------------------------------------------------------------
/legacy/custom-ondemand/scripts/postinstall:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | [[ $3 != "/" ]] && exit 0
4 |
5 | /usr/bin/touch /private/tmp/.com.github.outset.ondemand.launchd
6 |
7 | exit 0
8 |
--------------------------------------------------------------------------------
/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macadmins/outset/HEAD/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_128x128@2x.png
--------------------------------------------------------------------------------
/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_256x256@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macadmins/outset/HEAD/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_256x256@2x.png
--------------------------------------------------------------------------------
/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_512x512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macadmins/outset/HEAD/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_512x512@2x.png
--------------------------------------------------------------------------------
/Outset.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Outset/Outset.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Outset/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Outset.xcodeproj/xcuserdata/rea094.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
--------------------------------------------------------------------------------
/OutsetInstaller/OutsetInstaller.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | sample*
3 | *.pkg
4 | *.dmg
5 | *.pyc
6 | outset.wiki
7 | project.xcworkspace/
8 | Preview Content/
9 | xcuserdata/
10 | xcsharedata/
11 | build/
12 | outputs/
13 | OutsetPkg/
14 | build_info.txt
15 | build_info_main.txt
16 |
--------------------------------------------------------------------------------
/legacy/custom-ondemand/pkgroot/usr/local/outset/login-every/login-every_example.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | # Replace this script with your scripts and/or profiles
4 | # which you want to run at every login.
5 |
6 | print("These scripts will run at every login.")
7 |
--------------------------------------------------------------------------------
/legacy/custom-ondemand/pkgroot/usr/local/outset/login-once/login-once_example.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | # Replace this script with your scripts and/or profiles
4 | # which you want to run at login, only once.
5 |
6 | print("These scripts will run at login, once.")
7 |
--------------------------------------------------------------------------------
/legacy/custom-outset/pkgroot/usr/local/outset/boot-every/boot-every_example.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | # Replace this script with your scripts, profiles, and/or packages
4 | # which you want to run at every boot.
5 |
6 | print("These scripts will run at every boot.")
7 |
--------------------------------------------------------------------------------
/legacy/custom-outset/pkgroot/usr/local/outset/boot-once/boot-once_example.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | # Replace this script with your scripts, profiles, and/or packages
4 | # which you want to run at boot, only once.
5 |
6 | print("These scripts will run at boot, only once.")
7 |
--------------------------------------------------------------------------------
/legacy/custom-outset/pkgroot/usr/local/outset/login-every/login-every_example.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | # Replace this script with your scripts and/or profiles
4 | # which you want to run at every login, in the user context.
5 |
6 | print("These scripts will run at every login, in the user context.")
7 |
--------------------------------------------------------------------------------
/legacy/custom-outset/pkgroot/usr/local/outset/login-once/login-once_example.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | # Replace this script with your scripts and/or profiles
4 | # which you want to run at login, in the user context, only once.
5 |
6 | print("These scripts will run at login, in the user context, once.")
7 |
--------------------------------------------------------------------------------
/legacy/Requirements.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | os
6 |
7 | 10.15
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/legacy/custom-outset/pkgroot/usr/local/outset/login-privileged-every/login-privileged-every_example.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | # Replace this script with your packages, scripts, and/or profiles
4 | # which you want to run at every login, in the root context.
5 |
6 | print("These scripts will run at every login, in the root context.")
7 |
--------------------------------------------------------------------------------
/Outset.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Outset/Extensions/URL+additions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URL+additions.swift
3 | // Outset
4 | //
5 | // Created by Bart E Reardon on 5/9/2023.
6 | //
7 |
8 | import Foundation
9 |
10 | extension URL {
11 | var isDirectory: Bool {
12 | (try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/legacy/custom-outset/pkgroot/usr/local/outset/login-privileged-once/login-privileged-once_example.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | # Replace this script with your packages, scripts, and/or profiles
4 | # which you want to run at login, in the root context, only once.
5 |
6 | print("These scripts will run at login, in the root context, once.")
7 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 | All notable changes to this project will be documented in this file.
3 |
4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6 |
7 | ## [4.0] - 2023-03-23
8 | ### Added
9 | - Initial automated build of Outset
10 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | disabled_rules: # rule identifiers turned on by default to exclude from running
2 | - line_length
3 | - file_length
4 | - function_body_length
5 | - cyclomatic_complexity
6 | - large_tuple
7 | - force_cast
8 | - type_body_length
9 | - trailing_whitespace
10 |
11 | excluded: # paths to ignore during linting. Takes precedence over `included`.
12 | - ./build/SourcePackages/checkouts/swift-argument-parser/*
13 |
--------------------------------------------------------------------------------
/Outset/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | LSMinimumSystemVersion
6 | $(MACOSX_DEPLOYMENT_TARGET)
7 | CFBundleVersion
8 | 4.2.1
9 | CFBundleShortVersionString
10 | 4.2.1
11 |
12 |
13 |
--------------------------------------------------------------------------------
/legacy/support-files/com.chilcote.outset.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ignored_users
6 |
7 | super.sekrit.admin
8 |
9 | network_timeout
10 | 180
11 | wait_for_network
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/legacy/custom-ondemand/pkgroot/usr/local/outset/on-demand/on-demand_example.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Replace this script with your scripts
4 | # which you want to run on demand.
5 |
6 | /usr/bin/osascript -e 'display notification "Replace this script with scripts of your own!" with title "Outset"'
7 |
8 | # invoke login-once scripts during this on-demand run
9 | /usr/local/outset/outset --login-once
10 |
11 | # invoke login-every scripts during this on-demand run
12 | /usr/local/outset/outset --login-every
13 |
--------------------------------------------------------------------------------
/Outset/RunModes/Cleanup.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cleanup.swift
3 | // Outset
4 | //
5 | // Created by Bart Reardon on 6/8/2025.
6 | //
7 |
8 | import Foundation
9 |
10 | func runCleanup() {
11 | writeLog("Cleaning up on-demand directories.", logLevel: .info)
12 | pathCleanup(Trigger.onDemand.path)
13 | pathCleanup(Trigger.onDemandPrivileged.path)
14 | pathCleanup(PayloadType.onDemand.directoryPath)
15 | pathCleanup(PayloadType.onDemandPrivileged.directoryPath)
16 | pathCleanup(Trigger.cleanup.path)
17 | }
18 |
--------------------------------------------------------------------------------
/Outset.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "originHash" : "59ba1edda695b389d6c9ac1809891cd779e4024f505b0ce1a9d5202b6762e38a",
3 | "pins" : [
4 | {
5 | "identity" : "swift-argument-parser",
6 | "kind" : "remoteSourceControl",
7 | "location" : "https://github.com/apple/swift-argument-parser.git",
8 | "state" : {
9 | "revision" : "fddd1c00396eed152c45a46bea9f47b98e59301d",
10 | "version" : "1.2.0"
11 | }
12 | }
13 | ],
14 | "version" : 3
15 | }
16 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | _A clear and concise description of what the problem is._
12 |
13 | **Describe the solution you'd like**
14 | _A clear and concise description of what you want to happen._
15 |
16 | **Describe alternatives you've considered**
17 | _A clear and concise description of any alternative solutions or features you've considered._
18 |
19 | **Additional context**
20 | _Add any other context or screenshots about the feature request here._
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | _A clear and concise description of what the bug is._
12 |
13 | **To Reproduce**
14 | _Steps to reproduce the behavior:_
15 |
16 | **Expected behavior**
17 | _A clear and concise description of what you expected to happen._
18 |
19 | **Screenshots**
20 | _If applicable, add screenshots to help explain your problem._
21 |
22 | **System Details:**
23 | - OS:
24 | - Architecture
25 | - Version [e.g. 4.0]
26 |
27 | **Additional context**
28 | _Add any other context about the problem here._
29 |
--------------------------------------------------------------------------------
/Outset/Extensions/Data+additions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Data+additions.swift
3 | // Outset
4 | //
5 | // Created by Bart E Reardon on 5/9/2023.
6 | //
7 |
8 | import Foundation
9 | import CommonCrypto
10 |
11 | extension Data {
12 | // extension to the Data class that lets us compute sha256
13 | func sha256() -> Data {
14 | var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
15 | self.withUnsafeBytes {
16 | _ = CC_SHA256($0.baseAddress, CC_LONG(count), &hash)
17 | }
18 | return Data(hash)
19 | }
20 |
21 | func hexEncodedString() -> String {
22 | return map { String(format: "%02hhx", $0) }.joined()
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/legacy/custom-ondemand/Makefile:
--------------------------------------------------------------------------------
1 | PKGTITLE="outset-ondemand"
2 | PKGVERSION="1.0.0"
3 | PKGID=com.github.outset.ondemand
4 | PROJECT="outset-ondemand"
5 |
6 | #################################################
7 |
8 | ##Help - Show this help menu
9 | help:
10 | @fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//'
11 |
12 | ## clean - Clean up temporary working directories
13 | clean:
14 | rm -f ./outset*.pkg
15 | rm -f ./pkgroot/usr/local/outset/on-demand/*.pyc
16 |
17 | ## pkg - Create a package using pkgbuild
18 | pkg: clean
19 | pkgbuild --root pkgroot --scripts scripts --identifier ${PKGID} --version ${PKGVERSION} --ownership recommended ./${PKGTITLE}-${PKGVERSION}.pkg
20 |
--------------------------------------------------------------------------------
/Package/Library/LaunchDaemons/io.macadmins.Outset.boot.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Program
6 | /usr/local/outset/Outset.app/Contents/MacOS/Outset
7 | AssociatedBundleIdentifiers
8 | io.macadmins.Outset
9 | Label
10 | io.macadmins.Outset.boot
11 | ProgramArguments
12 |
13 | /usr/local/outset/Outset.app/Contents/MacOS/Outset
14 | --boot
15 |
16 | RunAtLoad
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Package/Library/LaunchAgents/io.macadmins.Outset.login.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Program
6 | /usr/local/outset/Outset.app/Contents/MacOS/Outset
7 | Label
8 | io.macadmins.Outset.login
9 | AssociatedBundleIdentifiers
10 | io.macadmins.Outset
11 | ProgramArguments
12 |
13 | /usr/local/outset/Outset.app/Contents/MacOS/Outset
14 | --login
15 |
16 | RunAtLoad
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Outset/Extensions/String+additions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+additions.swift
3 | // Outset
4 | //
5 | // Created by Bart E Reardon on 5/9/2023.
6 | //
7 |
8 | import Foundation
9 |
10 | extension String {
11 | func camelCaseToUnderscored() -> String {
12 | let regex = try? NSRegularExpression(pattern: "([a-z])([A-Z])", options: [])
13 | let range = NSRange(location: 0, length: utf16.count)
14 | return regex?.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: "$1_$2").lowercased() ?? self
15 | }
16 | }
17 |
18 | func getValueForKey(_ key: String, inArray array: [String: String]) -> String? {
19 | // short function that treats a [String: String] as a key value pair.
20 | return array[key]
21 | }
22 |
--------------------------------------------------------------------------------
/legacy/custom-outset/Makefile:
--------------------------------------------------------------------------------
1 | PKGTITLE="outset-custom"
2 | PKGVERSION="1.0.0"
3 | PKGID=com.github.outset.custom
4 | PROJECT="outset-custom"
5 |
6 | #################################################
7 |
8 | ##Help - Show this help menu
9 | help:
10 | @fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//'
11 |
12 | ## clean - Clean up temporary working directories
13 | clean:
14 | rm -f ./outset*.pkg
15 | rm -f ./pkgroot/usr/local/outset/*/*.pyc
16 |
17 | ## pkg - Create a package using pkgbuild
18 | pkg: clean
19 | pkgbuild --root pkgroot --identifier ${PKGID} --version ${PKGVERSION} --ownership recommended ./${PKGTITLE}-${PKGVERSION}.component.pkg
20 | productbuild --identifier ${PKGID}.${PKGVERSION} --package ./${PKGTITLE}-${PKGVERSION}.component.pkg ./${PKGTITLE}-${PKGVERSION}.pkg
21 | rm -f ./${PKGTITLE}-${PKGVERSION}.component.pkg
22 |
--------------------------------------------------------------------------------
/Package/Library/LaunchAgents/io.macadmins.Outset.login-window.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | OnDemand
6 |
7 | LaunchOnlyOnce
8 |
9 | Program
10 | /usr/local/outset/Outset.app/Contents/MacOS/Outset
11 | LimitLoadToSessionType
12 |
13 | LoginWindow
14 |
15 | AssociatedBundleIdentifiers
16 | io.macadmins.Outset
17 | Label
18 | io.macadmins.Outset.login-window
19 | ProgramArguments
20 |
21 | /usr/local/outset/Outset.app/Contents/MacOS/Outset
22 | --login-window
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Package/Library/LaunchDaemons/io.macadmins.Outset.cleanup.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Program
6 | /usr/local/outset/Outset.app/Contents/MacOS/Outset
7 | AssociatedBundleIdentifiers
8 | io.macadmins.Outset
9 | KeepAlive
10 |
11 | PathState
12 |
13 | /private/tmp/.io.macadmins.outset.cleanup.launchd
14 |
15 |
16 |
17 | Label
18 | io.macadmins.Outset.cleanup
19 | OnDemand
20 |
21 | ProgramArguments
22 |
23 | /usr/local/outset/Outset.app/Contents/MacOS/Outset
24 | --cleanup
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/Package/Library/LaunchAgents/io.macadmins.Outset.on-demand.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Program
6 | /usr/local/outset/Outset.app/Contents/MacOS/Outset
7 | AssociatedBundleIdentifiers
8 | io.macadmins.Outset
9 | KeepAlive
10 |
11 | PathState
12 |
13 | /private/tmp/.io.macadmins.outset.ondemand.launchd
14 |
15 |
16 |
17 | Label
18 | io.macadmins.Outset.on-demand
19 | OnDemand
20 |
21 | ProgramArguments
22 |
23 | /usr/local/outset/Outset.app/Contents/MacOS/Outset
24 | --on-demand
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/Package/Library/LaunchDaemons/io.macadmins.Outset.login-privileged.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Program
6 | /usr/local/outset/Outset.app/Contents/MacOS/Outset
7 | AssociatedBundleIdentifiers
8 | io.macadmins.Outset
9 | KeepAlive
10 |
11 | PathState
12 |
13 | /private/tmp/.io.macadmins.outset.login-privileged.launchd
14 |
15 |
16 |
17 | Label
18 | io.macadmins.Outset.login-privileged
19 | OnDemand
20 |
21 | ProgramArguments
22 |
23 | /usr/local/outset/Outset.app/Contents/MacOS/Outset
24 | --login-privileged
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/Package/Library/LaunchDaemons/io.macadmins.Outset.on-demand-privileged.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Program
6 | /usr/local/outset/Outset.app/Contents/MacOS/Outset
7 | AssociatedBundleIdentifiers
8 | io.macadmins.Outset
9 | KeepAlive
10 |
11 | PathState
12 |
13 | /private/tmp/.io.macadmins.outset.ondemand-privileged.launchd
14 |
15 |
16 |
17 | Label
18 | io.macadmins.Outset.on-demand-privileged
19 | OnDemand
20 |
21 | ProgramArguments
22 |
23 | /usr/local/outset/Outset.app/Contents/MacOS/Outset
24 | --on-demand-privileged
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/legacy/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | select = B,C,E,F,P,W,B9
3 | max-line-length = 80
4 | ### DEFAULT IGNORES FOR 4-space INDENTED PROJECTS ###
5 | # E127, E128 are hard to silence in certain nested formatting situations.
6 | # E203 doesn't work for slicing
7 | # E265, E266 talk about comment formatting which is too opinionated.
8 | # E402 warns on imports coming after statements. There are important use cases
9 | # that require statements before imports.
10 | # E501 is not flexible enough, we're using B950 instead.
11 | # E722 is a duplicate of B001.
12 | # P207 is a duplicate of B003.
13 | # P208 is a duplicate of C403.
14 | # W503 talks about operator formatting which is too opinionated.
15 | ignore = E127, E128, E203, E265, E266, E402, E501, E722, P207, P208, W503
16 | ### DEFAULT IGNORES FOR 2-space INDENTED PROJECTS (uncomment) ###
17 | # ignore = E111, E114, E121, E127, E128, E265, E266, E402, E501, P207, P208, W503
18 | exclude =
19 | .git,
20 | .hg,
21 | # This will be fine-tuned over time
22 | max-complexity = 65
23 |
--------------------------------------------------------------------------------
/Outset.xcodeproj/xcuserdata/rea094.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | Outset App Bundle.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 3
11 |
12 | Outset Installer Package.xcscheme_^#shared#^_
13 |
14 | orderHint
15 | 2
16 |
17 |
18 | SuppressBuildableAutocreation
19 |
20 | 4124EF8C293822F4003B00F4
21 |
22 | primary
23 |
24 |
25 | 4124EFAC29414A5D003B00F4
26 |
27 | primary
28 |
29 |
30 | 41A679EE2965A03D000BFFCE
31 |
32 | primary
33 |
34 |
35 | 41A67A05296BADFE000BFFCE
36 |
37 | primary
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/Outset/Utils/FileUtils/DMG.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DMG.swift
3 | // Outset
4 | //
5 | // Created by Bart E Reardon on 26/6/2024.
6 | //
7 |
8 | import Foundation
9 |
10 | func mountDmg(dmg: String) -> String {
11 | // Attaches dmg and returns the path
12 | let cmd = "/usr/bin/hdiutil attach -nobrowse -noverify -noautoopen \(dmg)"
13 | writeLog("Attaching \(dmg)", logLevel: .debug)
14 | let (output, error, status) = runShellCommand(cmd)
15 | if status != 0 {
16 | writeLog("Failed attaching \(dmg) with error \(error)", logLevel: .error)
17 | return error
18 | }
19 | return output.trimmingCharacters(in: .whitespacesAndNewlines)
20 | }
21 |
22 | func detachDmg(dmgMount: String) -> String {
23 | // Detaches dmg
24 | writeLog("Detaching \(dmgMount)", logLevel: .debug)
25 | let cmd = "/usr/bin/hdiutil detach -force \(dmgMount)"
26 | let (output, error, status) = runShellCommand(cmd)
27 | if status != 0 {
28 | writeLog("Failed detaching \(dmgMount) with error \(error)", logLevel: .error)
29 | return error
30 | }
31 | return output.trimmingCharacters(in: .whitespacesAndNewlines)
32 | }
33 |
--------------------------------------------------------------------------------
/legacy/Makefile:
--------------------------------------------------------------------------------
1 | PKGTITLE="outset"
2 | PKGVERSION="3.0.3"
3 | PKGID=com.github.outset
4 | PROJECT="outset"
5 |
6 | #################################################
7 |
8 | ##Help - Show this help menu
9 | help:
10 | @fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//'
11 |
12 | ## clean - Clean up temporary working directories
13 | clean:
14 | rm -f ./outset*.{dmg,pkg}
15 |
16 | ## pkg - Create a package using pkgbuild
17 | pkg: clean
18 | pkgbuild --root pkgroot --scripts scripts --identifier ${PKGID} --version ${PKGVERSION} --ownership recommended ./${PKGTITLE}-${PKGVERSION}.component.pkg
19 | productbuild --identifier ${PKGID}.${PKGVERSION} --product Requirements.plist --package ./${PKGTITLE}-${PKGVERSION}.component.pkg ./${PKGTITLE}-${PKGVERSION}.pkg
20 | rm -f ./${PKGTITLE}-${PKGVERSION}.component.pkg
21 |
22 | ## dmg - Wrap the package inside a dmg
23 | dmg: pkg
24 | rm -f ./${PROJECT}*.dmg
25 | rm -rf /tmp/${PROJECT}-build
26 | mkdir -p /tmp/${PROJECT}-build/
27 | cp ./README.md /tmp/${PROJECT}-build
28 | cp -R ./${PKGTITLE}-${PKGVERSION}.pkg /tmp/${PROJECT}-build
29 | hdiutil create -srcfolder /tmp/${PROJECT}-build -volname "${PROJECT}" -format UDZO -o ${PROJECT}-${PKGVERSION}.dmg
30 | rm -rf /tmp/${PROJECT}-build
31 |
--------------------------------------------------------------------------------
/MDM/Jamf Pro/service-status-ea.sh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | currentUser=$( echo "show State:/Users/ConsoleUser" | scutil | awk '/Name :/ { print $3 }' )
4 | ROOT_PATH="/usr/local/outset"
5 | BUNDLE="Outset.app"
6 | AGENTS="Contents/Library/LaunchAgents"
7 | DAEMONS="Contents/Library/LaunchDaemons"
8 |
9 | # If the bundle or agents etc are missing then something is wrong
10 | if [[ ! -d "${ROOT_PATH}/${BUNDLE}/${AGENTS}" ]] ||
11 | [[ ! -d "${ROOT_PATH}/${BUNDLE}/${DAEMONS}" ]] ||
12 | [[ ! -d "${ROOT_PATH}" ]]; then
13 | echo "Missing"
14 | exit 0
15 | fi
16 |
17 | # Get a count of what we expect from the agents and daemons listed in the app bundle
18 | EXPECTED_AGENTS=$(ls ${ROOT_PATH}/${BUNDLE}/${AGENTS} | wc -l)
19 | EXPECTED_DAEMONS=$(ls ${ROOT_PATH}/${BUNDLE}/${DAEMONS} | wc -l)
20 |
21 | # Indicate if all the Outset daemons are enabled or not.
22 | outsetStatusRaw=$(${ROOT_PATH}/outset --service-status)
23 | enabledDaemons=$(echo $outsetStatusRaw | grep -c 'Enabled$')
24 |
25 | # We expect all services to be present and loaded
26 | expectedServices=$(( EXPECTED_AGENTS + EXPECTED_DAEMONS ))
27 |
28 | # If there is no user logged in, none of the agents will be active, only the daemons.
29 | if [ -z "${currentUser}" -o "${currentUser}" = "loginwindow" ]; then
30 | expectedServices=${EXPECTED_DAEMONS}
31 | fi
32 |
33 | healthyStatus="Not Healthy"
34 |
35 | if [ ${enabledDaemons} -eq ${expectedServices} ]; then
36 | healthyStatus="Healthy"
37 | fi
38 |
39 | echo "$healthyStatus"
--------------------------------------------------------------------------------
/Outset/Utils/FileUtils/Packages.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Packages.swift
3 | // Outset
4 | //
5 | // Created by Bart E Reardon on 26/6/2024.
6 | //
7 |
8 | import Foundation
9 |
10 | func installPackage(pkg: String) -> Bool {
11 | // Installs pkg onto boot drive
12 | if isRoot {
13 | var pkgToInstall: String = ""
14 | var dmgMount: String = ""
15 |
16 | if pkg.lowercased().hasSuffix("dmg") {
17 | dmgMount = mountDmg(dmg: pkg)
18 | for files in folderContents(path: dmgMount) where ["pkg", "mpkg"].contains(files.lowercased().suffix(3)) {
19 | pkgToInstall = dmgMount
20 | }
21 | } else if ["pkg", "mpkg"].contains(pkg.lowercased().suffix(3)) {
22 | pkgToInstall = pkg
23 | }
24 | writeLog("Installing \(pkgToInstall)")
25 | let cmd = "/usr/sbin/installer -pkg \(pkgToInstall) -target /"
26 | let (output, error, status) = runShellCommand(cmd, verbose: true)
27 | if status != 0 {
28 | writeLog(error, logLevel: .error)
29 | } else {
30 | writeLog(output)
31 | }
32 |
33 | DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) {
34 | if !dmgMount.isEmpty {
35 | writeLog(detachDmg(dmgMount: dmgMount))
36 | }
37 | }
38 | return true
39 | } else {
40 | writeLog("Unable to process \(pkg)", logLevel: .error)
41 | writeLog("Must be root to install packages", logLevel: .error)
42 | }
43 | return false
44 | }
45 |
--------------------------------------------------------------------------------
/Outset/Utils/FileUtils/Shell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ShellUtils.swift
3 | // Outset
4 | //
5 | // Created by Bart E Reardon on 5/9/2023.
6 | //
7 |
8 | import Foundation
9 |
10 | func runShellCommand(_ command: String, args: [String] = [], verbose: Bool = false) -> (output: String, error: String, exitCode: Int32) {
11 | // runs a shell command passed as an argument
12 | // If the verbose parameter is set to true, will log the command being run and its status when completed.
13 | // returns the output, error and exit code as a tuple.
14 |
15 | if verbose {
16 | writeLog("Running task \(command)", logLevel: .debug)
17 | }
18 | let task = Process()
19 | let pipe = Pipe()
20 | let errorpipe = Pipe()
21 |
22 | var cmd = command
23 | for arg in args {
24 | cmd += " '\(arg)'"
25 | }
26 | let arguments = ["-c", cmd]
27 |
28 | var output: String = ""
29 | var error: String = ""
30 |
31 | task.launchPath = "/bin/sh"
32 | task.arguments = arguments
33 | task.standardOutput = pipe
34 | task.standardError = errorpipe
35 | task.launch()
36 |
37 | let data = pipe.fileHandleForReading.readDataToEndOfFile()
38 | let errordata = errorpipe.fileHandleForReading.readDataToEndOfFile()
39 |
40 | output.append(String(data: data, encoding: .utf8)!)
41 | error.append(String(data: errordata, encoding: .utf8)!)
42 |
43 | task.waitUntilExit()
44 | let status = task.terminationStatus
45 | if verbose {
46 | writeLog("Completed task \(command) with status \(status)", logLevel: .debug)
47 | writeLog("Task output: \n\(output)", logLevel: .debug)
48 | }
49 | return (output, error, status)
50 | }
51 |
--------------------------------------------------------------------------------
/Outset/Utils/SystemUtils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Functions.swift
3 | // outset
4 | //
5 | // Created by Bart Reardon on 1/12/2022.
6 | //
7 |
8 | import Foundation
9 | import SystemConfiguration
10 | import IOKit
11 | import CoreFoundation
12 |
13 | enum Action {
14 | case enable
15 | case disable
16 | }
17 |
18 | func ensureRoot(_ reason: String) {
19 | if !isRoot {
20 | writeLog("Must be root to \(reason)", logLevel: .error)
21 | exit(1)
22 | }
23 | }
24 |
25 | var isRoot: Bool {
26 | return NSUserName() == "root"
27 | }
28 |
29 | func getConsoleUserInfo() -> (username: String, userID: String) {
30 | // We need the console user, not the process owner so NSUserName() won't work for our needs when outset runs as root
31 | var uid: uid_t = 0
32 | if let consoleUser = SCDynamicStoreCopyConsoleUser(nil, &uid, nil) as? String {
33 | return (consoleUser, "\(uid)")
34 | } else {
35 | return ("", "")
36 | }
37 | }
38 |
39 | func loginWindowUpdateState(_ action: Action) {
40 | var cmd: String
41 | let loginWindowPlist: String = "/System/Library/LaunchDaemons/com.apple.loginwindow.plist"
42 | switch action {
43 | case .enable:
44 | writeLog("Enabling loginwindow process", logLevel: .debug)
45 | cmd = "/bin/launchctl load \(loginWindowPlist)"
46 | case .disable:
47 | writeLog("Disabling loginwindow process", logLevel: .debug)
48 | cmd = "/bin/launchctl unload \(loginWindowPlist)"
49 | }
50 | _ = runShellCommand(cmd)
51 | }
52 |
53 | @discardableResult
54 | func runIf(_ condition: Bool, _ action: () -> Void) -> Bool {
55 | if condition { action() }
56 | return condition
57 | }
58 |
--------------------------------------------------------------------------------
/Outset/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Outset.png_16x16.png",
5 | "idiom" : "mac",
6 | "scale" : "1x",
7 | "size" : "16x16"
8 | },
9 | {
10 | "filename" : "Outset.png_16x16@2x.png",
11 | "idiom" : "mac",
12 | "scale" : "2x",
13 | "size" : "16x16"
14 | },
15 | {
16 | "filename" : "Outset.png_32x32.png",
17 | "idiom" : "mac",
18 | "scale" : "1x",
19 | "size" : "32x32"
20 | },
21 | {
22 | "filename" : "Outset.png_32x32@2x.png",
23 | "idiom" : "mac",
24 | "scale" : "2x",
25 | "size" : "32x32"
26 | },
27 | {
28 | "filename" : "Outset.png_128x128.png",
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "filename" : "Outset.png_128x128@2x.png",
35 | "idiom" : "mac",
36 | "scale" : "2x",
37 | "size" : "128x128"
38 | },
39 | {
40 | "filename" : "Outset.png_256x256.png",
41 | "idiom" : "mac",
42 | "scale" : "1x",
43 | "size" : "256x256"
44 | },
45 | {
46 | "filename" : "Outset.png_256x256@2x.png",
47 | "idiom" : "mac",
48 | "scale" : "2x",
49 | "size" : "256x256"
50 | },
51 | {
52 | "filename" : "Outset.png_512x512.png",
53 | "idiom" : "mac",
54 | "scale" : "1x",
55 | "size" : "512x512"
56 | },
57 | {
58 | "filename" : "Outset.png_512x512@2x.png",
59 | "idiom" : "mac",
60 | "scale" : "2x",
61 | "size" : "512x512"
62 | }
63 | ],
64 | "info" : {
65 | "author" : "xcode",
66 | "version" : 1
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Package/Scripts/preinstall:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | # preinstall.sh
4 | # Outset
5 | #
6 | # Created by Bart Reardon on 25/3/2023.
7 | #
8 |
9 | ## Process legacy launchd items if upgrading from outset 3.0.3 or earlier
10 |
11 | LD_ROOT="/Library/LaunchDaemons"
12 | LA_ROOT="/Library/LaunchAgents"
13 | OUTSET_ROOT="/usr/local/outset"
14 | OUTSET_BACKUP="${OUTSET_ROOT}/backup"
15 |
16 | USER_ID=$(id -u "$(/usr/bin/stat -f %Su /dev/console)")
17 |
18 | ## LaunchDaemons
19 | DAEMONS=(
20 | "${LD_ROOT}/com.github.outset.boot.plist"
21 | "${LD_ROOT}/com.github.outset.cleanup.plist"
22 | "${LD_ROOT}/com.github.outset.login-privileged.plist"
23 | )
24 |
25 | ## LaunchAgents
26 | AGENTS=(
27 | "${LA_ROOT}/com.github.outset.login.plist"
28 | "${LA_ROOT}/com.github.outset.on-demand.plist"
29 | )
30 |
31 | # Unload if present
32 | for daemon in $DAEMONS; do
33 | if [ -e "${daemon}" ]; then
34 | /bin/launchctl bootout system "${daemon}"
35 | rm -fv "${daemon}"
36 | fi
37 | done
38 |
39 | for agent in $AGENTS; do
40 | if [ -e "${agent}" ]; then
41 | if [ ${USER_ID} -ne 0 ]; then
42 | /bin/launchctl bootout gui/${USER_ID} "${agent}"
43 | fi
44 | rm -fv "${agent}"
45 | fi
46 | done
47 |
48 | # backup existing preference files
49 | mkdir -p "${OUTSET_BACKUP}"
50 |
51 | if [ -d "${OUTSET_ROOT}/share" ]; then
52 | cp ${OUTSET_ROOT}/share/* "${OUTSET_BACKUP}/"
53 | fi
54 |
55 | for user in /Users/*; do
56 | if [ -e "/Users/${user}/Library/Preferences/com.github.outset.once.plist" ]; then
57 | mkdir -p "${OUTSET_ROOT}/backup/${user}"
58 | cp "/Users/${user}/Library/Preferences/com.github.outset.once.plist" "${OUTSET_BACKUP}/${user}/"
59 | fi
60 | done
61 |
--------------------------------------------------------------------------------
/Package/Scripts/postinstall:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | # Postinstall script for outset CLI
4 | #
5 | # Created by Bart Reardon on 9/1/2023.
6 | #
7 |
8 | # Get the macOS version number
9 | macos_version="$(sw_vers -productVersion)"
10 | LD_ROOT="/Library/LaunchDaemons"
11 | LA_ROOT="/Library/LaunchAgents"
12 |
13 | APP_PATH="/usr/local/outset/Outset.app"
14 | APP_ROOT="${APP_PATH}/Contents"
15 |
16 | # register the app bundle
17 | /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister "${APP_PATH}"
18 |
19 | # run boot argument - creates necessary paths and folders
20 | "${APP_PATH}/Contents/MacOS/Outset" --boot
21 |
22 | ### Commented out for now until issues with SMAppService are sorted
23 | # Check if the macOS version is 13 or newer. If so, we don't need to load the launchd plists manually.
24 | #if [[ $(echo "${macos_version}" | cut -d'.' -f1) -ge 13 ]]; then
25 | # # register the agents
26 | # "${APP_PATH}/Contents/MacOS/Outset" --enable-services
27 | #
28 | # # issue with ServiceManagement in that login-window agents don't get loaded so we'll copy that one over manually
29 | # cp "${APP_ROOT}/${LA_ROOT}/io.macadmins.outset.login-window.plist" "${LA_ROOT}"
30 | # exit 0
31 | #fi
32 | ###
33 |
34 | ## LaunchDaemons
35 | DAEMONS=(
36 | "${LD_ROOT}/io.macadmins.outset.boot.plist"
37 | "${LD_ROOT}/io.macadmins.outset.cleanup.plist"
38 | "${LD_ROOT}/io.macadmins.outset.login-privileged.plist"
39 | "${LD_ROOT}/io.macadmins.outset.on-demand-privileged.plist"
40 | )
41 |
42 | ## LaunchAgents
43 | AGENTS=(
44 | "${LA_ROOT}/io.macadmins.outset.login.plist"
45 | "${LA_ROOT}/io.macadmins.outset.on-demand.plist"
46 | "${LA_ROOT}/io.macadmins.outset.login-window.plist"
47 | )
48 |
49 | for daemon in ${DAEMONS}; do
50 | cp "${APP_ROOT}/${daemon}" "${LD_ROOT}"
51 | if [ -e "${daemon}" ]; then
52 | /bin/launchctl bootstrap system "${daemon}"
53 | fi
54 | done
55 |
56 | for agent in ${AGENTS}; do
57 | cp "${APP_ROOT}/${agent}" "${LA_ROOT}"
58 | echo ${agent}
59 | done
60 |
--------------------------------------------------------------------------------
/Outset/Utils/Network.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Network.swift
3 | // Outset
4 | //
5 | // Created by Bart E Reardon on 5/9/2023.
6 | //
7 |
8 | import Foundation
9 | import SystemConfiguration
10 |
11 | func isNetworkUp() -> Bool {
12 | // https://stackoverflow.com/a/39782859/17584669
13 | // perform a check to see if the network is available.
14 |
15 | var zeroAddress = sockaddr_in(sin_len: 0, sin_family: 0, sin_port: 0, sin_addr: in_addr(s_addr: 0), sin_zero: (0, 0, 0, 0, 0, 0, 0, 0))
16 | zeroAddress.sin_len = UInt8(MemoryLayout.size(ofValue: zeroAddress))
17 | zeroAddress.sin_family = sa_family_t(AF_INET)
18 |
19 | let defaultRouteReachability = withUnsafePointer(to: &zeroAddress) {
20 | $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {zeroSockAddress in
21 | SCNetworkReachabilityCreateWithAddress(nil, zeroSockAddress)
22 | }
23 | }
24 |
25 | var flags: SCNetworkReachabilityFlags = SCNetworkReachabilityFlags(rawValue: 0)
26 | if SCNetworkReachabilityGetFlags(defaultRouteReachability!, &flags) == false {
27 | return false
28 | }
29 |
30 | let isReachable = (flags.rawValue & UInt32(kSCNetworkFlagsReachable)) != 0
31 | let needsConnection = (flags.rawValue & UInt32(kSCNetworkFlagsConnectionRequired)) != 0
32 | let ret = (isReachable && !needsConnection)
33 |
34 | return ret
35 | }
36 |
37 | func waitForNetworkUp(timeout: Double) -> Bool {
38 | // used during --boot if "wait_for_network" prefrence is true
39 | var networkUp = false
40 | let deadline = DispatchTime.now() + timeout
41 | while !networkUp && DispatchTime.now() < deadline {
42 | writeLog("Waiting for network: \(timeout) seconds", logLevel: .debug)
43 | networkUp = isNetworkUp()
44 | if !networkUp {
45 | writeLog("Waiting...", logLevel: .debug)
46 | Thread.sleep(forTimeInterval: 1)
47 | }
48 | }
49 | if !networkUp && DispatchTime.now() > deadline {
50 | writeLog("No network connectivity detected after \(timeout) seconds", logLevel: .error)
51 | }
52 | return networkUp
53 | }
54 |
--------------------------------------------------------------------------------
/MDM/Jamf Pro/outset-preferences-schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "__preferencedomain": "io.macadmins.Outset",
3 | "options": {
4 | "remove_empty_properties": true
5 | },
6 | "title": "Outset (io.macadmins.Outset)",
7 | "description": "Outset automatically processes packages and scripts during the boot sequence, user logins, or on demand.",
8 | "properties": {
9 | "ignored_users": {
10 | "title": "Ignored Users",
11 | "description": "Exclude certain users from scripts that run in the login context (I.e. a local admin account or a static kiosk user).",
12 | "type": "array",
13 | "items": {
14 | "type": "string"
15 | }
16 | },
17 | "network_timeout": {
18 | "title": "Network Timeout",
19 | "description": "By default, during boot-once runs, Outset will wait for an active network connection before continuing. If that directory is populated, it would check every ten seconds for a valid network connection, timing out after a total of 180 seconds (three minutes) and skipping those items if no connection is successfully detected.",
20 | "type": "integer",
21 | "default": "180",
22 | "options": {
23 | "inputAttributes": {
24 | "placeholder": "180"
25 | }
26 | }
27 | },
28 | "wait_for_network": {
29 | "title": "Wait for Network",
30 | "description": "If you don't want Outset to wait for a valid network connection at all (and you don't want it to suppress the loginwindow process while running scripts) you can deliver a readable preference file with the wait_for_network value set to boolean false.",
31 | "type": "boolean",
32 | "default": true
33 | },
34 | "sha256sum": {
35 | "title": "Checksums",
36 | "description": "Enforce script integrity and ensure that only script that you want to run gets run. When used, every file to be processed must have a matching hash value. Absence of a hash value or value mismatch will prevent Outset from processing that file.",
37 | "type": "object",
38 | "additionalProperties": {
39 | "type": "string"
40 | }
41 | },
42 | "verbose_logging": {
43 | "title": "Verbose Logging",
44 | "description": "Enable verbose logging",
45 | "type": "boolean",
46 | "default": false
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/Package/generatePackage.zsh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | set -x
4 |
5 | cd "${BUILT_PRODUCTS_DIR}"
6 |
7 | STAGING_DIRECTORY="${TMPDIR}/staging"
8 | INSTALL_LOCATION="/usr/local/outset/"
9 | INSTALL_SCRIPTS=${SCRIPT_INPUT_FILE_1}
10 | OUTSET_ALIAS=${SCRIPT_INPUT_FILE_2}
11 | APP_NAME=${PROJECT_NAME}
12 | IDENTIFIER="${PRODUCT_BUNDLE_IDENTIFIER}"
13 | VERSION="${MARKETING_VERSION}"
14 |
15 | # Clean staging dir
16 | rm -r "${STAGING_DIRECTORY}"
17 |
18 | # Set up a staging directory with the contents to install.
19 | mkdir -p "${STAGING_DIRECTORY}/${INSTALL_LOCATION}"
20 | chmod 755 "${OUTSET_ALIAS}"
21 | cp "${OUTSET_ALIAS}" "${STAGING_DIRECTORY}/${INSTALL_LOCATION}"
22 | cp -r "Outset.app" "${STAGING_DIRECTORY}/${INSTALL_LOCATION}"
23 |
24 | # Generate the component property list.
25 | pkgbuild --analyze --root "${STAGING_DIRECTORY}" component.plist
26 |
27 | # Force the installation package (.pkg) to not be relocatable.
28 | # This ensures the package components install in `INSTALL_LOCATION`.
29 | plutil -replace BundleIsRelocatable -bool no component.plist
30 |
31 | # Build a temporary package using the component property list.
32 | pkgbuild --root "${STAGING_DIRECTORY}" --component-plist component.plist --identifier "${IDENTIFIER}" --version "${VERSION}" --scripts "${INSTALL_SCRIPTS}" tmp-package.pkg
33 |
34 | # Synthesize the distribution for the temporary package.
35 | productbuild --synthesize --package tmp-package.pkg --identifier "${IDENTIFIER}" --version "${VERSION}" Distribution
36 |
37 | # Synthesize the final package from the distribution.
38 | productbuild --distribution Distribution --package-path "${BUILT_PRODUCTS_DIR}" "${SCRIPT_OUTPUT_FILE_0}"
39 |
40 | # Get the developer installer identity of possible for signing
41 | # IDENTITY=$(security find-certificate -p -c "Developer ID Installer" 2>/dev/null) && IDENTITY=$(echo "${IDENTITY}" | openssl x509 -noout -subject | sed -n 's/.*CN=\([^/]*\).*/\1/p')
42 | #
43 | # if [[ -z ${IDENTITY} ]]; then
44 | # productbuild --distribution Distribution --package-path "${BUILT_PRODUCTS_DIR}" "${SCRIPT_OUTPUT_FILE_0}"
45 | # else
46 | # productbuild --distribution Distribution --package-path "${BUILT_PRODUCTS_DIR}" --sign "${IDENTITY}" "${SCRIPT_OUTPUT_FILE_0}"
47 | # fi
48 |
49 |
--------------------------------------------------------------------------------
/legacy/scripts/postinstall:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -x
3 |
4 | # Only proceed if we are installing on the booted volume
5 | [[ $3 != "/" ]] && exit 0
6 |
7 | # Let's play python roulette, and choose from some popular options, in this order:
8 | # 1. python.org https://www.python.org/downloads/
9 | # 2. MacAdmins https://github.com/macadmins/python
10 | # 3. Munki https://github.com/munki/munki
11 | # If none of these are on disk, then fall back to Apple's system python,
12 | # which can be installed via the Command Line Tools.
13 |
14 | # "What about python 2, which Apple still ships," you ask?
15 | # Outset does not support python 2, which was sunsetted on Jan 1, 2020.
16 | # See https://www.python.org/doc/sunset-python-2/.
17 | # If you choose to continue to use python 2, you'll want to create the symlink via other means,
18 | # with something like: /bin/ln -s /usr/bin/python /usr/local/outset/python3
19 |
20 | OUTSET_PYTHON=/usr/local/outset/python3
21 | ORG_PYTHON=/usr/local/bin/python3
22 | MACADMINS_PYTHON=/usr/local/bin/managed_python3
23 | MUNKI_MUNKI_PYTHON=/usr/local/munki/munki-python
24 | MUNKI_PYTHON=/usr/local/munki/python
25 | SYSTEM_PYTHON=/usr/bin/python3
26 |
27 | # Delete existing symlink
28 | [[ -L "${OUTSET_PYTHON}" ]] && /bin/rm "${OUTSET_PYTHON}"
29 |
30 | if [[ -L "${ORG_PYTHON}" ]]; then
31 | /bin/ln -s "${ORG_PYTHON}" "${OUTSET_PYTHON}"
32 | elif [[ -L "${MACADMINS_PYTHON}" ]]; then
33 | /bin/ln -s "${MACADMINS_PYTHON}" "${OUTSET_PYTHON}"
34 | elif [[ -L "${MUNKI_MUNKI_PYTHON}" ]]; then
35 | /bin/ln -s "${MUNKI_MUNKI_PYTHON}" "${OUTSET_PYTHON}"
36 | elif [[ -L "${MUNKI_PYTHON}" ]]; then
37 | /bin/ln -s "${MUNKI_PYTHON}" "${OUTSET_PYTHON}"
38 | else
39 | /bin/ln -s "${SYSTEM_PYTHON}" "${OUTSET_PYTHON}"
40 | fi
41 |
42 | # Load the LaunchDaemons
43 | /bin/launchctl load /Library/LaunchDaemons/com.github.outset.boot.plist
44 | /bin/launchctl load /Library/LaunchDaemons/com.github.outset.cleanup.plist
45 | /bin/launchctl load /Library/LaunchDaemons/com.github.outset.login-privileged.plist
46 |
47 | # Load the LaunchAgents
48 |
49 | user=$(/bin/echo "show State:/Users/ConsoleUser" | /usr/sbin/scutil | /usr/bin/awk '/Name :/&&!/loginwindow/{print $3}')
50 | console_user_uid=$(stat -f%u /dev/console)
51 | [[ -z "$user" ]] && exit 0
52 | /bin/launchctl asuser "$console_user_uid" /bin/launchctl load /Library/LaunchAgents/com.github.outset.login.plist
53 | /bin/launchctl asuser "$console_user_uid" /bin/launchctl load /Library/LaunchAgents/com.github.outset.on-demand.plist
54 |
55 | exit 0
56 |
--------------------------------------------------------------------------------
/.github/workflows/build_outset_release_manual.yml:
--------------------------------------------------------------------------------
1 | name: Build Outset Release (Manual)
2 |
3 | env:
4 | NOTARY_APP_PASSWORD: ${{ secrets.NOTARY_APP_PASSWORD_MAOS }}
5 |
6 | on: [workflow_dispatch]
7 |
8 | jobs:
9 | build:
10 | runs-on: macos-14
11 |
12 | steps:
13 | - name: Checkout outset repo
14 | uses: actions/checkout@v4
15 | with:
16 | fetch-depth: 0
17 |
18 | - name: Install Apple Xcode certificates
19 | uses: apple-actions/import-codesign-certs@v3
20 | with:
21 | keychain-password: ${{ github.run_id }}
22 | p12-file-base64: ${{ secrets.APP_CERTIFICATES_P12_MAOS }}
23 | p12-password: ${{ secrets.APP_CERTIFICATES_P12_PASSWORD_MAOS }}
24 |
25 | - name: Install Apple Installer certificates
26 | uses: apple-actions/import-codesign-certs@v3
27 | with:
28 | create-keychain: false # do not create a new keychain for this value
29 | keychain-password: ${{ github.run_id }}
30 | p12-file-base64: ${{ secrets.PKG_CERTIFICATES_P12_MAOS }}
31 | p12-password: ${{ secrets.PKG_CERTIFICATES_P12_PASSWORD_MAOS }}
32 |
33 | - name: Run build package script
34 | run: ./build_outset.zsh "CREATE_PKG" "$NOTARY_APP_PASSWORD"
35 |
36 | - name: get environment variables
37 | id: get_env_var
38 | run: |
39 | echo "OUTSET_VERSION=$(/bin/cat ./build_info.txt)" >> $GITHUB_ENV
40 | echo "OUTSET_MAIN_VERSION=$(/bin/cat ./build_info_main.txt)" >> $GITHUB_ENV
41 |
42 | - name: Generate changelog
43 | id: changelog
44 | uses: metcalfc/changelog-generator@v4
45 | with:
46 | myToken: ${{ secrets.GITHUB_TOKEN }}
47 | reverse: 'true'
48 |
49 | - name: Create Release
50 | id: create_release
51 | uses: softprops/action-gh-release@v2
52 | with:
53 | name: Outset ${{env.OUTSET_VERSION}}
54 | tag_name: v${{env.OUTSET_VERSION}}
55 | draft: true
56 | prerelease: false
57 | token: ${{ secrets.GITHUB_TOKEN }}
58 | body: |
59 | # Notes
60 | This is a version of Outset created by GitHub Actions.
61 | Outset.app has been signed and notarized. The package has been signed, notarized and stapled.
62 |
63 | # Changelog
64 | ${{ steps.changelog_reader.outputs.changes }}
65 |
66 | # Changes
67 | ${{ steps.changelog.outputs.changelog }}
68 | files: ${{github.workspace}}/outputs/*.pkg
69 |
70 | - name: Upload packages
71 | uses: actions/upload-artifact@v4
72 | with:
73 | name: packages
74 | path: outputs/
75 |
--------------------------------------------------------------------------------
/.github/workflows/build_outset_prerelease_manual.yml:
--------------------------------------------------------------------------------
1 | name: Build Outset pre-release (Manual)
2 |
3 | env:
4 | NOTARY_APP_PASSWORD: ${{ secrets.NOTARY_APP_PASSWORD_MAOS }}
5 |
6 | on: [workflow_dispatch]
7 |
8 | jobs:
9 | build:
10 | runs-on: macos-14
11 |
12 | steps:
13 | - name: Checkout outset repo
14 | uses: actions/checkout@v4
15 | with:
16 | fetch-depth: 0
17 |
18 | - name: Install Apple Xcode certificates
19 | uses: apple-actions/import-codesign-certs@v3
20 | with:
21 | keychain-password: ${{ github.run_id }}
22 | p12-file-base64: ${{ secrets.APP_CERTIFICATES_P12_MAOS }}
23 | p12-password: ${{ secrets.APP_CERTIFICATES_P12_PASSWORD_MAOS }}
24 |
25 | - name: Install Apple Installer certificates
26 | uses: apple-actions/import-codesign-certs@v3
27 | with:
28 | create-keychain: false # do not create a new keychain for this value
29 | keychain-password: ${{ github.run_id }}
30 | p12-file-base64: ${{ secrets.PKG_CERTIFICATES_P12_MAOS }}
31 | p12-password: ${{ secrets.PKG_CERTIFICATES_P12_PASSWORD_MAOS }}
32 |
33 | - name: Run build package script
34 | run: ./build_outset.zsh "CREATE_PKG" "$NOTARY_APP_PASSWORD"
35 |
36 | - name: get environment variables
37 | id: get_env_var
38 | run: |
39 | echo "OUTSET_VERSION=$(/bin/cat ./build_info.txt)" >> $GITHUB_ENV
40 | echo "OUTSET_MAIN_VERSION=$(/bin/cat ./build_info_main.txt)" >> $GITHUB_ENV
41 |
42 | - name: Generate changelog
43 | id: changelog
44 | uses: metcalfc/changelog-generator@v4
45 | with:
46 | myToken: ${{ secrets.GITHUB_TOKEN }}
47 | reverse: 'true'
48 |
49 | - name: Create Release
50 | id: create_release
51 | uses: softprops/action-gh-release@v2
52 | with:
53 | name: Outset ${{env.OUTSET_VERSION}}
54 | tag_name: v${{env.OUTSET_VERSION}}
55 | draft: true
56 | prerelease: true
57 | token: ${{ secrets.GITHUB_TOKEN }}
58 | body: |
59 | # Notes
60 | This is a version of Outset created by GitHub Actions.
61 | Outset.app has been signed and notarized. The package has been signed, notarized and stapled.
62 |
63 | # Changelog
64 | ${{ steps.changelog_reader.outputs.changes }}
65 |
66 | # Changes
67 | ${{ steps.changelog.outputs.changelog }}
68 | files: ${{github.workspace}}/outputs/*.pkg
69 |
70 | - name: Upload packages
71 | uses: actions/upload-artifact@v4
72 | with:
73 | name: packages
74 | path: outputs/
75 |
--------------------------------------------------------------------------------
/Outset.xcodeproj/xcshareddata/xcschemes/Outset Installer Package.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
44 |
50 |
51 |
57 |
58 |
59 |
60 |
62 |
63 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/Outset/RunModes/Boot.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Boot.swift
3 | // Outset
4 | //
5 | // Created by Bart Reardon on 6/8/2025.
6 | //
7 |
8 | import Foundation
9 |
10 | /// Processes all scheduled tasks that run at system boot.
11 | ///
12 | /// This function handles the complete boot-time workflow:
13 | /// - Rotates the log files to maintain the maximum number of boot logs.
14 | /// - Ensures required working folders exist.
15 | /// - Writes the current `OutsetPreferences` to disk.
16 | /// - Optionally waits for network connectivity before running scripts.
17 | /// - Runs `.bootOnce` and `.bootEvery` payload scripts as configured.
18 | /// - Restores login window state if it was modified during execution.
19 | ///
20 | /// The function will skip running boot scripts if:
21 | /// - Network connectivity is required (`prefs.waitForNetwork == true`) but
22 | /// cannot be established within `prefs.networkTimeout` seconds.
23 | ///
24 | /// - Parameter prefs: The `OutsetPreferences` object containing runtime
25 | /// configuration, including network wait settings and timeouts.
26 | ///
27 | /// - Note: This function writes informational and error logs throughout the
28 | /// process, and may update the login window state temporarily during execution.
29 | ///
30 | /// - SeeAlso: `processItems(_:)`, `scriptPayloads.processPayloadScripts(ofType:)`
31 | func processBootTasks(prefs: OutsetPreferences) {
32 | // perform log file rotation
33 | performLogRotation(logFolderPath: logDirectory, logFileBaseName: logFileName, maxLogFiles: logFileMaxCount)
34 |
35 | writeLog("Processing scheduled runs for boot", logLevel: .info)
36 | ensureWorkingFolders()
37 |
38 | writeOutsetPreferences(prefs: prefs)
39 |
40 | let bootOnceDir = PayloadType.bootOnce
41 | let bootEveryDir = PayloadType.bootEvery
42 |
43 | if prefs.waitForNetwork {
44 | loginwindowState = false
45 | loginWindowUpdateState(.disable)
46 | continueFirstBoot = waitForNetworkUp(timeout: floor(Double(prefs.networkTimeout) / 10))
47 | }
48 |
49 | if continueFirstBoot {
50 | let ranBootOnce = scriptPayloads.processPayloadScripts(ofType: .bootOnce)
51 | let ranBootEvery = scriptPayloads.processPayloadScripts(ofType: .bootEvery)
52 |
53 | if !(ranBootOnce || bootOnceDir.isEmpty) {
54 | processItems(.bootOnce, deleteItems: true)
55 | }
56 |
57 | if !(ranBootEvery || bootEveryDir.isEmpty) {
58 | processItems(.bootEvery)
59 | }
60 |
61 | if !loginwindowState {
62 | loginWindowUpdateState(.enable)
63 | }
64 |
65 | } else {
66 | writeLog("Unable to connect to network. Skipping boot scripts...", logLevel: .error)
67 | }
68 |
69 | writeLog("Boot processing complete")
70 | }
71 |
--------------------------------------------------------------------------------
/Outset/RunModes/OnDemand.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OnDemand.swift
3 | // Outset
4 | //
5 | // Created by Bart Reardon on 6/8/2025.
6 | //
7 |
8 | import Foundation
9 |
10 | /// Processes all `.onDemand` tasks for the current console user session.
11 | ///
12 | /// This function runs user-triggered tasks that have been placed in the
13 | /// `.onDemand` payload directory.
14 | /// - Skips execution if the `.onDemand` folder is empty.
15 | /// - Skips execution if there is no current user session (i.e., `consoleUser`
16 | /// is `"root"` or `"loginwindow"`).
17 | /// - Skips execution if the current process user is not the active console
18 | /// user.
19 | ///
20 | /// If conditions are met:
21 | /// - Executes `.onDemand` payload items via `processItems(_:)`.
22 | /// - Creates a `.cleanup` trigger after completion.
23 | ///
24 | /// - Note: All decisions and actions are logged at `.info` level.
25 | /// - SeeAlso: `processOnDemandPrivilegedTasks()`
26 | func processOnDemandTasks() {
27 | writeLog("Processing on-demand", logLevel: .info)
28 | if !folderContents(type: .onDemand).isEmpty {
29 | if !["root", "loginwindow"].contains(consoleUser) {
30 | let currentUser = NSUserName()
31 | if consoleUser == currentUser {
32 | processItems(.onDemand)
33 | createTrigger(Trigger.cleanup.path)
34 | } else {
35 | writeLog("User \(currentUser) is not the current console user. Skipping on-demand run.", logLevel: .info)
36 | }
37 | } else {
38 | writeLog("No current user session. Skipping on-demand run.", logLevel: .info)
39 | }
40 | }
41 | }
42 |
43 | /// Processes all `.onDemandPrivileged` tasks for the system.
44 | ///
45 | /// This function runs privileged on-demand tasks that have been placed in the
46 | /// `.onDemandPrivileged` payload directory.
47 | /// - Requires root privileges.
48 | /// - Skips execution if there is no current user session (`consoleUser`
49 | /// equals `"loginwindow"`).
50 | /// - Skips execution if the `.onDemandPrivileged` folder is empty.
51 | ///
52 | /// If conditions are met:
53 | /// - Executes `.onDemandPrivileged` payload items via `processItems(_:)`.
54 | /// - Cleans up both the `.onDemandPrivileged` trigger path and its directory.
55 | ///
56 | /// - Note: All decisions are logged at `.info` level for visibility.
57 | /// - SeeAlso: `processOnDemandTasks()`
58 | func processOnDemandPrivilegedTasks() {
59 | ensureRoot("execute on-demand-privileged")
60 | writeLog("Processing on-demand-privileged", logLevel: .info)
61 | if !["loginwindow"].contains(consoleUser) {
62 | if !folderContents(type: .onDemandPrivileged).isEmpty {
63 | processItems(.onDemandPrivileged)
64 | pathCleanup(Trigger.onDemandPrivileged.path)
65 | pathCleanup(PayloadType.onDemandPrivileged.directoryPath)
66 | }
67 | } else {
68 | writeLog("No current user session. Skipping on-demand-privileged run.", logLevel: .info)
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Outset/RunModes/IgnoredUser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IgnoredUser.swift
3 | // Outset
4 | //
5 | // Created by Bart Reardon on 6/8/2025.
6 | //
7 |
8 | import Foundation
9 |
10 | /// Adds one or more usernames to the ignored users list in preferences.
11 | ///
12 | /// This function ensures the current process has root privileges before
13 | /// modifying the ignored users list.
14 | /// - For each provided username:
15 | /// - If the username is already present in `prefs.ignoredUsers`, a log entry
16 | /// is written at `.info` level indicating no change.
17 | /// - If the username is not present, it is appended to the ignored users list
18 | /// and a log entry is written at `.info` level indicating the addition.
19 | ///
20 | /// After processing all usernames, the updated preferences are written to disk.
21 | ///
22 | /// - Parameters:
23 | /// - userArray: An array of usernames to add. Defaults to an empty array.
24 | /// - prefs: The `OutsetPreferences` object containing the current ignored
25 | /// users list and other configuration.
26 | ///
27 | /// - Important: Requires root privileges.
28 | /// - SeeAlso: `removeIgnoredUsers(_:prefs:)`
29 | func addIgnoredUsers(_ userArray: [String] = [], prefs: inout OutsetPreferences) {
30 | ensureRoot("add to ignored users")
31 | for username in userArray {
32 | if prefs.ignoredUsers.contains(username) {
33 | writeLog("User \"\(username)\" is already in the ignored users list", logLevel: .info)
34 | } else {
35 | writeLog("Adding \(username) to ignored users list", logLevel: .info)
36 | prefs.ignoredUsers.append(username)
37 | }
38 | }
39 | writeOutsetPreferences(prefs: prefs)
40 | }
41 |
42 | /// Removes one or more usernames from the ignored users list in preferences.
43 | ///
44 | /// This function ensures the current process has root privileges before
45 | /// modifying the ignored users list.
46 | /// - For each username in `userArray`:
47 | /// - If the username exists in `prefs.ignoredUsers`, it is removed and a log
48 | /// entry is written at `.info` level indicating the removal.
49 | /// - If the username does not exist in the list, a log entry is written at
50 | /// `.info` level indicating that no change was made.
51 | ///
52 | /// After processing, the updated preferences are written to disk.
53 | ///
54 | /// - Parameters:
55 | /// - userArray: An array of usernames to remove. Defaults to an empty array.
56 | /// - prefs: The `OutsetPreferences` object containing the current ignored
57 | /// users list and other configuration.
58 | ///
59 | /// - Important: Requires root privileges.
60 | /// - SeeAlso: `addIgnoredUsers(_:prefs:)`
61 | func removeIgnoredUsers(_ userArray: [String] = [], prefs: inout OutsetPreferences) {
62 | ensureRoot("remove ignored users")
63 | for username in userArray {
64 | if let index = prefs.ignoredUsers.firstIndex(of: username) {
65 | writeLog("Removing \(username) from ignored users list", logLevel: .info)
66 | prefs.ignoredUsers.remove(at: index)
67 | } else {
68 | writeLog("User \"\(username)\" is not in the ignored users list", logLevel: .info)
69 | }
70 |
71 | }
72 | writeOutsetPreferences(prefs: prefs)
73 | }
74 |
--------------------------------------------------------------------------------
/Outset.xcodeproj/xcshareddata/xcschemes/Outset App Bundle.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
57 |
58 |
59 |
60 |
66 |
68 |
74 |
75 |
76 |
77 |
79 |
80 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/Outset/Utils/SystemInfo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SystemInfo.swift
3 | // Outset
4 | //
5 | // Created by Bart E Reardon on 5/9/2023.
6 | //
7 |
8 | import Foundation
9 |
10 | public var osVersion: String {
11 | // Returns the OS version
12 | let osVersion = ProcessInfo().operatingSystemVersion
13 | let version = "\(osVersion.majorVersion).\(osVersion.minorVersion).\(osVersion.patchVersion)"
14 | return version
15 | }
16 |
17 | public var osBuildVersion: String {
18 | // Returns the current OS build from sysctl
19 | var size = 0
20 | sysctlbyname("kern.osversion", nil, &size, nil, 0)
21 | var osversion = [CChar](repeating: 0, count: size)
22 | sysctlbyname("kern.osversion", &osversion, &size, nil, 0)
23 | return String(cString: osversion)
24 |
25 | }
26 |
27 | public var deviceSerialNumber: String {
28 | // Returns the current devices serial number
29 | let platformExpert = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice") )
30 | guard platformExpert > 0 else {
31 | return "Serial Unknown"
32 | }
33 | guard let serialNumber = (IORegistryEntryCreateCFProperty(platformExpert, kIOPlatformSerialNumberKey as CFString, kCFAllocatorDefault, 0).takeUnretainedValue() as? String)?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) else {
34 | return "Serial Unknown"
35 | }
36 | IOObjectRelease(platformExpert)
37 | return serialNumber
38 | }
39 |
40 | public var marketingModel: String {
41 | return !marketingModelARM.isEmpty ? marketingModelARM : marketingModelIntel
42 | }
43 |
44 | public var marketingModelARM: String {
45 | if #available(macOS 12.0, *) {
46 | let appleSiliconProduct = IORegistryEntryFromPath(kIOMainPortDefault, "IOService:/AppleARMPE/product")
47 | let cfKeyValue = IORegistryEntryCreateCFProperty(appleSiliconProduct, "product-description" as CFString, kCFAllocatorDefault, 0)
48 | IOObjectRelease(appleSiliconProduct)
49 | let keyValue: AnyObject? = cfKeyValue?.takeUnretainedValue()
50 | if keyValue != nil, let data = keyValue as? Data {
51 | return String(data: data, encoding: String.Encoding.utf8)?.trimmingCharacters(in: CharacterSet(["\0"])) ?? ""
52 | }
53 | } else {
54 | return deviceHardwareModel
55 | }
56 | return ""
57 | }
58 |
59 | public var marketingModelIntel: String {
60 | guard let locale = Locale.current.languageCode else { return "en" }
61 |
62 | let modelIdentifier = deviceHardwareModel
63 |
64 | var path = "/System/Library/PrivateFrameworks/ServerInformation.framework/Versions/A/Resources/"
65 | path += locale + ".lproj"
66 | path += "/SIMachineAttributes.plist"
67 |
68 | if let fileData = FileManager.default.contents(atPath: path) {
69 | if let plistContents = try? PropertyListSerialization.propertyList(from: fileData, format: nil)
70 | as? [String: Any] {
71 | if let contents = plistContents[modelIdentifier] as? [String: Any],
72 | let localizable = contents["_LOCALIZABLE_"] as? [String: String] {
73 | let marketingModel = localizable["marketingModel"] ?? modelIdentifier
74 | return marketingModel
75 | }
76 | }
77 | }
78 | return modelIdentifier
79 | }
80 |
81 | public var deviceHardwareModel: String {
82 | // Returns the current devices hardware model from sysctl
83 | var size = 0
84 | sysctlbyname("hw.model", nil, &size, nil, 0)
85 | var model = [CChar](repeating: 0, count: size)
86 | sysctlbyname("hw.model", &model, &size, nil, 0)
87 | return String(cString: model)
88 | }
89 |
--------------------------------------------------------------------------------
/legacy/README.md:
--------------------------------------------------------------------------------
1 | Outset
2 | ======
3 |
4 | Outset is a script which automatically processes packages, profiles, and scripts during the boot sequence, user logins, or on demand.
5 |
6 | Requirements
7 | ------------
8 | + macOS 10.15+
9 | + python 3.7+
10 |
11 | If you need to support 10.14 or lower, stick with the 2.x version.
12 |
13 | python3 can be installed from one of these sources:
14 | - [python.org](https://www.python.org/downloads/)
15 | - [MacAdmins](https://github.com/macadmins/python)
16 | - [Munki](https://github.com/munki/munki)
17 |
18 | If none of these are on disk, then fall back to Apple's system python3, which can be installed via the Command Line Tools.
19 |
20 | Outset no longer supports python 2, which was [sunsetted on Jan 1, 2020](https://www.python.org/doc/sunset-python-2/). If you choose to continue to use python 2, you'll want to create the symlink via other means, with something like:
21 |
22 | `/bin/ln -s /usr/bin/python /usr/local/outset/python3`
23 |
24 | Usage
25 | -----
26 |
27 | usage: outset [-h]
28 | (--boot | --login | --login-privileged | --on-demand | --login-every | --login-once | --cleanup | --version | --add-ignored-user username | --remove-ignored-user username | --add-override scripts | --remove-override scripts)
29 |
30 | This script automatically processes packages, profiles, and/or scripts at
31 | boot, on demand, and/or login.
32 |
33 | optional arguments:
34 | -h, --help show this help message and exit
35 | --boot Used by launchd for scheduled runs at boot
36 | --login Used by launchd for scheduled runs at login
37 | --login-privileged Used by launchd for scheduled privileged runs at login
38 | --on-demand Process scripts on demand
39 | --login-every Manually process scripts in login-every
40 | --login-once Manually process scripts in login-once
41 | --cleanup Used by launchd to clean up on-demand dir
42 | --version Show version number
43 | --add-ignored-user username
44 | Add user to ignored list
45 | --remove-ignored-user username
46 | Remove user from ignored list
47 | --add-override scripts
48 | Add scripts to override list
49 | --remove-override scripts
50 | Remove scripts from override list
51 |
52 | See the [wiki](https://github.com/chilcote/outset/wiki) for info on how to use Outset.
53 |
54 | Credits
55 | -------
56 | This script was an excuse for me to try to learn python. I learn best when I can pull apart existing scripts. As such, this script is heavily based on the great work by [Nate Walck](https://github.com/natewalck/Scripts/blob/master/scriptRunner.py), [Allister Banks](https://gist.github.com/arubdesu/8271ba29ac5aff8f982c), [Rich Trouton](https://github.com/rtrouton/First-Boot-Package-Install), [Graham Gilbert](https://github.com/grahamgilbert/first-boot-pkg/blob/master/Resources/first-boot), and [Greg Neagle](https://github.com/munki/munki/blob/master/code/client/managedsoftwareupdate#L87).
57 |
58 | Special thanks to @homebysix for working on the python3 compatibility release.
59 |
60 | License
61 | -------
62 |
63 | Copyright Joseph Chilcote
64 |
65 | Licensed under the Apache License, Version 2.0 (the "License");
66 | you may not use this file except in compliance with the License.
67 | You may obtain a copy of the License at
68 |
69 | http://www.apache.org/licenses/LICENSE-2.0
70 |
71 | Unless required by applicable law or agreed to in writing, software
72 | distributed under the License is distributed on an "AS IS" BASIS,
73 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
74 | See the License for the specific language governing permissions and
75 | limitations under the License.
76 |
--------------------------------------------------------------------------------
/Outset/RunModes/Override.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Override.swift
3 | // Outset
4 | //
5 | // Created by Bart Reardon on 6/8/2025.
6 | //
7 |
8 | import Foundation
9 |
10 | /// Adds one or more scripts to the login-once override list.
11 | ///
12 | /// This function ensures the current process has root privileges before
13 | /// modifying the override list.
14 | /// - Each override entry is processed as follows:
15 | /// 1. If it begins with `"payload="`, the prefix is stripped, leaving only
16 | /// the payload path or script name.
17 | /// 2. If it does not already contain the `.loginOnce` or `.loginPrivilegedOnce`
18 | /// payload directory path, the `.loginOnce` path is prepended.
19 | /// - The processed override path is added to `prefs.overrideLoginOnce` with the
20 | /// current date as the value.
21 | ///
22 | /// After processing all overrides, the updated preferences are written to disk.
23 | ///
24 | /// - Parameters:
25 | /// - overrides: An array of script names or payload paths to add to the
26 | /// override list. Defaults to an empty array.
27 | /// - prefs: The `OutsetPreferences` object containing the current override
28 | /// list and other configuration.
29 | ///
30 | /// - Important: Requires root privileges.
31 | /// - SeeAlso: `runRemoveOveride(_:prefs:)`
32 | func runAddOveride(_ overrides: [String] = [], prefs: inout OutsetPreferences) {
33 | ensureRoot("add scripts to override list")
34 | let loginOnce = PayloadType.loginOnce
35 | let loginPrivilegedOnce = PayloadType.loginPrivilegedOnce
36 |
37 | for var override in overrides {
38 | if override.starts(with: "payload=") {
39 | override = override.components(separatedBy: "=").last ?? "nil"
40 | } else if !override.contains(loginOnce.directoryPath) &&
41 | !override.contains(loginPrivilegedOnce.directoryPath) {
42 | override = "\(loginOnce.directoryPath)/\(override)"
43 | }
44 | writeLog("Adding \(override) to override list", logLevel: .debug)
45 | prefs.overrideLoginOnce[override] = Date()
46 | }
47 | writeOutsetPreferences(prefs: prefs)
48 | }
49 |
50 | /// Removes one or more scripts from the login-once override list.
51 | ///
52 | /// This function ensures the current process has root privileges before
53 | /// modifying the override list.
54 | /// - Each override entry is processed as follows:
55 | /// 1. If it begins with `"payload="`, the prefix is stripped, leaving only
56 | /// the payload path or script name.
57 | /// 2. If it does not already contain the `.loginOnce` payload directory path,
58 | /// the `.loginOnce` path is prepended.
59 | /// - The processed override path is removed from `prefs.overrideLoginOnce` if
60 | /// present.
61 | ///
62 | /// After processing all overrides, the updated preferences are written to disk.
63 | ///
64 | /// - Parameters:
65 | /// - overrides: An array of script names or payload paths to remove from the
66 | /// override list. Defaults to an empty array.
67 | /// - prefs: The `OutsetPreferences` object containing the current override
68 | /// list and other configuration.
69 | ///
70 | /// - Important: Requires root privileges.
71 | /// - SeeAlso: `runAddOveride(_:prefs:)`
72 | func runRemoveOveride(_ overrides: [String] = [], prefs: inout OutsetPreferences) {
73 | ensureRoot("remove scripts to override list")
74 | let loginOnce = PayloadType.loginOnce
75 |
76 | for var override in overrides {
77 | if override.starts(with: "payload=") {
78 | override = override.components(separatedBy: "=").last ?? "nil"
79 | } else if !override.contains(loginOnce.directoryPath) {
80 | override = "\(loginOnce.directoryPath)/\(override)"
81 | }
82 | writeLog("Removing \(override) from override list", logLevel: .debug)
83 | prefs.overrideLoginOnce.removeValue(forKey: override)
84 | }
85 | writeOutsetPreferences(prefs: prefs)
86 | }
87 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Outset
2 | ======
3 |
4 | 
5 |
6 | Outset is a utility application which automatically processes scripts and packages during the boot sequence, user logins, or on demand.
7 |
8 | [Check out the wiki](https://github.com/macadmins/outset/wiki) for more information on how to use Outset or find out [how it works](https://github.com/macadmins/outset/wiki/FAQ).
9 |
10 | ## Requirements
11 | + macOS 10.15+
12 |
13 | ## Usage
14 |
15 | OPTIONS:
16 | --boot Used by launchd for scheduled runs at boot
17 | --login Used by launchd for scheduled runs at login
18 | --login-window Used by launchd for scheduled runs at the login window
19 | --login-privileged Used by launchd for scheduled privileged runs at login
20 | --on-demand Process scripts on demand
21 | --login-every Manually process scripts in login-every
22 | --login-once Manually process scripts in login-once
23 | --cleanup Used by launchd to clean up on-demand dir
24 | --add-ignored-user
25 | Add one or more users to ignored list
26 | --remove-ignored-user
27 | Remove one or more users from ignored list
28 | --add-override