Category: Automation

  • From Bash Script to Native macOS App: The Evolution of Simple Security Check

    Why build an app to check macOS updates?

    Managing a fleet of macOS devices through SimpleMDM often requires constant vigilance over security updates, encryption status, and OS versions. What started as a practical shell script for checking device security status evolved into a full-featured native macOS application. This is the cold/flu season inspired adventure of a crazy idea that a simple shell script could become a Swift app and live in the Mac App Store.

    With enough help from friends and current AI tools those fever dreams can become real. Join us on a long detailed rant from a 278-line Bash script to a modern SwiftUI app with secure credential management, intelligent caching, and a semi-decent and mostly functional user interface.

    Simple Security Check app with test data
    Simple Security Check app with test data

    The Beginning: A Shell Script Solution

    The original tool was born from a simple need: cross-reference SimpleMDM device data against the SOFA (Simple Organized Feed for Apple Software Updates) macOS security feed to identify which devices needed macOS updates. The shell script was straightforward but capable enough to export a spreadsheet for clients to review in a simple presentation:

    ```bash
    
    #!/usr/bin/env bash
    
    set -euo pipefail
    
    
    
    
    # Fetch devices from SimpleMDM
    
    # Compare against SOFA feed
    
    # Export CSV reports
    
    ```
    
    

    What the Shell Script Did Well

    The shell script handled several complex tasks more or less efficiently:

    1. **API Pagination**: Properly implemented cursor-based pagination for SimpleMDM’s API, handling potentially thousands of devices across multiple pages with retry logic and exponential backoff. Note: the very first version I posted didn’t do this at all, but thanks to a reminder from a helpful MacAdmin I remembered I needed to implement pagination and do it properly. Thanks!

    2. **Smart Caching**: Cached both SimpleMDM device lists and SOFA feed data for 24 hours, reducing API calls and improving performance.

    3. **Comprehensive Security Tracking**: Monitored FileVault encryption, System Integrity Protection (SIP), firewall status, and OS version compliance.

    4. **Flexible Exports**: Generated three types of CSV reports and full JSON exports with timestamps, automatically opening them in the default applications.

    5. **Version Intelligence**: Compared devices against both their current major OS version’s latest release and the maximum compatible OS version for their hardware model.

    The Pain Points

    However, the shell script approach had limitations:

    – **API Key Management**: The API key had to be entered each time or set as an environment variable—no secure storage mechanism.

    – **Single Account**: No support for managing multiple SimpleMDM accounts or environments.

    – **Limited Search**: Finding specific devices required opening CSVs and using spreadsheet search.

    – **No Visual Interface**: Everything was command-line based, requiring users comfortable with terminal operations.

    – **Manual Execution**: I had to remember to run it periodically.

    The script even had a TODO comment acknowledging its destiny:

    ```bash
    
    # to do: make into a native swift/swiftUI app for macOS
    
    # with better UX saving multiple API key entries into
    
    # the keychain with a regular alias

    “`

    The Transformation: Building a Native macOS App

    The decision to create a native macOS application wasn’t about abandoning what worked—it was about preserving that core functionality while addressing its limitations. And most importantly, being nerd-sniped by a colleague saying why not make it into a Swift app using current AI tools. I thought I could try it. How hard could it be? haha. What do I know about Swift, and what do I know about what is possible? Let’s see. The goal was clear: maintain 100% feature parity with the shell script while adding the convenience users expect from modern macOS software. And simplicity. I wanted a simple app to use to make all our lives easier. At least, this one part.

    Architecture Decisions

    The app was built using SwiftUI with a clear separation of concerns:

    **AppState.swift** – The Brain

    ```swift
    
    @MainActor
    
    class AppState: ObservableObject {
    
        @Published var apiKeys: [APIKeyEntry] = []
    
        @Published var devices: [SimpleMDMDevice] = []
    
        @Published var sofaFeed: SOFAFeed?
    
        @Published var searchText = ""
    
        @Published var showOnlyNeedingUpdate = false
    
    }
    
    ```
    
    

    This centralized state manager coordinates all data operations, making the UI reactive and keeping business logic separate from presentation.

    **KeychainManager.swift** - Secure Storage
    
    ```swift
    
    class KeychainManager {
    
        func saveAPIKey(_ key: String, for alias: String) throws {
    
            // Store in macOS Keychain with kSecAttrAccessibleWhenUnlocked
    
        }
    
    }
    
    ```

    One of the shell script’s biggest weaknesses became one of the app’s strongest features. API keys are now stored securely in macOS Keychain, never exposed in plain text, and protected by the system’s security model.

    **DatabaseManager.swift** - Intelligent Caching
    
    ```swift
    
    class DatabaseManager {
    
        func getCachedDevices(forAPIKey alias: String) -> [SimpleMDMDevice]? {
    
            // Query SQLite with 24-hour cache validation
    
            // Indexed for fast search
    
        }
    
    }
    
    ```

    The file-based JSON caching from the shell script evolved into a SQLite database with indexed search capabilities. Each API key gets its own cached dataset, and the 24-hour cache duration from the original script was preserved.

    **APIService.swift** - Network Layer
    
    ```swift
    
    class APIService {
    
        func fetchAllDevices(apiKey: String,
    
                            apiKeyAlias: String,
    
                            forceRefresh: Bool) async throws -> [SimpleMDMDevice] {
    
            // Same pagination logic as shell script
    
            // Same retry mechanism with exponential backoff
    
            // Same User-Agent header pattern
    
        }
    
    }

    “`

    The API fetching logic was ported almost line-for-line from the shell script. The same pagination handling, the same retry logic, even the same User-Agent pattern. If it worked in Bash, it works in Swift. And the User-Agent pattern came from a helpful Issue submitted in GitHub about making the shell script a better part of the SOFA ecosystem. Thanks again!

    What Got Better

    **Multiple API Key Support**

    The single biggest improvement was supporting multiple SimpleMDM accounts. IT administrators often manage multiple clients or environments. The app now stores unlimited API keys with custom aliases:

    – “Production” for your main environment

    – “Testing” for sandbox testing

    – “Client A“, “Client B” for MSPs managing multiple organizations

    Each API key appears as a tab in the interface, with separately cached data for instant switching.

    **Real-Time Search and Filtering**

    The shell script required exporting to CSV and searching in a spreadsheet. The app provides instant, full-text search across all device attributes:

    ```swift
    
    var filteredDevices: [SimpleMDMDevice] {
    
        var result = devices
    
    
    
    
        if showOnlyNeedingUpdate {
    
            result = result.filter { $0.needsUpdate }
    
        }
    
    
    
    
        if !searchText.isEmpty {
    
            result = result.filter { device in
    
                name.localizedCaseInsensitiveContains(searchText) ||
    
                deviceName.localizedCaseInsensitiveContains(searchText) ||
    
                serial.localizedCaseInsensitiveContains(searchText) ||
    
                // ... and more fields
    
            }
    
        }
    
    
    
    
        return result
    
    }
    
    ```

    Type a serial number, see the device instantly. Toggle “Needs Update” to focus on out-of-date machines. Sort by most columns with a click. Note: I did run into a limitation with the number of sortable columns in the Swift code, many iterations and trials and eventually I found something that worked. Yeah Swift!

    **Automatic Refresh with Progress**

    The shell script required manual execution. The app handles refresh automatically:

    – Background refresh respects the 24-hour cache

    – Progress indicators show API fetch status

    – Force refresh option bypasses cache when needed

    – Errors display in-app with clear messaging

    What Stayed the Same (Intentionally)

    Certain aspects of the shell script were functional and useful so they were copied in the app:

    **Export Format Compatibility**

    The CSV exports use the exact same format as the shell script:

    ```csv
    
    "name","device_name","serial","os_version","latest_major_os",
    
    "needs_update","product_name","filevault_status",
    
    "filevault_recovery_key","sip_enabled","firewall_enabled",
    
    "latest_compatible_os","latest_compatible_os_version","last_seen_at"
    
    ```

    Users who had automated workflows processing these CSVs didn’t need to change anything.

    **Output Directory Structure**

    Files still export to `/Users/Shared/simpleMDM_export/` with the same naming convention:

    “`

    simplemdm_devices_full_2025-12-11_1430.csv

    simplemdm_devices_needing_update_2025-12-11_1430.csv

    simplemdm_supported_macos_models_2025-12-11_1430.csv

    simplemdm_all_devices_2025-12-11_1430.json

    “`

    **Cache Duration**

    The 24-hour cache validity period was retained. It’s a sensible balance between API rate limiting and data freshness for device management.

    **SOFA Integration Logic**

    The algorithm for matching devices against SOFA feed data remained identical:

    1. Build a lookup table of latest OS versions by major version

    2. Match each device’s hardware model against SOFA’s compatibility data

    3. Determine both “latest for current major” and “latest compatible overall”

    This dual-version approach is valuable for planning: devices might be current on macOS 13.x but capable of running macOS 15 or macOS 26. It’s good to know.

     Technical Highlights

     Security Model

    The app runs fully sandboxed with carefully scoped entitlements:

    ```xml
    
    <key>com.apple.security.app-sandbox</key>
    
    <true/>
    
    <key>com.apple.security.network.client</key>
    
    <true/>
    
    <key>com.apple.security.files.user-selected.read-write</key>
    
    <true/>
    
    ```

    API keys use Keychain with `kSecAttrAccessibleWhenUnlocked`, meaning they’re protected when the Mac is locked.

     Data Flow

    1. **Launch**: Load API key metadata from UserDefaults, actual keys from Keychain

    2. **Refresh**: Check SQLite cache validity, fetch from APIs if needed

    3. **Process**: Merge SimpleMDM and SOFA data using the same algorithm as the shell script

    4. **Cache**: Store in SQLite with timestamp and API key association

    5. **Display**: Render in SwiftUI Table with reactive filtering

    Test Mode Feature

    A unique addition not in the shell script: a test mode that generates dummy devices for demonstrations and screenshots:

    ```swift
    
    func toggleTestMode() {
    
        testModeEnabled.toggle()
    
    
    
    
        if testModeEnabled {
    
            let demoEntry = DummyDataGenerator.createDemoAPIKeyEntry()
    
            apiKeys.insert(demoEntry, at: 0)
    
    
    
    
            let dummyDevices = DummyDataGenerator.generateDummyDevices(count: 15)
    
            // ... process with real SOFA data
    
        }
    
    }
    
    ```
    
    
    

    This allows testing the full UI without a SimpleMDM account—perfect for App Store screenshots or demos. And it turns out a requirement for the App Store review process since the alternative was giving them API keys to real data to test with, which I could not do, of course, and which brought us to generating test data. A perfect plan.

     Lessons Learned

    What Worked

    **Preserve the Core Logic**: The shell script’s API handling, caching strategy, and data processing were good enough and worked. Porting them to Swift rather than redesigning saved time and avoided regressions.

    **Prioritize Security from Day One**: Building Keychain integration first made everything else easier. API keys are sensitive, and getting that right early prevented technical debt.

    **SwiftUI for Rapid UI Development**: Building the table view, settings panel, and navigation in SwiftUI was dramatically faster than AppKit would have been. But since my experience was using an app like Platypus for simple app creation using SwiftUI was definitely more flexible and possible with help from current tools.

    What Was Challenging

    **Async/Await Migration**: The shell script’s sequential curl calls had to become proper async Swift code with structured concurrency.

    **SQLite in Swift**: While more powerful than file caching, setting up proper SQLite bindings and schema management added complexity. and app sandbox rules moved the location of the cache and added a wrinkle in testing.

    **Tab-Based Multi-Account UI**: The shell script only handled one API key. Designing an intuitive interface for switching between multiple accounts required several iterations.

     Performance Comparison

    **Shell Script**:
    
    - Initial fetch: ~8-12 seconds for 100 devices
    
    - Subsequent runs (cached): ~2-3 seconds
    
    - Search: N/A (requires opening CSV)
    
    
    
    
    **Swift App**:
    
    - Initial fetch: ~8-12 seconds for 100 devices (same API calls)
    
    - Subsequent launches: <1 second (SQLite cache)
    
    - Search: Real-time (indexed database queries)
    
    - Switching API keys: Instant (cached data)

     The Result

    The final application preserves everything that made the shell script valuable while transforming the user experience:

    – **Same data, better access**: All the security metrics, none of the manual CSV searching

    – **Same exports, more secure**: Identical CSV format, Keychain-protected credentials

    – **Same caching, faster searches**: 24-hour cache retained, SQLite indexed queries added

    – **One account to many**: Support for unlimited SimpleMDM accounts. Good for testing.

    – **Terminal to GUI**: From command-line to native macOS interface

    The app isn’t just a shell script wrapped in a window—it’s a giant leap into Swift app production which challenged me enormously for troubleshooting and app testing. This app is a small step in the code adventures that await us all when we want to take an idea, from shell code to Mac app.

     Future Enhancements

    While the current version achieves feature parity and then some, there’s room to grow:

    – **Scheduled Auto-Refresh**: Background fetching on a schedule

    – **Push Notifications**: Alerts when devices fall out of compliance

    – **Export Automation**: Scheduled exports to specific directories

    – **Custom Filters**: Save filter configurations for different report types

    – **Device Groups**: Tag and organize devices into custom categories

    – **Trend Analysis**: Historical tracking of fleet compliance over time

    This is not the end

    The journey from shell script to native app demonstrates that nerd-sniping does work and we can be pushed to try new things. The shell script’s core logic—its API handling, caching strategy, and data processing—was already ok, somewhat decent, and at least functional. The leap to all Swift was about making that functionality more accessible, more secure, while making testing and troubleshooting more difficult and confusing, but also a valuable learning opportunity. Xcode 26.1 has some basic code fixing abilities that we tested many times. It helped!

    For IT administrators managing Mac fleets, the app delivers what the script did (device security monitoring and reporting) with what the script couldn’t (multi-account support, instant search, secure credential storage, and a native interface).

    The script’s final TODO comment has been fulfilled:

    ```bash
    
    # to do: make into a native swift/swiftUI app for macOS
    
    # with better UX saving multiple API key entries into
    
    # the keychain with a regular alias
    
    ```

    ✅ Done.

    **Simple Security Check** is available for macOS 15.0 (Sequoia) and later from the Mac App Store. The original bash source code and architecture documentation can be found in the project GitHub repository.

    *Built with SwiftUI, powered by the same bad logic that served IT admins well in its shell script form, now with the wild woodland scent of a native macOS application.*

  • Don’t stop!

    Using a REST API to code a bunch of useful apps

    Background: I released some scripts to help users manage Archiware P5 servers on code.matx.ca with a first blog post as a background to the scripting approaches of cli vs REST API and then I discussed my cute Platypus built apps and my first swift/swiftUI Mac apps… so of course now I’m going to discuss two new Mac apps that use a REST API to explore a P5 Archive.

    I’m building tools to help me and my clients work with Archiware’s amazing and awesome P5 Archive product. It’s great. It archives, it restores, it has a great web UI, and it’s always getting better. So why build apps? Sometimes you want something different, in my case my clients wanted spreadsheets. Yup. Data in a sheet. To look at. So I poked at P5’s databases via cli (see P5 Archive Manager) and with the REST API. Here are the results, two new apps P5 Archive Overview and P5 Archive Search.

    https://code.matx.ca/ is code on GitHub + Mac apps that help manage data in Archiware P5

    Why API? And not cli

    Usually the cli (command line interface) is perfect for working in Terminal and in shell scripts or other programming languages. Using an API or application programming interface, allows different software applications to communicate and share data with each other. Instead of cli commands and arguments programs make requests using specific methods like GET or POST to retrieve or send data. See: API on Wikipedia

    Using an API for my Mac apps means they could use a protocol like HTTP, and make a request using GET (to retrieve data).

    The magic parts of an API

    • API Client: The application making the request.
    • Endpoints: Specific URLs where the API can be accessed.

    For the P5 Archive Overview app we need to use the specific API Endpoint for archive overview detailed by Archiware here which lucky for us is a simple call for data which our app can display, save as json, format as csv (for the spreadsheet!!) and stash in a SQLite database for historical searches.

    However, the P5 Archive Search app has to make many calls to walk through the index tree which is an inventory by path of the files archived. So we ask the user to name the storage they want to search and we make a breadcrumb through the storage, storing everything in SQLite as well as saving json and csv snippets of what we find. A lot more API calls but perfect for an app to do in the background.

    The P5 Archive Search app queries the P5 Archive Index Inventory REST API:

    “`

    GET http://{server}:{port}/rest/v1/archive/indexes/{archive-index}/inventory/{path}

    “`

    Example:

    “`

    http://192.168.1.100:8000/rest/v1/archive/indexes/Default-Archive/inventory/Volumes/BigStorageSMB

    Made for Macs, macOS 14 and up. Bring your own jq

    When running macOS 15 and up jq is installed for free and can help make the csv files form the json, but if you’re running macOs 14 then you need to install it with homebrew, MacPorts, or on your own.

    One funny story during the early testing, I had to fix the jq detection in my first version because it totally missed reading the unix path correctly and yeah, thanks to a friend on the MacAdmins Slack who had an issue when trying it out. I was able to go back and fix that. Friends help friends make better code.

    I’m not done making apps, and I’ll keep tweaking these three I have so far and making new ones from the scripts I have. The goal is to manage data, get a better understanding of the data in the archives and let the clients and owners of the data to know what they have.

    Stay tuned.

  • Swiftly make an app

    This is a blog post about an app, there are many of them but this is mine. How I made a swift app in Xcode but took the long way ’round to get there. And an even longer post to tell the story.

    P5 Archive Manager, an app I created with AI tools to help check files archived with Archiware P5

    I recently posted about some scripts I released on github to help other admins using Archiware P5 archive software to manage their servers and the data in the archive vault.

    Claude: “How can I help you today?”

    Using free credits in various AI tools will get you pretty far, but how far, really? Can you make an app? A useful app? Maybe, yes. Encouraged to transform some simple shell script projects and make a “proper” swift / swiftUI based Mac app I started using Claude as a test. The result is two apps for checking Archiware P5’s archive with a drag and drop tool that verifies files with local or remote servers. Code and apps on GitHub.

    # Path to nsdchat
    chatcmd="/usr/local/aw/bin/nsdchat -c"

    Working with Microsoft’s Visual Studio Code app with co-pilot (tied to my GitHub account) and occasional ChatGPT coding sessions, when co-pilot ran out of free credits, I was surprised that I made a lot of progress on some more complicated scripts.

    ChatGPT: “Ask anything”

    It all started with shell scripts to help me automate some tasks with managing data and eventually I wanted to create a Mac app (or two) for my clients and me to use. I like scripts, but I have so many. And solving a problem in terminal and sifting through all those amazing scripts became more and more complicated, so maybe I needed a nice app that you can click and launch and be done with it to do that one thing. Single minded apps for single purpose objectives.

    Platypus created app. Check if files are archived by drag and drop

    I asked my friendly AI super tool to suggest a way to make an app from a shell script to see if it had any ideas, and it did have a few, although Mac specific ones were not plentiful. Looking through a list of possible methods, it suggested I use the excellent and awesome Platypus app. Now that’s a name I haven’t heard of it a while.

    Platypus app UI. Pick a script, choose an interface and create an app.
    #!/bin/sh
    
    alias nuke="/Applications/Nuke5.2v1/Nuke5.2v1.app/Nuke5.2v1"
    export NUKE_PATH="/Volumes/XSAN/District9/Foundry/Nuke/"
    export OFX_PLUGIN_PATH="/Volumes/XSAN/District9/Foundry/OFX/"
    
    /Applications/Nuke5.2v1/Nuke5.2v1.app/Nuke5.2v1

    I used it many years back in VFX when working with Nuke and other pipeline tools but now I really needed it. Lucky for me it is still around and still works great. So I built a bunch of small Mac apps wrapped around simple shell scripts. Many of my scripts for P5 acted on a path (ie what folder of files do you need to examine) and making drag and drop app in Platypus was incredible easy. Add swiftDialog in the mix and you get nice messages to communicate progress.

    Platypus app with embedded files, including swiftDialog.

    These Platypus born simple apps worked well but I wanted to sign and notarize them and honestly that’s always fun in part because Apple, like every company, changes the way to do it all the time so I often stumbled at this step and looked for more helper apps and guidance.

    I previously used SD Notary Tool and while I had success in the past I got stuck somewhere in the process and couldn’t figure it out. Then I remembered someone had posted in the MacAdmins Slack about a cool new app they built called Signaro to help sign apps. I had looked at it and was initially confused (lots of buttons and options!) but now I needed it. So I tried again and it really helped.

    Signaro app for signing Mac apps, and notarizing and the whole app distribution workflow

    At first I was stuck in the same step, and it was the app specific password that was my first main issue. I couldn’t figure it out and so re-created it with Apple and still nothing, then one more time and for some reason it worked. Success!

    Small side story: I did find a small bug in Signaro when entering the AppleID and App password that I reported and it was fixed right away. In the process of submitting the bug I chatted with the author and I learned about the keychain profile option to make this step easier so thanks again for the helpers in this community.

    xcrun notarytool store-credentials MacVFX
    
    This process stores your credentials securely in the Keychain. You reference these credentials later using a profile name.

    Another confusing part of signing and notarizing is that there are so many certificates you need so I was super pleased to have Signaro identify that I was using an incorrect cert and suggested how to fix the issue. The step by step process and the clear messaging is a big win in this app. It’s a Swiss army knife for signing, notarizing, making DMGs, and all the steps for preparing your app for distribution. Huge shout out to the author.

    Signaro app certificate check.

    So I’m pretty setup I have all the free AI coding tools to smash on my bash shell scripts, I have Platypus for creating the Mac app, bundled swiftDialog and other assets, and lastly a smooth signing and notarizing workflow with Signaro to distribute the apps. What else do I need? What about a native swift app built in Xcode?

    Again talking with the friendly author of Signaro he suggested why not make a swift Xcode app with some AI super tool? Hmm, I thought. Ok, and 3 hours later I’d built something that didn’t work and my “free credits” had run out. Surprisingly, Xcode has its own AI tool helper built-in to macOS 26 and I found it quite useful to fix small issues.

    Xcode has built in AI tools to fix small mistakes.

    When I ran into my first coding road block and free AI credit exhaustion, I also realized I needed lunch and some fresh air, even maybe a break from coding at my desk. As I took a short break I realized I could troubleshoot the old fashioned way and figure out what was not working while I waited for more “free credits” to re-materialize (or I could sign up!). After food and rest it only took me a few minutes to realize that my app did in fact work correctly, but I was testing it incorrectly!

    So maybe you need fresh air and a break from the keyboard occasionally. Truth time. I finally did sign up for a month to test Claude out some more and it is great, but it also gets tired and reaches a maximum length of a conversation. Everyone needs fresh air.

    Even with a simple app, and before running out of credits, you reach max length of conversation.

  • Archiware P5 scripts

    Background: I manage a few backup and archive servers for clients and these run the Archiware P5 suite of software (archive, backup and sync). To help manage these servers over the years I’ve written some P5 monitoring tools for Watchman and MunkiReport as well worked on helper scripts using the cli tools or more recently the API.

    In an effort to share some simple examples of what is possible I have organized a few samples from my GitHub repos on the code.matx.ca page with some useful descriptions and text about usage and purpose of the scripts. They are hosted on in repo here:

    https://github.com/macvfx/Archiware

    I have more scripts of general and specific interest in my repo to P5 users or anyone managing files. See this post on find scripts.

    The P5 code toolbox

    The following P5 scripts are just a few examples and I have more to share, if there’s interest. I created most of these simple tools initially to run on the P5 server directly but I have since created, for my clients, versions which run from anywhere. Also, in some cases, a few scripts have now been built into easy to use Mac applications where it makes sense. If you want some help and you want to hire me to help with these things please reach out.

    The scripts are in three categories: 

    1) P5 archive intelligence (all archive jobs from Db exported as a spreadsheet, or get recent archive jobs via REST API),

    2) P5 housekeeping (make all full tapes read only, show all appendable archive tapes), and 

    3) P5 info backup (export all volumes into one csv, and export all volumes inventory as TSV with barcode as the name)

    P5 Archive Intelligence

    What do I mean by “archive intelligence”? Simply, I want to know about everything I’ve archived. One should consider the P5 Archive server as the ultimate source for all things archived but in some cases my clients don’t use the P5 server directly, or they want the information organized differently, like in a spreadsheet. And while the Archiware P5 suite of software is ever evolving, growing and adding features (even some lovely visual dashboards in v.7.4) I have been attempting to solve the perennial question of “what do we have archived?” in better and more useful ways.

    P5 Information Backup

    Related to archive intelligence is knowing what is in my P5 archive system entirely. I modified a provided shell script from the Archiware cli manual to output a csv of a list of all P5 volumes in the tape library (aka jukebox) so that I might know what is in the system at all times, and even if my P5 server is not running I have a record of every tape. This is one of the scripts I run with my periodic and backup workflows but more on my own special P5 backups (backups of the P5 Db and other metadata) in another post. The Archiware provided P5 volume list script inspired my own script to list full and appendable archive tapes which I have as a one-click desktop app for my clients. When they want to restore something P5 will tell them what to put in the tape library but if you have a lot of tapes maybe you want to know what to remove and so I have a list of candidates (ie take out the full tapes, and leave the appendable archive tapes). Helpful, yes.

    P5 Inventory

    There is a P5 cli command to export out the complete inventory and depending on how long you’ve been archiving and how much is in your archive this tool can take a long time to export a list of every file ever. And because of my mostly non-advanced super skills some times I’d find this process would time out. (There are ways around this but that’s another post). Basically, Too much archive! When it didn’t error out I had a big file… so one day a friend of mine suggested we use Jupyter notebook and yes, use Python!, to do some data analysis. A really fun project, great tool, but this is a hard problem to solve. We made a thing, it worked for a while then I wanted to find a better way. People liked my bar graphs and total amount archived but they also wanted spreadsheets. So let’s give them what they want.

    Two (or three) approaches:

    • cli
    • api
    • db

    I like lower case letters which are acronyms but let’s explore further.

    cli

    Using the cli (command line interface) usually in a shell script (but also in clever Mac apps) typically requires running the script with the Archiware P5 cli (nsdchat) locally on the P5 server and certainly this is what I did when I was testing scripts and various tools. It makes sense if you’re administering a server and you remote in (ssh or screenshare) and that’s where you start. After I wile I discovered the trick to make these cli based scripts run from anywhere which was handy if I wanted to connect to all my p5 servers at once in a script or have my client use an app on their desktop which talks to the P5 server. More on awsock in another post.

    An example of a cli script using nsdchat I have a script for taking the inventory contents of each archive tape (LTO) and writing its contents to a TSV file (tab separated values) which is like a CSV (comma separated values).

    nsdchat Volume names

    Give me a list of p5 volumes (ie tapes)and tell me which one are archive tapes and that are readonly and what is the barcode.

    api

    Ok instead of a cli command dependent on the nsdchat binary installed somewhere we are doing http (web) magic with the API (application programming interface) — a set of known commands in a path based on GET, PUT, POST, DELETE. The API has a different way of doing things than the cli but you can ask a lot of the same basic questions.

    In my api-archive-overview script I am sending one command then using jq to select elements to organize the info into a csv (spreadsheet). This example is set to run locally but this is easily modifiable to run from anywhere. For one client I have a script that talks to every P5 server, each in a different city and asks them all what they’ve been archiving then organizes it all into one spreadsheet. It’s fun, and it’s helpful.

    db

    The best for last. I mentioned above my attempts to use the inventory command which itself goes to all the relavant databases and gathers all the requested data about every archive file, its size and when it was archived etc. Yeah, that’s one way to do it. I’ve shown two examples above for the cli and api but a third is to just talk to the database directly. This is an advanced technique and should only be attempted by an expert. Ok, I’m kidding. As long as you’re not writing to the database and only reading from it, this is pretty safe. What you do with the info is another magic trick and one which I’ve been working on. Two db examples are dump all jobs in a csv file and the second, dump only archive jobs. I’ve got a more advanced script which takes the data and uses sql commands to organize into a csv of how much data archived per day per week per month per year and totals, which is nice for some people who like spreadsheets and want to know about everything ever done. Caution: once you look into the Db you’ll see a lot of things, and sorting through it takes time. I found when making more advanced and selective scripts that the cli jobs used by the very old P5 Archive app (by Andre Aulich) for example showed up as system jobs not archive jobs, so you have to be careful if you want to include those. Have fun.

    P5 Housekeeping

    Finally, some housekeeping scripts are included in the example repo, like the script to make all archive tapes that are “full” to be marked “read only” which is handy is you also have a script to only export the contents of each tapes from archive tapes marked readonly so many little scripts to do little things.

    P5 Archive Prep scripts

    There’s another category of scripts which I haven’t elaborated on but I do have a few examples in my repo. I do have scripts to prepare or examine files and folders going to be archived to LTO with P5 Archive. These scripts do various things like check for trailing space in the name or check file name length but maybe the most important ones I have are scripts which take the path of the archived projects and create html maps, file size directory listings and spreadsheets (again!) of the exif data of all files to keep for future. Clients do refer to the archive stub files (p5c) but they also find it handy to see the directory map and the file size of archived items without going into the p5 server. I’m not trying to replicate the P5 server, or replace it, but this falls into p5 housekeeping and p5 information backup.

    That’s enough for now. If you’ve been reading and following along then let me know if you any questions or want any help with a P5 or other related projects. If you have better ways to do these things feel free to share. My scripts are always evolving and I love to learn.

    Reference:

    For more info on Archiware P5 scripting and building code to interact with it I’d recommend checking out the main P5 manual, as well as the CLI (command line interface) manual, and the API documentation, knowledge base (support). As well as the sample scripts and the Archiware blog, and the video series generally.

  • Add header here

    Or remove it, up to you,

    Had some fun creating a longer script to add a text header to some shell scripts, then because I wrote the wrong thing to all my shell scripts I had some more fun tweaking my script to find and remove this header. I’ve added it to my GitHub repo with a couple of other scripts based on the find command, one of my favourite unix tools since it is so handy.

    The script that should be a Unix one-liner: add (or remove) a header

    Some of the other example scripts based on find might be of interest to some, such as the

    File Name Check

    Especially important with certain filesystems (certain encrypted filesystems) with file name “length limits”. So why not check for these files and zip them up and put them aside for safe keeping. In practise, the only files which push this limit are downloaded (purchased) from stock photos sites and write the file name with every keyword. Nice, but why can’t we have standard metadata handling these days? (I can dream!)

    Archiware P5

    The last two scripts I made with Archiware P5 in mind, as I manage many servers for clients with P5 Archive and I really do love this software and the team. More Archiware P5 inspired scripts are in other repos here or on my main P5 code site

    Find A Trailing Space

    In this case, besides it just being nice to clean up folder names with invisible trailing spaces before the archive job it was also necessary when using the P5 Companion (desktop) app which will not archive a top-level folder with a space at the end of the name.

    Make (Only) One Thumbnail

    This script makes only one image thumbnail per RDC folder, as they normally have a lot of R3D files which are part of the same shot. Also, I don’t want a lot of duplicates and only one is enough.

    And while yes technically P5 Archive can make thumbnails and proxy videos when it is archiving (and I do use this feature) making proxies of RED files is an intensive process for older computers which means taking a long time, so pre-processing these R3D files ahead of time on faster computers can make the final archive job quicker. As part of some pre-processing before archiving to LTO (or wherever) is making sure some formats like R3D (aka RED) files have a thumbnail which will then end up in the P5 Archive created Archive index.

  • Steal This Idea

    check all your Macs at once with SOFA feed

    Note: this blog post relates to the previous one where I introduce the scripts to check SimpleMDM devices and compare with latest version info in the SOFA feed here: Use the SOFA feed to check if SimpleMDM devices needs updates

    Ok, please steal this idea. The idea? To check all your Macs at one time, instead of each device, on device, one at a time.

    What do I mean? Well, when I first heard about the SOFA feed which contained all the latest versions I didn’t know what to do with it honestly but soon after I realized that my clever script for checking XProtect version and which I made into a custom attribute in SimpleMDM and added to the dashboard was an incomplete idea.

    Ok, I’m smart, I got the XProtect version on each Mac by running a script and then I got SimpleMDM to display it in a dashboard. But what’s missing? Context. Is it the latest version or not? So I added a SOFA check to the script then made SimpleMDM display both the local version and the latest version so I’d know if it was the latest or not. Great, right? Well, maybe.

    The problem, I realized is that I wanted to do this for the macOS version too because I wanted to share info with a client/manager etc and realized the list of devices and info about macOS versions for example, lacked the context of whether it was the latest, and should we take action or not. That’s the point, right? collect info then do something about it, if action is required. Update your macOS now.

    And then I wondered why I’m getting every Mac to ask itself what is its macOS or XProtect version, etc, when SimpleMDM was asking a lot of those questions already and putting it in a dashboard, accessible via API….

    Then it happened, the idea that should be stolen by SimpleMDM and all other management tools. Don’t just display info about a Mac’s macOS version, show the latest version next to it, because I want to know if it should be updated. And also what is the latest that Mac can upgrade to. Maybe it’s running macOS 13.6, is that the latest or is 13.7.7, no wait it changed again, it’s 13.7.8. And by the way the latest compatible upgrade is 15.6.1, now that’s useful info.

    product_nameos_versionlatest_major_osneeds_updatelatest_compatible_oslatest_compatible_os_version
    Mac13,114.7.414.7.8yesSequoia 1515.6.1
    MacBookPro17,114.6.114.7.8yesSequoia 1515.6.1
    Mac13,215.615.6.1yesSequoia 1515.6.1
    iMac21,115.515.6.1yesSequoia 1515.6.1
    MacBookPro17,113.613.7.8yesSequoia 1515.6.1

    References:

    Check SimpleMDM device list and compare macOS version vs SOFA feed latest

    XProtect check version compared to latest SOFA

  • Use the SOFA feed to check if SimpleMDM devices needs updates

    I wrote a “simple” bash script to check SimpleMDM device list by API and check if any devices need updates and/or are compatible with the latest macOS. Of course, it will output some CSVs for fun and profit. Send to clients, managers, security professionals and be well.

    Note: It was a quick hack and for reasons I made 3 output CSVs for testing various presentations of the data that combines the full SimpleMDM device list and matches the macOS with available updates and max supported versions. There may be errors or omissions. Please test. Use and modify. I know I will. This is a test. Just a test.

    The script is in my GitHub repo

    Fetching SimpleMDM device list...
    Downloading SOFA feed...
    ✅ Exported:
      → Full device CSV: /Users/Shared/simplemdm_devices_full_2025-07-30.csv
      → Outdated devices CSV: /Users/Shared/simplemdm_devices_needing_update_2025-07-30.csv
      → Supported macOS per model: /Users/Shared/simplemdm_supported_macos_models_2025-07-30.csv
    ✅ Export complete.
    

    References:

    SOFA MacAdmins Getting Started

    https://sofa.macadmins.io/getting-started.html

    https://github.com/macadmins/sofa/tree/main/tool-scripts

    SimpleMDM API docs

    https://api.simplemdm.com/v1#retrieve-one-dep-device

    squirke1977 / simpleMDM_API

    https://github.com/squirke1977/simpleMDM_API/blob/master/device_details.py

  • Dynamic Groups – SimpleMDM tricks and tips part2

    When we last left our hero the big news was the discovery custom attributes and running scripts to test for certain conditions in SimpleMDM, like “is the firewall on” to post in the main dashboard was all the excitement, this year we present “dynamic groups” which in combination with custom attributes or by itself ups the game to the next level. Keep up!

    What if we wanted to know what is the current version of XProtect across the Mac fleet? and what if this wasn’t collected by default by MDM tool, in my case, SimpleMDM. Well, I can write a script to collect this info, for my purposes I’ve chosen to use silnite from Howard Oakley of eclectic light co fame and write the version number to a custom attribute. The next step is use SimpleMDM’s new dynamic groups (in preview, at the time of this blog post), and then I can watch the result filter in with a special group watching for “is matching this version” or the opposite “is not this version”. Just depends on what you want to act on or how you want to see the information. The new dynamic groups is the exciting part. I’m sooo excited.

    The custom attribute

    Screenshot

    Setting up a custom attribute of “XProtectV: and a default value of “Version Unknown” should be done before the script runs. If I get the default result then the script didn’t run or some other reason.

    The code

    #!/bin/bash
    LOG_DIR="/Users/Shared"
    DATE=$(date +"%Y-%m-%d_%H-%M-%S")
    LOG_FILE="$LOG_DIR/silcheck-log-$DATE.txt"
    /usr/local/bin/silnite aj > "/Users/Shared/silnite-xprotectv-$DATE.json"
    XPROTECTV=$(/usr/bin/plutil -extract XProtectV raw "/Users/Shared/silnite-xprotectv-$DATE.json")
    echo "$XPROTECTV" | tee -a "$LOG_FILE"
    

    The simple script writes a log into /Users/Shared just because I want to and uses the silnite binary to write out the XProtect info and plutil to extract the info from the json Note: you could also use jq in latest macOS 15 but this way is more compatible across macOS versions for now. The XProtect version is saved as an attribute which SimpleMDM picks up and reports back to base.

    The dynamic group

    Screenshot

    The filter headings are a little cut off in the screenshot but it basically says choose from all devices, refer to the custom attribute I set of XprotectV and makes sure the value equals the latest (at blog post writing) 5297 and further filter results for devices last seen in the last day. If I had switched it to the not equal to version 5297 I would see all the devices not up to date. And it’s easy to change on the fly. Easier than refreshing the main device dashboard page to see these results as I was trying to do previously and that method made it hard to further filter.

    The exciting part

    Yes the best part is to set up a job in SimpleMDM that runs the scripts on the devices to refresh the value of XProtect (I have it set to recurring as well) and then watch the results roll into a dynamic group which has its members populate as the scripts runs and results report back. Easey peasy.

    Screenshot

    Addendum:

    Adding an example screenshot to show how you can change the filter from matches an exact value of XProtect, in this example, to “not equal to” to see all the devices that haven’t upgraded yet. It’s as easy as changing the filter and clicking on “staging filter changes” button. Et voilà !

    Updated: May 16, 2025 – 19h00 local time

  • SimpleMDM tricks and tips – part 1

    Custom Attributes

    Custom Attributes in SimpleMDM are a way to assign values in a few different cases. I will show one use case, scripting, and one example: checking the firewall.

    Note: for more fun use cases see Steve Quirke’s blog post, or the talk “Making SimpleMDM complicated” by Lucas Hall at MacDevOps:YVR in 2021 or even the official documentation.

    The goal: Checking the firewall

    I wanted to see the status of the macOS firewall in the device view dashboard. That’s so simple, right? Well, I wanted to see it at a glance for every device, and not have to go into each device entry to see if the firewall was enabled.

    Write a script:

    #!/bin/bash
    
    # Check firewall status
    firewall=$(defaults read /Library/Preferences/com.apple.alf globalstate)
    
    if [ "$firewall" = "1" ]; then
        echo "Firewall is enabled"
    elif [ "$firewall" = "0" ]; then
        # Set firewall status
        #defaults write /Library/Preferences/com.apple.alf globalstate -int 0
        echo "Firewall is NOT enabled"
    else
        echo "Unable to determine firewall status"
    fi
    
    
    

    Note: This is my script. This seems to work. If you have other working examples let me know.

    Add it to SimpleMDM scripts

    Add your script to the scripts section. Check the “Enable attribute support” check box.

    Add a custom attribute

    Set up a custom attribute that your script will populate with its variable later. I set up one for the firewall.

    Create a job

    Your script will need to run (once, or scheduled) to populate the value into the variable and into the custom attribute. Choose what script runs where on what Macs. And choose the custom attribute.

    And choose the custom attribute.

    Note: The cancel job if not started is helpful if your devices are not responding. And is a premonition to issues you may have with this feature and might give some flashbacks to the ancient way of using scripts in ARD (Apple Remote Desktop) to try to make changes, back in the days before MDM or good configuration management tools ie. munki puppet chef salt etc

    Dashboard Devices

    Add your custom attribute to the viewable columns in the Devices dashboard and your life will be full of joy. Seeing at a glance your scripts output variable as a custom attribute.

    And now I just have to recreate everything in MunkiReport as a custom attribute and then I’ll be good.

    Script debugging.

    Running scripts is all well and good until your devices don’t check in and don’t run the scripts for whatever reason. Rebooting the Mac helps. Refreshing the inventory in SimpleMDM helps (maybe) and well, you’ll see it’s like the old ARD scripts run ad hoc and you’ll wish for better tools like fully functional DDM (declarative device management) which is like configuration management of the days of old. Incorporate MunkiReport and Fleet’s osquery tools and save me the trouble of doing piecemeal.

    Enjoy the script output in the custom attributes for now and send me your awesome ideas for what to script next.

  • “I love SQL” and other lies you tell yourself

    Navigating a database to get what you want, that is the goal. Do you love it? No, but you do need to do it? Yes, to get the data. Remember the goal.

    Many popular (and many lesser known) applications use SQL, and SQLite in particular, to store data. That’s fine. That’s great. But unfortunately on occasion you need to go spelunking to find data you want and get it out. This is not a blog post about how much I love SQL (structured query language), because I do not love it. This is also not a blog post about how SQL is awesome, because I can’t say that. But what I hope to share are some tips and tricks for getting in and out with the data you want.

    Use an app – DB Browser for SQLite

    https://sqlitebrowser.org/dl/

    If you’re not a fan of SQL and you’ve got a need for DB data then this app will let you open a database and explore. This is a great app because you can see the tables and what’s in the Db which will no doubt help you late explore in Terminal or in a script. I personally need a visual map sometimes before I jump in. Exploring the Db in this app will also show you the arcane commands necessary to do the same in Terminal. You will be in awe of whomever decided to create this complicated series of commands which makes long insane Unix commands seem logical in comparison.

    You can use DB Browser to export a csv (comma separated values) for a spreadsheet or as JSON (JavaScript Object Notation) which all the cool kids like these days. Better start loving this. More on JSON and APIs in a future blog post. Its not XML, but it makes you wish it was.

    Use Terminal – Type the commands by yourself

    In Terminal we can tell sqlite we want to export a csv file of everything. Add a header and tell it to be in csv mode then SELECT everything.

    sqlite> .header on

    sqlite> .mode csv

    sqlite> .output export.csv

    sqlite> .quit

    Export just some the data as a CSV

    sqlite3 /path/to/the/database.db
    SQLite version 3.40.1 2022-12-28 14:03:47
    Enter “.help” for usage hints.
    sqlite> .header on
    sqlite> .mode csv
    sqlite> .output JustTheSelectFieldsPlease.csv
    sqlite> SELECT label, timeDated, fancyList, sillyList, boringFiles, indexName FROM tableName;
    sqlite> .quit

    Automate and Make a Script

    #!/bin/bash
    sqlite3 /path/to/the/database.db <<EOF
    .header on
    .mode csv
    .output JustTheSelectFieldsPlease.csv
    SELECT label, timeDated, fancyList, sillyList, boringFiles, indexName FROM tableName;
    .quit
    EOF

    UPDATE: You must check out Datasette!

    After posting this I was reminded of an app called Datasette which is truly remarkable and awesome. It’s also a pip install thing but I’ve been using the standalone Mac app which has everything self-contained.

    With Datasette it’s easy to load SQLite DBs directly and filtering out the tables I want by easily set conditions, which makes exporting a workable JSON or CSV file quite easy in one step. There’s also a small and lightweight web app called Datasette Lite to make installing and running Datasette extremely simple. Datasette has plugins too. A lot. More to say on those in a later post.