Skip to content

Commit fb88cf0

Browse files
authored
Merge pull request #246 from theneiljohnson/neils-updates
Add Intune Agent timing custom attributes
2 parents dae2527 + eb511eb commit fb88cf0

File tree

3 files changed

+539
-0
lines changed

3 files changed

+539
-0
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Intune Agent Timing Custom Attributes for macOS
2+
3+
This folder contains two Microsoft Intune custom attribute scripts that measure how long it takes a macOS device to move from initial MDM profile installation to key Intune agent milestones.
4+
5+
Both scripts are designed to return a single value suitable for Intune custom attribute ingestion:
6+
7+
- A decimal number of seconds when the required markers are found
8+
- `unknown` when the relevant historical log entries are no longer available
9+
10+
## Included Scripts
11+
12+
| Script | Purpose | Data sources | Default output |
13+
|---|---|---|---|
14+
| `intune_agent_install_time_from_mdm.sh` | Measures time from MDM profile installation to Microsoft Intune Agent installation | Unified log (`mdmclient`, `appstored`) | Seconds or `unknown` |
15+
| `intune_agent_health_time_from_mdm.sh` | Measures time from MDM profile installation to the first healthy Intune agent marker | Unified log plus `/Library/Logs/Microsoft/Intune/IntuneMDMDaemon *.log` | Seconds or `unknown` |
16+
17+
## What Each Script Looks For
18+
19+
### `intune_agent_install_time_from_mdm.sh`
20+
21+
Start marker:
22+
- `mdmclient` log entry showing installation of the Microsoft MDM management profile
23+
24+
End marker:
25+
- Preferred: `Application was installed at:` for `com.microsoft.intuneMDMAgent`
26+
- Fallback: `installClientDidFinish`
27+
28+
### `intune_agent_health_time_from_mdm.sh`
29+
30+
Start marker:
31+
- `mdmclient` log entry showing installation of the Microsoft MDM management profile
32+
33+
End marker priority:
34+
1. `HealthCheckWorkflow | Completed health check Domain: regular`
35+
2. `VerifyEnrollmentStatus | Successfully verified enrollment status.`
36+
3. `VerifyEnrollmentStatus | Successfully verified device status.`
37+
4. `VerifyEnrollmentStatus | Retrieved enrollment info.`
38+
39+
## Requirements
40+
41+
- macOS device with Microsoft Intune enrollment activity in the local logs
42+
- Access to unified logs via `/usr/bin/log`
43+
- For the health script, Intune agent log files under `/Library/Logs/Microsoft/Intune`
44+
45+
## Usage
46+
47+
These scripts are intended for Microsoft Intune custom attributes and should be uploaded without additional arguments.
48+
49+
### Local validation
50+
51+
Run locally to verify the current output:
52+
53+
```bash
54+
./intune_agent_install_time_from_mdm.sh
55+
./intune_agent_health_time_from_mdm.sh
56+
```
57+
58+
Use verbose mode when you want to see the markers that were selected:
59+
60+
```bash
61+
./intune_agent_install_time_from_mdm.sh --last 7d --verbose
62+
./intune_agent_health_time_from_mdm.sh --last 7d --verbose
63+
```
64+
65+
`--last` accepts values supported by `log show --last`, such as `12h`, `7d`, or `30d`.
66+
67+
## Intune Notes
68+
69+
- Keep the scripts in their default output mode for custom attribute collection
70+
- Do **not** use `--verbose` in Intune, because verbose mode prints multiple lines for troubleshooting
71+
- If the required historical markers have already rolled out of the available logs, the script returns `unknown` instead of failing
72+
73+
## Example Output
74+
75+
```text
76+
482.153
77+
```
78+
79+
or
80+
81+
```text
82+
unknown
83+
```
84+
85+
## Troubleshooting
86+
87+
If a script returns `unknown`, common reasons include:
88+
89+
- The requested lookback window does not reach the original enrollment event
90+
- Relevant unified log entries have already aged out
91+
- For the health script, the Intune daemon log files are unavailable or no healthy marker exists after the MDM install event
92+
93+
Increase the lookback window and run with `--verbose` locally to see which markers were found.
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
#!/bin/zsh
2+
# shellcheck shell=zsh
3+
#
4+
# Script Name: intune_agent_health_time_from_mdm.sh
5+
# Description : Returns the elapsed time in seconds between Microsoft MDM
6+
# profile installation and the first healthy Microsoft Intune
7+
# agent marker.
8+
# Usage : ./intune_agent_health_time_from_mdm.sh [--last <window>] [--verbose]
9+
# Default : Searches the last 30d of unified and Intune agent logs.
10+
# Dependencies: /usr/bin/log, /bin/date, /usr/bin/awk, /usr/bin/find,
11+
# /usr/bin/sort, /Library/Logs/Microsoft/Intune
12+
# Output : Prints elapsed seconds as a decimal value, or "unknown" when
13+
# the required markers are unavailable.
14+
15+
setopt errexit nounset pipefail
16+
17+
LOOKBACK="30d"
18+
VERBOSE=0
19+
LOG_DIR="/Library/Logs/Microsoft/Intune"
20+
LOCAL_TZ_OFFSET="$(date +%z)"
21+
MDM_PREDICATE='process == "mdmclient" AND eventMessage CONTAINS[c] "Installed configuration profile: Management Profile (Microsoft.Profiles.MDM"'
22+
23+
MDM_TS=""
24+
MDM_MS=""
25+
MDM_LINE=""
26+
HEALTH_TS=""
27+
HEALTH_MS=""
28+
HEALTH_LINE=""
29+
HEALTH_FILE=""
30+
HEALTH_LABEL=""
31+
32+
typeset -A FALLBACK_TS FALLBACK_MS FALLBACK_LINE FALLBACK_FILE
33+
34+
usage() {
35+
cat <<'EOF'
36+
Usage: intune_agent_health_time_from_mdm.sh [--last <window>] [--verbose]
37+
38+
Returns the time in seconds from the Microsoft MDM management profile install to
39+
Intune agent health.
40+
41+
If the required historical markers are no longer available, the script prints
42+
"unknown" instead of failing.
43+
44+
Primary healthy marker:
45+
HealthCheckWorkflow | Completed health check Domain: regular
46+
47+
Fallbacks:
48+
- VerifyEnrollmentStatus | Successfully verified enrollment status.
49+
- VerifyEnrollmentStatus | Successfully verified device status.
50+
- VerifyEnrollmentStatus | Retrieved enrollment info.
51+
EOF
52+
}
53+
54+
parse_args() {
55+
while (( $# > 0 )); do
56+
case "$1" in
57+
--last)
58+
(( $# >= 2 )) || { print -u2 -- "Missing value for --last"; exit 2; }
59+
LOOKBACK="$2"
60+
shift 2
61+
;;
62+
-v|--verbose)
63+
VERBOSE=1
64+
shift
65+
;;
66+
-h|--help)
67+
usage
68+
exit 0
69+
;;
70+
*)
71+
print -u2 -- "Unknown option: $1"
72+
exit 2
73+
;;
74+
esac
75+
done
76+
}
77+
78+
extract_unified_ts() {
79+
emulate -L zsh
80+
local line="$1"
81+
local -a fields
82+
fields=(${=line})
83+
(( ${#fields} >= 2 )) || return 1
84+
print -r -- "${fields[1]} ${fields[2]}"
85+
}
86+
87+
extract_pipe_ts() {
88+
emulate -L zsh
89+
local line="$1"
90+
local ts="${line%% |*}"
91+
[[ "$ts" == "$line" ]] && return 1
92+
print -r -- "$ts"
93+
}
94+
95+
to_epoch_ms() {
96+
emulate -L zsh
97+
local ts="$1"
98+
local re='^([0-9]{4}-[0-9]{2}-[0-9]{2}) ([0-9]{2}:[0-9]{2}:[0-9]{2})([.:]([0-9]{1,6}))?([+-][0-9]{4})?$'
99+
local date_part time_part frac tz epoch
100+
101+
if [[ ! "$ts" =~ $re ]]; then
102+
return 1
103+
fi
104+
105+
date_part="$match[1]"
106+
time_part="$match[2]"
107+
frac="${match[4]:-0}"
108+
tz="${match[5]:-$LOCAL_TZ_OFFSET}"
109+
frac="${frac}000"
110+
frac="${frac[1,3]}"
111+
112+
if ! epoch="$(date -j -f "%Y-%m-%d %H:%M:%S %z" "$date_part $time_part $tz" +%s 2>/dev/null)"; then
113+
return 1
114+
fi
115+
116+
print -r -- $(( epoch * 1000 + 10#$frac ))
117+
}
118+
119+
format_seconds_ms() {
120+
emulate -L zsh
121+
local value="$1"
122+
/usr/bin/awk -v ms="$value" 'BEGIN { printf "%.3f\n", ms / 1000 }'
123+
}
124+
125+
emit_unknown() {
126+
emulate -L zsh
127+
local reason="$1"
128+
129+
if (( VERBOSE )); then
130+
print "Result : unknown"
131+
print "Reason : $reason"
132+
if [[ -n "$MDM_TS" ]]; then
133+
print "MDM marker : $MDM_TS"
134+
print "$MDM_LINE"
135+
fi
136+
if [[ -n "$HEALTH_TS" ]]; then
137+
print
138+
print "Health marker: $HEALTH_TS"
139+
print "$HEALTH_LINE"
140+
print "Log file : $HEALTH_FILE"
141+
print "Marker used : $HEALTH_LABEL"
142+
fi
143+
else
144+
print "unknown"
145+
fi
146+
}
147+
148+
find_mdm_marker() {
149+
emulate -L zsh
150+
local line ts
151+
152+
line="$(/usr/bin/log show --style compact --info --last "$LOOKBACK" --predicate "$MDM_PREDICATE" 2>/dev/null | /usr/bin/grep 'Installed configuration profile: Management Profile (Microsoft.Profiles.MDM' | tail -1)"
153+
[[ -n "$line" ]] || return 1
154+
ts="$(extract_unified_ts "$line")" || return 1
155+
MDM_TS="$ts"
156+
MDM_MS="$(to_epoch_ms "$ts")" || return 1
157+
MDM_LINE="$line"
158+
}
159+
160+
record_fallback_once() {
161+
emulate -L zsh
162+
local key="$1"
163+
local ts="$2"
164+
local ms="$3"
165+
local file="$4"
166+
local line="$5"
167+
[[ -n ${FALLBACK_TS[$key]-} ]] && return 0
168+
FALLBACK_TS[$key]="$ts"
169+
FALLBACK_MS[$key]="$ms"
170+
FALLBACK_FILE[$key]="$file"
171+
FALLBACK_LINE[$key]="$line"
172+
}
173+
174+
find_health_marker() {
175+
emulate -L zsh
176+
local file line ts ms
177+
178+
while IFS= read -r file; do
179+
while IFS= read -r line; do
180+
ts="$(extract_pipe_ts "$line")" || continue
181+
ms="$(to_epoch_ms "$ts")" || continue
182+
(( ms >= MDM_MS )) || continue
183+
184+
if [[ "$line" == *"HealthCheckWorkflow | Completed health check Domain: regular"* ]]; then
185+
HEALTH_TS="$ts"
186+
HEALTH_MS="$ms"
187+
HEALTH_FILE="$file"
188+
HEALTH_LINE="$line"
189+
HEALTH_LABEL="Completed regular health check"
190+
return 0
191+
fi
192+
193+
if [[ "$line" == *"VerifyEnrollmentStatus | Successfully verified enrollment status."* ]]; then
194+
record_fallback_once verify_enrollment "$ts" "$ms" "$file" "$line"
195+
elif [[ "$line" == *"VerifyEnrollmentStatus | Successfully verified device status."* ]]; then
196+
record_fallback_once verify_device "$ts" "$ms" "$file" "$line"
197+
elif [[ "$line" == *"VerifyEnrollmentStatus | Retrieved enrollment info."* ]]; then
198+
record_fallback_once enrollment_info "$ts" "$ms" "$file" "$line"
199+
fi
200+
done < "$file"
201+
done < <(/usr/bin/find "$LOG_DIR" -maxdepth 1 -type f -name 'IntuneMDMDaemon *.log' -print | /usr/bin/sort)
202+
203+
if [[ -n ${FALLBACK_TS[verify_enrollment]-} ]]; then
204+
HEALTH_TS="${FALLBACK_TS[verify_enrollment]}"
205+
HEALTH_MS="${FALLBACK_MS[verify_enrollment]}"
206+
HEALTH_FILE="${FALLBACK_FILE[verify_enrollment]}"
207+
HEALTH_LINE="${FALLBACK_LINE[verify_enrollment]}"
208+
HEALTH_LABEL="Successfully verified enrollment status"
209+
return 0
210+
fi
211+
212+
if [[ -n ${FALLBACK_TS[verify_device]-} ]]; then
213+
HEALTH_TS="${FALLBACK_TS[verify_device]}"
214+
HEALTH_MS="${FALLBACK_MS[verify_device]}"
215+
HEALTH_FILE="${FALLBACK_FILE[verify_device]}"
216+
HEALTH_LINE="${FALLBACK_LINE[verify_device]}"
217+
HEALTH_LABEL="Successfully verified device status"
218+
return 0
219+
fi
220+
221+
if [[ -n ${FALLBACK_TS[enrollment_info]-} ]]; then
222+
HEALTH_TS="${FALLBACK_TS[enrollment_info]}"
223+
HEALTH_MS="${FALLBACK_MS[enrollment_info]}"
224+
HEALTH_FILE="${FALLBACK_FILE[enrollment_info]}"
225+
HEALTH_LINE="${FALLBACK_LINE[enrollment_info]}"
226+
HEALTH_LABEL="Retrieved enrollment info"
227+
return 0
228+
fi
229+
230+
return 1
231+
}
232+
233+
main() {
234+
parse_args "$@"
235+
find_mdm_marker || { emit_unknown "Unable to find the MDM management profile install event in the requested log window."; return 0; }
236+
find_health_marker || { emit_unknown "Unable to find a health marker after the MDM profile install in the available Intune logs."; return 0; }
237+
238+
local delta_ms=$(( HEALTH_MS - MDM_MS ))
239+
(( delta_ms >= 0 )) || { emit_unknown "Healthy marker occurred before the MDM profile install, so the elapsed time cannot be trusted."; return 0; }
240+
241+
if (( VERBOSE )); then
242+
print "MDM marker : $MDM_TS"
243+
print "$MDM_LINE"
244+
print
245+
print "Health marker: $HEALTH_TS"
246+
print "$HEALTH_LINE"
247+
print "Log file : $HEALTH_FILE"
248+
print "Marker used : $HEALTH_LABEL"
249+
print
250+
print "Seconds : $(format_seconds_ms "$delta_ms")"
251+
return 0
252+
fi
253+
254+
format_seconds_ms "$delta_ms"
255+
}
256+
257+
main "$@"

0 commit comments

Comments
 (0)