Author SHA1 Message Date
Wesley van TilburgandClaude Opus 5.5 111d3bea66 fedora-intune: give the Intune compliance check a Defender health snapshot
Build containers / fedora-cosmic (44) (pull_request) Canceled after 0s
Build containers / fedora-cosmic (rawhide) (pull_request) Canceled after 0s
Build containers / fedora-remote (44) (pull_request) Canceled after 0s
Build containers / fedora-intune (44) (pull_request) Canceled after 0s
Build containers / Compute changes (pull_request) Canceled after 9s
Build containers / Prune old releases and tags (pull_request) Skipped
Himmelblau runs the custom compliance script inside himmelblaud-tasks,
and Defender refuses to answer `mdatp health` there: the daemon can't
read /proc/<himmelblaud_tasks>/cmdline while checking the caller
("Permission denied" in microsoft_defender_err.log). The script saw no
daemon, and every Defender rule failed although Defender was healthy.

Add mdatp-health-snapshot.timer, which saves the health fields to
/run/mdatp-health every 5 minutes as root, outside that service. The
compliance script reads the snapshot when it's under 20 minutes old and
otherwise asks mdatp directly, so it still works on other distros.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 13:23:17 +00:00
Misthios 4d780e900a Merge pull request 'Misthios/fedora intune defender os version' (#5) from misthios/fedora-intune-defender-os-version into main
Build containers / Compute changes (push) Successful in 22s
Build containers / fedora-cosmic (44) (push) Skipped
Build containers / fedora-cosmic (rawhide) (push) Skipped
Build containers / fedora-remote (44) (push) Skipped
Build containers / fedora-intune (44) (push) Successful in 12m35s
Build containers / Prune old releases and tags (push) Successful in 26s
Reviewed-on: #5
2026-10-20 12:49:33 +00:00
Wesley van TilburgandClaude Opus 5.5 e8cfa8d4cf fedora-intune: don't repeat the version in the reported OS version
Build containers / fedora-cosmic (44) (pull_request) Canceled after 0s
Build containers / fedora-cosmic (rawhide) (pull_request) Canceled after 0s
Build containers / fedora-remote (44) (pull_request) Canceled after 0s
Build containers / fedora-intune (44) (pull_request) Canceled after 0s
Build containers / Compute changes (pull_request) Canceled after 13s
Build containers / Prune old releases and tags (pull_request) Skipped
libhimmelblau reports "PRETTY_NAME VERSION_ID" as the OS version, and
Fedora's PRETTY_NAME already contains the version, so Entra showed
"Fedora Linux 44.20261020.0 (Forty Four) 44.20261020.0". In the
os-release the Himmelblau daemons see, set PRETTY_NAME to NAME, giving
"Fedora Linux 44.20261020.0". Nothing else in Himmelblau reads
PRETTY_NAME.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 12:48:44 +00:00
Wesley van TilburgandClaude Opus 5.5 5504f4e7aa fedora-intune: install the Defender managed config when Defender starts
The managed config shipped in /etc never reached updated devices:
mdatp-firstboot creates /etc/opt/microsoft/mdatp on the device, so ostree's
/etc merge treats that directory as local and doesn't add new image files
under it. VM 123 had the file in /usr/etc but not in /etc, and Defender
kept its defaults.

Ship it in /usr/share/mdatp-managed/ and install it into
/etc/opt/microsoft/mdatp/managed/ from an ExecStartPre in mdatp.service.
This also applies changes from newer images and undoes local edits.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 12:48:44 +00:00
Misthios 561036582c Merge pull request 'Misthios/defender onboarding validation' (#4) from misthios/defender-onboarding-validation into main
Build containers / Compute changes (push) Successful in 21s
Build containers / fedora-cosmic (44) (push) Skipped
Build containers / fedora-cosmic (rawhide) (push) Skipped
Build containers / fedora-remote (44) (push) Skipped
Build containers / fedora-intune (44) (push) Successful in 12m40s
Build containers / Prune old releases and tags (push) Successful in 28s
Reviewed-on: #4
2026-10-20 12:23:30 +00:00
Wesley van TilburgandClaude Opus 5.5 86039eeb9d fedora-intune: enforce Defender protection settings
Defender's defaults left behavior monitoring off and every setting
unmanaged. Ship /etc/opt/microsoft/mdatp/managed/mdatp_managed.json:
real-time protection, behavior monitoring, PUA blocking, cloud-delivered
protection at the high block level, automatic definition updates and
sample submission, and scheduled scans (daily quick at 12:00, weekly full
on Wednesday 12:30, low priority). Users can't change managed settings.
The build fails if the file isn't valid JSON.

Network protection (Insiders channels only) and tamper protection
(audit-only preview on Linux) are left out.

The compliance script and rules now also check behavior monitoring and
cloud-delivered protection.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 12:23:30 +00:00
Wesley van TilburgandClaude Opus 5.5 3afef0d821 fedora-intune: validate the onboarding JSON in the Defender script
A paste into Intune that lost the first "{" line produced an unreadable
mdatp_onboard.json, and Defender reported "No license found". Check that
the pasted JSON parses and has onboardingInfo before writing it, and log
what to fix otherwise.

The placeholder also appeared twice, so a find-and-replace of it broke
the script; match it by prefix so it appears only once.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 12:23:30 +00:00
Misthios 07434cedab Merge pull request 'fedora-intune: grant sudo to the Linux admins Entra group' (#3) from misthios/fedora-intune-sudo-groups into main
Build containers / fedora-cosmic (44) (push) Canceled after 0s
Build containers / fedora-cosmic (rawhide) (push) Canceled after 0s
Build containers / fedora-remote (44) (push) Canceled after 0s
Build containers / fedora-intune (44) (push) Canceled after 0s
Build containers / Compute changes (push) Canceled after 12s
Build containers / Prune old releases and tags (push) Successful in 26s
Reviewed-on: #3
2026-10-20 12:22:57 +00:00
Wesley van TilburgandClaude Opus 5.5 0e76505715 fedora-intune: grant sudo to the Linux admins Entra group
Build containers / fedora-cosmic (44) (pull_request) Canceled after 0s
Build containers / fedora-cosmic (rawhide) (pull_request) Canceled after 0s
Build containers / fedora-remote (44) (pull_request) Canceled after 0s
Build containers / fedora-intune (44) (pull_request) Canceled after 0s
Build containers / Compute changes (pull_request) Canceled after 12s
Build containers / Prune old releases and tags (pull_request) Skipped
local_sudo_group was set to wheel but sudo_groups was empty, so no Entra
user ever got sudo. Members of the Entra group
c3b1cfcc-7834-47eb-948b-a22204276453 now get wheel at sign-in, and lose
it again after leaving the group.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 11:30:20 +00:00
Misthios f48a0ffaf6 Merge pull request 'Misthios/fedora intune os version defender' (#2) from misthios/fedora-intune-os-version-defender into main
Build containers / Compute changes (push) Successful in 20s
Build containers / fedora-intune (44) (push) Canceled after 0s
Build containers / fedora-cosmic (44) (push) Canceled after 33s
Build containers / fedora-cosmic (rawhide) (push) Canceled after 33s
Build containers / fedora-remote (44) (push) Canceled after 34s
Build containers / Prune old releases and tags (push) Successful in 24s
Reviewed-on: #2
2026-10-20 09:22:41 +00:00
12 changed files with 179 additions and 4 deletions

No files matched your search

+20 -1
View File
@@ -26,6 +26,23 @@ add-files:
- ["mdatp-firstboot.service", "/usr/lib/systemd/system/mdatp-firstboot.service"] - ["mdatp-firstboot.service", "/usr/lib/systemd/system/mdatp-firstboot.service"]
- ["mdatp-firstboot.conf", "/usr/lib/systemd/system/mdatp.service.d/10-firstboot.conf"] - ["mdatp-firstboot.conf", "/usr/lib/systemd/system/mdatp.service.d/10-firstboot.conf"]
- ["50-defender.preset", "/usr/lib/systemd/system-preset/50-defender.preset"] - ["50-defender.preset", "/usr/lib/systemd/system-preset/50-defender.preset"]
# Defender health for the Intune compliance script, which can't query
# Defender itself from inside himmelblaud-tasks; see the script.
- ["mdatp-health-snapshot", "/usr/libexec/mdatp-health-snapshot"]
- ["mdatp-health-snapshot.service", "/usr/lib/systemd/system/mdatp-health-snapshot.service"]
- ["mdatp-health-snapshot.timer", "/usr/lib/systemd/system/mdatp-health-snapshot.timer"]
# Enforced Defender settings (shown as "[managed]" in `mdatp health`; local
# changes are refused): real-time protection, behavior monitoring, PUA
# blocking, cloud protection at the "high" block level, automatic updates
# and sample submission, a daily quick scan at 12:00 and a weekly full scan
# on Wednesday 12:30 (low priority, up to 2 h later at random). Settings
# reference: https://learn.microsoft.com/defender-endpoint/linux-preferences
# Not set here: network protection (Insiders channels only) and tamper
# protection (audit-only preview on Linux).
- ["mdatp_managed.json", "/usr/share/mdatp-managed/mdatp_managed.json"]
# Installed into /etc/opt/microsoft/mdatp/managed/ whenever Defender starts;
# see the drop-in.
- ["mdatp-managed-config.conf", "/usr/lib/systemd/system/mdatp.service.d/20-managed-config.conf"]
postprocess: postprocess:
- | - |
@@ -42,5 +59,7 @@ postprocess:
[[ -f "${pp}" ]] && break [[ -f "${pp}" ]] && break
done done
semodule -n -i "${pp}" semodule -n -i "${pp}"
# Fail the build on a typo rather than ship a config Defender can't read.
python3 -m json.tool /usr/share/mdatp-managed/mdatp_managed.json > /dev/null
rpm -q --qf '%{POSTIN}' mdatp > /usr/libexec/mdatp-postinst rpm -q --qf '%{POSTIN}' mdatp > /usr/libexec/mdatp-postinst
chmod 0755 /usr/libexec/mdatp-firstboot /usr/libexec/mdatp-postinst chmod 0755 /usr/libexec/mdatp-firstboot /usr/libexec/mdatp-postinst /usr/libexec/mdatp-health-snapshot
@@ -2,3 +2,5 @@
enable mdatp-firstboot.service enable mdatp-firstboot.service
enable mdatp.service enable mdatp.service
enable mde_netfilter_v2.socket enable mde_netfilter_v2.socket
# Health snapshot for the Intune compliance script.
enable mdatp-health-snapshot.timer
@@ -5,16 +5,23 @@
# "44". Himmelblau reports VERSION_ID as the device's OS version and has no # "44". Himmelblau reports VERSION_ID as the device's OS version and has no
# setting for it. The rest of the system keeps the stock VERSION_ID, which # setting for it. The rest of the system keeps the stock VERSION_ID, which
# toolbox, flatpak and dnf expect to be the plain release. # toolbox, flatpak and dnf expect to be the plain release.
#
# PRETTY_NAME becomes just NAME: libhimmelblau reports "PRETTY_NAME VERSION_ID"
# as the OS version string, and Fedora's PRETTY_NAME already contains the
# version ("Fedora Linux 44.20261020.0 (Forty Four) 44.20261020.0"). Nothing
# else in Himmelblau reads PRETTY_NAME.
set -euo pipefail set -euo pipefail
SRC=/usr/lib/os-release SRC=/usr/lib/os-release
DEST=/run/himmelblau-os-release DEST=/run/himmelblau-os-release
version="$(. "${SRC}" && echo "${OSTREE_VERSION:-${IMAGE_VERSION:-}}")" version="$(. "${SRC}" && echo "${OSTREE_VERSION:-${IMAGE_VERSION:-}}")"
name="$(. "${SRC}" && echo "${NAME:-Linux}")"
tmp="$(mktemp "${DEST}.XXXXXX")" tmp="$(mktemp "${DEST}.XXXXXX")"
if [[ -n "${version}" ]]; then if [[ -n "${version}" ]]; then
sed "s/^VERSION_ID=.*/VERSION_ID=${version}/" "${SRC}" > "${tmp}" sed -e "s/^VERSION_ID=.*/VERSION_ID=${version}/" \
-e "s/^PRETTY_NAME=.*/PRETTY_NAME=\"${name}\"/" "${SRC}" > "${tmp}"
else else
# No image version (not an ostree/bootc deployment): pass it through as-is. # No image version (not an ostree/bootc deployment): pass it through as-is.
cat "${SRC}" > "${tmp}" cat "${SRC}" > "${tmp}"
@@ -11,6 +11,9 @@ apply_policy = true
# Fedora's admin group; members of sudo_groups (Entra group object IDs) are # Fedora's admin group; members of sudo_groups (Entra group object IDs) are
# added to it. # added to it.
local_sudo_group = wheel local_sudo_group = wheel
# Linux admins: members get wheel (sudo) at sign-in, and lose it again within
# local_groups_reconcile_interval after leaving the group.
sudo_groups = c3b1cfcc-7834-47eb-948b-a22204276453
# Home directories named after the account (e.g. /home/jdoe), with a UPN alias. # Home directories named after the account (e.g. /home/jdoe), with a UPN alias.
home_attr = CN home_attr = CN
home_alias = SPN home_alias = SPN
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Save `mdatp health` fields to /run/mdatp-health/<field> for the Intune
# custom compliance script (intune/defender-compliance.sh).
#
# That script runs inside himmelblaud-tasks.service, and Defender refuses to
# answer it there: the daemon checks the calling process tree, can't read
# /proc/<himmelblaud_tasks pid>/cmdline ("Permission denied" in
# microsoft_defender_err.log), and the script sees no daemon. This runs from a
# timer outside that service instead. Each file holds the raw
# `mdatp health --field` output; the directory is replaced as a whole.
set -euo pipefail
MDATP=/usr/bin/mdatp
DEST=/run/mdatp-health
FIELDS=(
healthy
licensed
org_id
real_time_protection_enabled
behavior_monitoring
cloud_enabled
definitions_status
definitions_updated_minutes_ago
app_version
)
tmp="$(mktemp -d "${DEST}.XXXXXX")"
trap 'rm -rf "${tmp}"' EXIT
for field in "${FIELDS[@]}"; do
"${MDATP}" health --field "${field}" > "${tmp}/${field}" 2>/dev/null || true
done
chmod 0755 "${tmp}"
chmod 0644 "${tmp}"/*
# Swap in the new snapshot; readers see either the old or the new one.
rm -rf "${DEST}.old"
[[ -d "${DEST}" ]] && mv "${DEST}" "${DEST}.old"
mv "${tmp}" "${DEST}"
rm -rf "${DEST}.old"
trap - EXIT
@@ -0,0 +1,7 @@
[Unit]
Description=Save Microsoft Defender health for the Intune compliance check
After=mdatp.service
[Service]
Type=oneshot
ExecStart=/usr/libexec/mdatp-health-snapshot
@@ -0,0 +1,9 @@
[Unit]
Description=Save Microsoft Defender health every 5 minutes
[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
[Install]
WantedBy=timers.target
@@ -0,0 +1,7 @@
# Install the image's enforced Defender settings each time Defender starts.
# The image can't ship them in /etc directly: mdatp-firstboot creates
# /etc/opt/microsoft/mdatp on the device, so on updates that directory counts
# as local and ostree's /etc merge never adds new files under it. Copying on
# every start also picks up changes from newer images and undoes local edits.
[Service]
ExecStartPre=+/usr/bin/install -D -Z -m 0644 /usr/share/mdatp-managed/mdatp_managed.json /etc/opt/microsoft/mdatp/managed/mdatp_managed.json
@@ -0,0 +1,29 @@
{
"antivirusEngine": {
"enforcementLevel": "real_time",
"behaviorMonitoring": "enabled",
"scanAfterDefinitionUpdate": true,
"scanArchives": true,
"disallowedThreatActions": ["allow", "restore"],
"threatTypeSettingsMergePolicy": "admin_only",
"threatTypeSettings": [
{ "key": "potentially_unwanted_application", "value": "block" },
{ "key": "archive_bomb", "value": "audit" }
],
"scheduledScan": "enabled"
},
"scheduledScan": {
"dailyConfiguration": { "timeOfDay": 720 },
"weeklyConfiguration": { "dayOfWeek": 4, "scanType": "full", "timeOfDay": 750 },
"lowPriorityScheduledScan": true,
"checkForDefinitionsUpdate": true,
"randomizeScanStartTime": 2
},
"cloudService": {
"enabled": true,
"diagnosticLevel": "required",
"automaticSampleSubmissionConsent": "safe",
"automaticDefinitionUpdateEnabled": true,
"cloudBlockLevel": "high"
}
}
@@ -84,6 +84,34 @@
} }
] ]
}, },
{
"SettingName": "DefenderBehaviorMonitoring",
"Operator": "IsEquals",
"DataType": "Boolean",
"Operand": true,
"MoreInfoUrl": "https://learn.microsoft.com/en-us/defender-endpoint/linux-install-manually",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "Behavior monitoring is off.",
"Description": "Microsoft Defender behavior monitoring must be enabled. Update the device to the latest image or contact IT."
}
]
},
{
"SettingName": "DefenderCloudProtection",
"Operator": "IsEquals",
"DataType": "Boolean",
"Operand": true,
"MoreInfoUrl": "https://learn.microsoft.com/en-us/defender-endpoint/linux-install-manually",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "Cloud-delivered protection is off.",
"Description": "Microsoft Defender cloud-delivered protection must be enabled. Update the device to the latest image or contact IT."
}
]
},
{ {
"SettingName": "DefenderDefinitionsUpToDate", "SettingName": "DefenderDefinitionsUpToDate",
"Operator": "IsEquals", "Operator": "IsEquals",
@@ -10,11 +10,21 @@
# instead of as a script error. # instead of as a script error.
MDATP=/usr/bin/mdatp MDATP=/usr/bin/mdatp
# Under Himmelblau, Defender won't answer `mdatp health` from this script, so
# a root timer may save the fields in /run/mdatp-health (the fedora-intune
# image does). Used when under 20 minutes old, otherwise mdatp is asked.
SNAPSHOT=/run/mdatp-health
if [[ -n "$(find "${SNAPSHOT}/healthy" -mmin -20 2>/dev/null)" ]]; then
raw_field() { cat "${SNAPSHOT}/$1" 2>/dev/null; }
else
raw_field() { "${MDATP}" health --field "$1" 2>/dev/null; }
fi
# Print one `mdatp health` field without quotes or a trailing " [managed]". # Print one `mdatp health` field without quotes or a trailing " [managed]".
# Unlicensed or unonboarded, mdatp prints "ATTENTION: ..." lines first. # Unlicensed or unonboarded, mdatp prints "ATTENTION: ..." lines first.
field() { field() {
"${MDATP}" health --field "$1" 2>/dev/null | grep -v '^ATTENTION' | tail -n1 \ raw_field "$1" | grep -v '^ATTENTION' | tail -n1 \
| sed -e 's/ \[managed\]$//' -e 's/^"//' -e 's/"$//' | sed -e 's/ \[managed\]$//' -e 's/^"//' -e 's/"$//'
} }
@@ -28,6 +38,8 @@ healthy=false
licensed=false licensed=false
onboarded=false onboarded=false
rtp=false rtp=false
behavior_monitoring=false
cloud_protection=false
definitions_ok=false definitions_ok=false
definitions_age=-1 definitions_age=-1
version="" version=""
@@ -40,6 +52,8 @@ if [[ -x "${MDATP}" ]]; then
healthy="$(bool "$(field healthy)")" healthy="$(bool "$(field healthy)")"
licensed="$(bool "$(field licensed)")" licensed="$(bool "$(field licensed)")"
rtp="$(bool "$(field real_time_protection_enabled)")" rtp="$(bool "$(field real_time_protection_enabled)")"
[[ "$(field behavior_monitoring)" == enabled ]] && behavior_monitoring=true
cloud_protection="$(bool "$(field cloud_enabled)")"
[[ -n "$(field org_id)" ]] && onboarded=true [[ -n "$(field org_id)" ]] && onboarded=true
[[ "$(field definitions_status)" == up_to_date ]] && definitions_ok=true [[ "$(field definitions_status)" == up_to_date ]] && definitions_ok=true
age="$(field definitions_updated_minutes_ago)" age="$(field definitions_updated_minutes_ago)"
@@ -57,6 +71,8 @@ cat <<EOF
"DefenderLicensed": ${licensed}, "DefenderLicensed": ${licensed},
"DefenderOnboarded": ${onboarded}, "DefenderOnboarded": ${onboarded},
"DefenderRealTimeProtection": ${rtp}, "DefenderRealTimeProtection": ${rtp},
"DefenderBehaviorMonitoring": ${behavior_monitoring},
"DefenderCloudProtection": ${cloud_protection},
"DefenderDefinitionsUpToDate": ${definitions_ok}, "DefenderDefinitionsUpToDate": ${definitions_ok},
"DefenderDefinitionsAgeDays": ${definitions_age}, "DefenderDefinitionsAgeDays": ${definitions_age},
"DefenderVersion": "${version}" "DefenderVersion": "${version}"
@@ -71,8 +71,16 @@ if ! wait_for_daemon; then
fi fi
if [[ -z "$(field org_id)" ]]; then if [[ -z "$(field org_id)" ]]; then
if [[ "${ONBOARDING_JSON}" == REPLACE_WITH_MDATP_ONBOARD_JSON ]]; then # Prefix match, so the placeholder appears only once in this file and a
# find-and-replace of it can't hit this line.
if [[ "${ONBOARDING_JSON}" == REPLACE_WITH_* ]]; then
log "Defender is not onboarded and this script has no onboarding JSON." log "Defender is not onboarded and this script has no onboarding JSON."
elif ! printf '%s\n' "${ONBOARDING_JSON}" \
| python3 -c 'import json, sys; assert "onboardingInfo" in json.load(sys.stdin)' 2>/dev/null; then
# A paste that lost the first "{" or last "}" line would otherwise leave
# Defender with an unreadable onboarding file ("No license found").
log "The onboarding JSON in this script is not valid; paste mdatp_onboard.json again, including its first '{' and last '}' lines."
exit 1
else else
log "Defender is not onboarded; writing ${ONBOARD_FILE}." log "Defender is not onboarded; writing ${ONBOARD_FILE}."
install -d -m 0755 "$(dirname "${ONBOARD_FILE}")" install -d -m 0755 "$(dirname "${ONBOARD_FILE}")"