Add touchid_system_config and touchid_user_config tables#110
Add touchid_system_config and touchid_user_config tables#110robbiet480 wants to merge 10 commits into
Conversation
Two macOS Touch ID tables populated from bioutil(1) and ioreg(8):
- touchid_system_config: machine-wide Touch ID / Secure Enclave posture
(touchid_compatible, secure_enclave, touchid_enabled, touchid_unlock)
plus two sensor-presence columns:
* touchid_builtin -- a built-in Touch ID sensor exists (laptops),
detected via an AppleBiometricSensor IORegistry node.
* touchid_sensor_present -- a usable sensor exists: built-in OR an
attached external Touch ID accessory (e.g. a Magic Keyboard with
Touch ID), the latter detected via the AppleMesaAccessory class.
Why the new columns: touchid_compatible / touchid_enabled come from
bioutil's "Biometrics functionality" flag, which reports the on-die
Secure Enclave -- true on every Apple Silicon Mac, including a
keyboard-less Mac mini/Studio that has no fingerprint sensor at all.
touchid_sensor_present is the signal callers actually want for "can
this user enroll a fingerprint." AppleMesaAccessory is a capability
class, not a product-string match, so it is name-, transport-
(USB/Bluetooth), and localization-independent, and a non-Touch-ID
keyboard correctly reads as no sensor. Verified on hardware across
MacBook (built-in), Mac Studio + Touch ID keyboard, keyboard-less
Mac mini/Studio, and an iMac with a disconnected Touch ID keyboard.
- touchid_user_config: per-user Touch ID configuration and enrolled
fingerprint count (uid, fingerprints_registered, touchid_unlock,
touchid_applepay, effective_unlock, effective_applepay). Without a
WHERE uid = constraint it returns a row per real local account.
fingerprints_registered comes from `bioutil -c -s` (needs root, reads
all users at once); the config flags come from `bioutil -r` run in the
user's login session via launchctl asuser (needs the user logged in)
and are left NULL when unavailable rather than 0, so an enabled-but-
logged-out user is not misreported as disabled.
Both tables shell out via the injected utils.CmdRunner and are unit-tested
with MultiMockCmdRunner (no real binaries invoked); coverage 74.3%.
ioreg is parsed as plist (ioreg -a) per the alt_system_info convention.
Apple Silicon only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds new macOS Touch ID osquery tables that report system-wide sensor/SE posture and per-user Touch ID configuration, backed by bioutil and ioreg.
Changes:
- Introduces
touchid_system_configandtouchid_user_configtable implementations with parsing/helpers. - Adds unit tests and Bazel build targets for the new
touchidpackage. - Registers new tables in
main.go, documents them in the README, and bumps the project version.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tables/touchid/touchid.go | Implements Touch ID table schemas, generators, and command-output parsing. |
| tables/touchid/touchid_test.go | Adds unit tests covering parsers and table row construction via mocked runners. |
| tables/touchid/BUILD.bazel | Adds Bazel targets for the new Go library and tests. |
| main.go | Registers the new Touch ID tables in the osquery plugin list. |
| README.md | Documents the new tables and their behaviors/constraints. |
| VERSION | Bumps version to 1.5.0 to reflect new functionality. |
| BUILD.bazel | Adds the new touchid package to the root build dependencies. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Source secure_enclave from `sysctl -n hw.model` instead of `system_profiler SPiBridgeDataType`. Both return the SoC/model identifier (e.g. "Mac16,5"), but system_profiler can take seconds and added noticeable latency to every query; sysctl is near-instant. Drops the chipModelFromSPiBridge parser and its test. - Check bufio.Scanner.Err() in parseBioutil / parseFingerprintCounts / parseLocalUIDs, and enlarge the scan buffer (newLineScanner, 1 MiB cap) so a truncated read or an over-long line is treated as "unknown" (empty result) rather than silently producing a partial parse. - Rewrite TestColumns to compare full column-name slices via a columnNames helper, so a count/order mismatch is a clean assertion failure instead of an index-out-of-range panic. All three were raised by the Copilot reviewer on macadmins#110. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the previous Copilot round. The "return empty on scanner error" change conflated parse failure with valid-empty results: - parseFingerprintCounts now returns (counts, ok). GetUserConfigs sets countsKnown from ok, so a truncated/failed parse is treated as "unknown" rather than "every user has 0 enrolled fingerprints". - parseBioutil now returns (fields, ok). On a scanner error the caller skips populating the flag columns entirely (leaving them NULL) instead of running boolField over an empty map and reporting every flag as "0"/disabled. - Add nullableBoolField and use it for the per-user touchid_user_config flags (unlock / applepay / effective_*), so a flag bioutil did not emit (e.g. on a macOS version that omits it) is reported as unknown (NULL) rather than silently "0". The zero-fingerprint effective-flag workaround now only fires on flags that were actually present. - Add a sysctlPath constant for the hw.model lookup, matching bioutilPath / ioregPath / dsclPath. Raised by Copilot on macadmins#110. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TouchIDUserConfigGenerate gated all four flag columns on `Unlock != ""`, so a config where touchid_unlock is present but ApplePay/effective_* are absent (now possible since nullableBoolField returns "" for missing keys) would set those columns to "" — an invalid value for an IntegerColumn that should be NULL/omitted. Set each column independently, only when its value is known. Extracted the row-shaping into userConfigsToRows so the NULL-omission behavior is unit-testable; added tests for the partial-flags and all-known cases. Raised by Copilot on macadmins#110. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- touchid_compatible was set to "1" based on the *presence* of the "Biometrics functionality" field rather than its value, so a hypothetical "Biometrics functionality: 0" from bioutil would report compatible=1. Derive it from the value via boolField (same field that drives touchid_enabled). Added a test for the value=0 case. - Reword the minHumanUID/maxHumanUID comment so it matches the implemented inclusive [501, 60000] range (the old "60000+ are transient" wording was ambiguous about whether 60000 itself is included — it is). Raised by Copilot on macadmins#110. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
This PR is ready for human review |
|
I checked this against a Mac with no Touch ID sensor. The command behavior looks safe: For |
…-sensor signal Per @grahamgilbert's testing on a no-Touch-ID Mac: touchid_user_config still returns a row per local account (UID populated, Touch ID columns NULL) on such hardware, because user enumeration is independent of the sensor. Document this in the table's README notes and point callers at touchid_system_config.touchid_sensor_present = 0 as the authoritative "no usable Touch ID sensor" signal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for testing this on real no-Touch-ID hardware — that's exactly the case I most wanted validated, and your read matches the intent: empty Your point about |
|
BTW: As I said at the top of the PR, this is Apple Silicon only. I hope that's okay. I don't have a Intel Mac to test on anyway, but obviously if someone wanted to add support for Intel Macs in the future, that would be great. |
|
@grahamgilbert mind approving the workflows to make sure they all pass? |
No, this should be explicitly handled in the code. This isn’t something that should be in the readme. |
Per Graham's review: a Mac with no usable Touch ID sensor must be handled in code, not punted to callers via the README. touchid_user_config previously emitted a row per local account (UID populated, every Touch ID column NULL) on a keyboard-less Mac mini/Studio, because user enumeration via dscl is independent of the hardware. Callers then had to filter on touchid_system_config.touchid_sensor_present = 0 themselves. Now the table gates on sensor presence and returns no rows at all when no usable sensor exists. Extracted the built-in-OR-accessory detection into a shared SensorPresent helper so touchid_system_config and touchid_user_config agree on what "has a sensor" means; GetUserConfigs takes a sensorPresent bool and short-circuits to no rows when false; TouchIDUserConfigGenerate computes it via SensorPresent. Tests: TestGetUserConfigs_NoSensorReturnsNoRows and the constraint variant assert no rows on a sensor-less Mac (both with and without a WHERE uid =). README updated to document the no-rows behavior instead of caller-side filtering. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
You're right — moved it into the code (9f3cee0).
New tests cover the no-sensor case both with and without a uid constraint ( |
|
This one is ready to review and merge. |
Adds two macOS Touch ID tables, populated from
bioutil(1)andioreg(8). Apple Silicon only.touchid_system_configMachine-wide Touch ID / Secure Enclave posture:
touchid_compatiblebioutil -r -ssecure_enclavesysctl -n hw.modeltouchid_enabledbioutil -r -stouchid_unlockbioutil -r -stouchid_builtinioreg -a -r -c AppleBiometricSensortouchid_sensor_presentioreg -a -r -c AppleMesaAccessoryWhy the two new presence columns
touchid_compatible/touchid_enabledboth derive frombioutil'sBiometrics functionalityflag, which reports the on-die Secure Enclave — present on every Apple Silicon Mac, including a keyboard-less Mac mini or Mac Studio that has no fingerprint sensor at all. So neither column can answer "does this Mac actually have a usable Touch ID sensor."touchid_sensor_presentis the signal callers want for "can this user enroll a fingerprint":AppleBiometricSensorIORegistry nodes.AppleBiometricSensornode, but do register anAppleMesaAccessorynode. "Mesa" is Apple's codename for the Touch ID sensor subsystem; theAccessoryclass exists only when an external sensor is connected.AppleMesaAccessoryis a capability class, not a product-string match, so it is name-, transport- (USB or Bluetooth), and localization-independent — and a non-Touch-ID keyboard (old or new) correctly reads as no sensor. The sibling classesAppleMesaSEPDriver/AppleMesaResourcesare not usable for this; they are SEP scaffolding present on every Apple Silicon Mac.Verified on hardware across five states:
touchid_compatibletouchid_builtintouchid_sensor_presentNote
touchid_compatibleis1in every row — including the sensor-less ones — which is the whole reason the presence columns exist. The accessory half oftouchid_sensor_presentis a live signal: a sleeping/disconnected Touch ID keyboard reads0until it reconnects (it is the user's actual ability to use Touch ID right now).touchid_user_configPer-user Touch ID configuration and enrolled fingerprint count:
uid,fingerprints_registered,touchid_unlock,touchid_applepay,effective_unlock,effective_applepay. Without aWHERE uid =constraint, returns a row per real local account (uid 501–60000, enumerated viadscl . -list /Users UniqueID).No usable Touch ID sensor → no rows. User enumeration via
dsclis independent of the hardware, so a keyboard-less Mac mini/Studio would otherwise emit a NULL-filled row per local account. This table now gates on sensor presence (the same built-in-OR-accessoryioregcheck behindtouchid_system_config.touchid_sensor_present, factored into a sharedSensorPresenthelper) and returns no rows at all when there is no usable sensor — handled in code, not punted to callers. The gate short-circuits before anydscl/bioutilwork, so it holds even with an explicitWHERE uid =constraint.The two data sources have different access models:
fingerprints_registered←bioutil -c -s(needs root — the context an osquery extension runs in — and reads all users at once regardless of login state).bioutil -rrun in the target user's login session vialaunchctl asuser(needs the user logged in).When a column can't be read it is left NULL rather than
0, so an enabled-but-logged-out user is never misreported as disabled. There is also a workaround for a long-standingbioutilquirk where theeffective_*flags can report1with zero enrolled fingerprints — forced to0only when the count is known to be 0.Implementation notes
utils.CmdRunnerand are unit-tested withMultiMockCmdRunner— no real binaries are invoked in tests.ioregis parsed as plist (ioreg -a+micromdm/plist), matching thealt_system_infoconvention. Presence islen(plist array) > 0; an absent class yields empty output.main.go'sdarwinPlugins;BUILD.bazelfiles generated bygazelle;VERSIONbumped 1.4.1 → 1.5.0; README updated.Testing
bazel test //.../go test ./...→ all pass (includes no-sensor coverage fortouchid_user_config, both with and without a uid constraint)bazel build //:osquery-extension-mac-{arm,amd}+ linux-amd → all buildgolangci-lint run→ 0 issues;gofmtclean🤖 Generated with Claude Code