├── 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 | ![Outset Icon](https://github.com/macadmins/outset/blob/78002def008bea2a51f793e0cd033fe71ff81c9b/Outset/Assets.xcassets/AppIcon.appiconset/Outset.png_128x128.png?raw=true) 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