fedora-intune: report full image version to Intune, Defender compliance script

Himmelblau reports VERSION_ID from /etc/os-release as the device OS version,
so Intune showed "44". Give himmelblaud and himmelblaud-tasks their own
os-release (bind-mounted in their mount namespace only) with VERSION_ID set to
OSTREE_VERSION, e.g. 44.20261019.0. The rest of the system keeps the stock
VERSION_ID that toolbox, flatpak and dnf expect.

Add an Intune custom compliance discovery script and rules for Microsoft
Defender for Endpoint (installed, running, onboarded, licensed, healthy,
real-time protection, definitions up to date). These are uploaded to Intune,
not shipped in the image.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wesley van TilburgandClaude Opus 5.5 committed 2026-10-20 09:13:03 +00:00
1 parent 350b381200
commit 75c3b502d7
6 files changed
+214

No files matched your search

@@ -0,0 +1,9 @@
# Report the full image version (VERSION_ID=44.20261019.0) to Entra ID and
# Intune instead of "44"; see /usr/libexec/himmelblau-os-release. Only this
# service's mount namespace sees the replacement os-release.
[Unit]
Requires=himmelblau-os-release.service
After=himmelblau-os-release.service
[Service]
BindReadOnlyPaths=/run/himmelblau-os-release:/etc/os-release
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Write the os-release that the Himmelblau daemons see (10-os-release.conf):
# the image's /usr/lib/os-release with VERSION_ID set to the full image version
# (OSTREE_VERSION, e.g. 44.20261019.0), so Intune shows the build and not just
# "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
# toolbox, flatpak and dnf expect to be the plain release.
set -euo pipefail
SRC=/usr/lib/os-release
DEST=/run/himmelblau-os-release
version="$(. "${SRC}" && echo "${OSTREE_VERSION:-${IMAGE_VERSION:-}}")"
tmp="$(mktemp "${DEST}.XXXXXX")"
if [[ -n "${version}" ]]; then
sed "s/^VERSION_ID=.*/VERSION_ID=${version}/" "${SRC}" > "${tmp}"
else
# No image version (not an ostree/bootc deployment): pass it through as-is.
cat "${SRC}" > "${tmp}"
fi
chmod 0644 "${tmp}"
mv -f "${tmp}" "${DEST}"
@@ -0,0 +1,10 @@
[Unit]
Description=Generate the os-release reported by Himmelblau to Intune
DefaultDependencies=no
After=local-fs.target
Before=himmelblaud.service himmelblaud-tasks.service
[Service]
Type=oneshot
ExecStart=/usr/libexec/himmelblau-os-release
RemainAfterExit=yes
+6
View File
@@ -25,6 +25,11 @@ add-files:
- ["himmelblau.conf", "/etc/himmelblau/himmelblau.conf"] - ["himmelblau.conf", "/etc/himmelblau/himmelblau.conf"]
- ["50-himmelblau.preset", "/usr/lib/systemd/system-preset/50-himmelblau.preset"] - ["50-himmelblau.preset", "/usr/lib/systemd/system-preset/50-himmelblau.preset"]
- ["50-himmelblau-user.preset", "/usr/lib/systemd/user-preset/50-himmelblau-user.preset"] - ["50-himmelblau-user.preset", "/usr/lib/systemd/user-preset/50-himmelblau-user.preset"]
# Report the full image version (44.20261019.0) as the OS version in Intune.
- ["himmelblau-os-release", "/usr/libexec/himmelblau-os-release"]
- ["himmelblau-os-release.service", "/usr/lib/systemd/system/himmelblau-os-release.service"]
- ["10-himmelblau-os-release.conf", "/usr/lib/systemd/system/himmelblaud.service.d/10-os-release.conf"]
- ["10-himmelblau-os-release.conf", "/usr/lib/systemd/system/himmelblaud-tasks.service.d/10-os-release.conf"]
postprocess: postprocess:
# The package's %post selects its authselect vendor profile but drops # The package's %post selects its authselect vendor profile but drops
@@ -38,6 +43,7 @@ postprocess:
getent passwd "${user}" >/dev/null getent passwd "${user}" >/dev/null
done done
grep -q pam_himmelblau.so /etc/pam.d/system-auth grep -q pam_himmelblau.so /etc/pam.d/system-auth
chmod 0755 /usr/libexec/himmelblau-os-release
# himmelblau-selinux builds and loads its module in %posttrans, which does # himmelblau-selinux builds and loads its module in %posttrans, which does
# nothing in a compose; do it here. # nothing in a compose; do it here.
@@ -0,0 +1,102 @@
{
"Rules": [
{
"SettingName": "DefenderInstalled",
"Operator": "IsEquals",
"DataType": "Boolean",
"Operand": true,
"MoreInfoUrl": "https://learn.microsoft.com/en-us/defender-endpoint/linux-install-manually",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "Microsoft Defender is not installed.",
"Description": "Microsoft Defender for Endpoint (mdatp) must be installed on this device."
}
]
},
{
"SettingName": "DefenderRunning",
"Operator": "IsEquals",
"DataType": "Boolean",
"Operand": true,
"MoreInfoUrl": "https://learn.microsoft.com/en-us/defender-endpoint/linux-install-manually",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "Microsoft Defender is not running.",
"Description": "The Microsoft Defender service (mdatp.service) is not responding. Restart the device or contact IT."
}
]
},
{
"SettingName": "DefenderOnboarded",
"Operator": "IsEquals",
"DataType": "Boolean",
"Operand": true,
"MoreInfoUrl": "https://learn.microsoft.com/en-us/defender-endpoint/linux-install-manually",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "Microsoft Defender is not onboarded.",
"Description": "This device is not onboarded to your organization's Microsoft Defender tenant. Contact IT."
}
]
},
{
"SettingName": "DefenderLicensed",
"Operator": "IsEquals",
"DataType": "Boolean",
"Operand": true,
"MoreInfoUrl": "https://learn.microsoft.com/en-us/defender-endpoint/linux-install-manually",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "Microsoft Defender is not licensed.",
"Description": "Microsoft Defender has no valid license on this device. Contact IT."
}
]
},
{
"SettingName": "DefenderHealthy",
"Operator": "IsEquals",
"DataType": "Boolean",
"Operand": true,
"MoreInfoUrl": "https://learn.microsoft.com/en-us/defender-endpoint/linux-install-manually",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "Microsoft Defender reports a problem.",
"Description": "Run 'mdatp health' to see the reported issues, or contact IT."
}
]
},
{
"SettingName": "DefenderRealTimeProtection",
"Operator": "IsEquals",
"DataType": "Boolean",
"Operand": true,
"MoreInfoUrl": "https://learn.microsoft.com/en-us/defender-endpoint/linux-install-manually",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "Real-time protection is off.",
"Description": "Microsoft Defender real-time protection must be enabled."
}
]
},
{
"SettingName": "DefenderDefinitionsUpToDate",
"Operator": "IsEquals",
"DataType": "Boolean",
"Operand": true,
"MoreInfoUrl": "https://learn.microsoft.com/en-us/defender-endpoint/linux-install-manually",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "Microsoft Defender definitions are out of date.",
"Description": "Connect the device to the network so Microsoft Defender can update its security intelligence."
}
]
}
]
}
+64
View File
@@ -0,0 +1,64 @@
#!/bin/bash
# Intune custom compliance discovery script for Linux: is Microsoft Defender
# for Endpoint (mdatp) installed, onboarded, licensed and protecting?
#
# Upload in Intune > Devices > Compliance > Scripts (Linux), and pair it with
# defender-compliance.json in a Linux compliance policy (Custom compliance).
# Himmelblau runs it as root inside himmelblaud-tasks.service and sends the
# JSON on stdout to Intune, which evaluates it against the rules. Every setting
# is always emitted, so a missing or stopped Defender reads as non-compliant
# instead of as a script error.
MDATP=/usr/bin/mdatp
# Print one `mdatp health` field without quotes or a trailing " [managed]".
# Unlicensed or unonboarded, mdatp prints "ATTENTION: ..." lines first.
field() {
"${MDATP}" health --field "$1" 2>/dev/null | grep -v '^ATTENTION' | tail -n1 \
| sed -e 's/ \[managed\]$//' -e 's/^"//' -e 's/"$//'
}
bool() {
[[ "$1" == true ]] && echo true || echo false
}
installed=false
running=false
healthy=false
licensed=false
onboarded=false
rtp=false
definitions_ok=false
definitions_age=-1
version=""
if [[ -x "${MDATP}" ]]; then
installed=true
if [[ "$(field healthy)" =~ ^(true|false)$ ]]; then
# The daemon answered.
running=true
healthy="$(bool "$(field healthy)")"
licensed="$(bool "$(field licensed)")"
rtp="$(bool "$(field real_time_protection_enabled)")"
[[ -n "$(field org_id)" ]] && onboarded=true
[[ "$(field definitions_status)" == up_to_date ]] && definitions_ok=true
age="$(field definitions_updated_minutes_ago)"
[[ "${age}" =~ ^[0-9]+$ ]] && definitions_age=$(( age / 1440 ))
version="$(field app_version)"
version="${version//[^0-9A-Za-z._-]/}"
fi
fi
cat <<EOF
{
"DefenderInstalled": ${installed},
"DefenderRunning": ${running},
"DefenderHealthy": ${healthy},
"DefenderLicensed": ${licensed},
"DefenderOnboarded": ${onboarded},
"DefenderRealTimeProtection": ${rtp},
"DefenderDefinitionsUpToDate": ${definitions_ok},
"DefenderDefinitionsAgeDays": ${definitions_age},
"DefenderVersion": "${version}"
}
EOF