diff --git a/images/fedora-intune/files/10-himmelblau-os-release.conf b/images/fedora-intune/files/10-himmelblau-os-release.conf new file mode 100644 index 0000000..758289a --- /dev/null +++ b/images/fedora-intune/files/10-himmelblau-os-release.conf @@ -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 diff --git a/images/fedora-intune/files/himmelblau-os-release b/images/fedora-intune/files/himmelblau-os-release new file mode 100644 index 0000000..06d015b --- /dev/null +++ b/images/fedora-intune/files/himmelblau-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}" diff --git a/images/fedora-intune/files/himmelblau-os-release.service b/images/fedora-intune/files/himmelblau-os-release.service new file mode 100644 index 0000000..83aa21e --- /dev/null +++ b/images/fedora-intune/files/himmelblau-os-release.service @@ -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 diff --git a/images/fedora-intune/himmelblau.yaml b/images/fedora-intune/himmelblau.yaml index b8b8e96..4f378d3 100644 --- a/images/fedora-intune/himmelblau.yaml +++ b/images/fedora-intune/himmelblau.yaml @@ -25,6 +25,11 @@ add-files: - ["himmelblau.conf", "/etc/himmelblau/himmelblau.conf"] - ["50-himmelblau.preset", "/usr/lib/systemd/system-preset/50-himmelblau.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: # The package's %post selects its authselect vendor profile but drops @@ -38,6 +43,7 @@ postprocess: getent passwd "${user}" >/dev/null done 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 # nothing in a compose; do it here. diff --git a/images/fedora-intune/intune/defender-compliance.json b/images/fedora-intune/intune/defender-compliance.json new file mode 100644 index 0000000..da37223 --- /dev/null +++ b/images/fedora-intune/intune/defender-compliance.json @@ -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." + } + ] + } + ] +} diff --git a/images/fedora-intune/intune/defender-compliance.sh b/images/fedora-intune/intune/defender-compliance.sh new file mode 100755 index 0000000..f5f3de7 --- /dev/null +++ b/images/fedora-intune/intune/defender-compliance.sh @@ -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 <