From 6afa1d21e68368e43165b2ddc951aae12bdd8bc4 Mon Sep 17 00:00:00 2001 From: Wesley van Tilburg Date: Tue, 20 Oct 2026 09:15:28 +0000 Subject: [PATCH] fedora-intune: cron for Intune config scripts, Defender remediation script Himmelblau installs Intune Linux custom configuration scripts as /etc/cron.d jobs, and the image had no cron, so a script policy failed to apply. Add cronie and enable crond. Add a custom configuration script that repairs Microsoft Defender on its schedule: finishes first-boot setup, restarts a stopped daemon, onboards with the tenant's mdatp_onboard.json (pasted into the script in Intune), and re-enables real-time protection and updates definitions when needed. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../fedora-intune/files/50-himmelblau.preset | 2 + images/fedora-intune/himmelblau.yaml | 3 + .../intune/defender-remediation.sh | 102 ++++++++++++++++++ 3 files changed, 107 insertions(+) create mode 100755 images/fedora-intune/intune/defender-remediation.sh diff --git a/images/fedora-intune/files/50-himmelblau.preset b/images/fedora-intune/files/50-himmelblau.preset index 9822af2..a86eaf7 100644 --- a/images/fedora-intune/files/50-himmelblau.preset +++ b/images/fedora-intune/files/50-himmelblau.preset @@ -2,3 +2,5 @@ enable himmelblaud.service enable himmelblaud-tasks.service enable himmelblau-hsm-pin-init.service +# Runs the Intune custom configuration scripts Himmelblau puts in /etc/cron.d. +enable crond.service diff --git a/images/fedora-intune/himmelblau.yaml b/images/fedora-intune/himmelblau.yaml index 4f378d3..f395150 100644 --- a/images/fedora-intune/himmelblau.yaml +++ b/images/fedora-intune/himmelblau.yaml @@ -20,6 +20,8 @@ packages: # himmelblau-selinux compiles its policy module at install time. - make - selinux-policy-devel + # Intune custom configuration scripts are installed as /etc/cron.d jobs. + - cronie add-files: - ["himmelblau.conf", "/etc/himmelblau/himmelblau.conf"] @@ -44,6 +46,7 @@ postprocess: done grep -q pam_himmelblau.so /etc/pam.d/system-auth chmod 0755 /usr/libexec/himmelblau-os-release + test -d /etc/cron.d # 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-remediation.sh b/images/fedora-intune/intune/defender-remediation.sh new file mode 100755 index 0000000..7f8c75c --- /dev/null +++ b/images/fedora-intune/intune/defender-remediation.sh @@ -0,0 +1,102 @@ +#!/bin/bash +# Intune custom configuration script for Linux: bring Microsoft Defender for +# Endpoint (mdatp) back to the state defender-compliance.sh checks for. +# +# Intune has no "run only when non-compliant" trigger for Linux, so this runs +# on the policy's schedule and only acts on what is wrong; on a healthy device +# it only reads `mdatp health`. Himmelblau installs it as an /etc/cron.d job. +# +# Intune > Devices > Configuration > Create > Linux > Settings catalog > +# Custom Configuration: +# Execution context: root +# Execution frequency: every 1 hour (or 15 minutes while rolling out) +# Execution retries: 1 +# Script: this file, with ONBOARDING_JSON filled in +# +# Exit status is non-zero when Defender is still not healthy afterwards. + +# Contents of mdatp_onboard.json from the Defender portal: Settings > Endpoints +# > Onboarding > Linux Server > "Local Script (Python)". The downloaded +# MicrosoftDefenderATPOnboardingLinuxServer.py writes this file to +# /etc/opt/microsoft/mdatp/ on the machine it runs on; paste that file's +# contents here. Leave the placeholder to skip onboarding. +ONBOARDING_JSON=$(cat <<'JSON' +REPLACE_WITH_MDATP_ONBOARD_JSON +JSON +) + +MDATP=/usr/bin/mdatp +ONBOARD_FILE=/etc/opt/microsoft/mdatp/mdatp_onboard.json + +log() { + logger -t defender-remediation -- "$*" + echo "$*" +} + +# Print one `mdatp health` field without quotes, a trailing " [managed]" or +# the "ATTENTION: ..." lines mdatp prints while unlicensed. +field() { + "${MDATP}" health --field "$1" 2>/dev/null | grep -v '^ATTENTION' | tail -n1 \ + | sed -e 's/ \[managed\]$//' -e 's/^"//' -e 's/"$//' +} + +# Wait up to 2 minutes for the daemon to answer. +wait_for_daemon() { + local i + for i in $(seq 1 24); do + [[ "$(field healthy)" =~ ^(true|false)$ ]] && return 0 + sleep 5 + done + return 1 +} + +if [[ ! -x "${MDATP}" ]]; then + log "mdatp is not installed; it ships in the image, so update or rebase the device." + exit 1 +fi + +# The image finishes the Defender install on first boot (mdatp-firstboot). +if [[ ! -f /var/lib/mdatp-firstboot.done ]]; then + log "Defender first-boot setup has not completed; starting it." + systemctl start mdatp-firstboot.service +fi + +if ! systemctl is-active --quiet mdatp.service; then + log "mdatp.service is not running; restarting it." + systemctl restart mdatp.service +fi +if ! wait_for_daemon; then + log "The Defender daemon does not respond." + exit 1 +fi + +if [[ -z "$(field org_id)" ]]; then + if [[ "${ONBOARDING_JSON}" == REPLACE_WITH_MDATP_ONBOARD_JSON ]]; then + log "Defender is not onboarded and this script has no onboarding JSON." + else + log "Defender is not onboarded; writing ${ONBOARD_FILE}." + install -d -m 0755 "$(dirname "${ONBOARD_FILE}")" + printf '%s\n' "${ONBOARDING_JSON}" > "${ONBOARD_FILE}.tmp" + chown root:mdatp "${ONBOARD_FILE}.tmp" + chmod 0600 "${ONBOARD_FILE}.tmp" + mv -f "${ONBOARD_FILE}.tmp" "${ONBOARD_FILE}" + systemctl restart mdatp.service + wait_for_daemon || { log "The Defender daemon does not respond after onboarding."; exit 1; } + fi +fi + +# Settings Defender's own policy manages show " [managed]" and can't be +# changed locally; this only fixes a locally disabled real-time protection. +if [[ "$(field real_time_protection_enabled)" != true ]]; then + log "Real-time protection is off; enabling it." + "${MDATP}" config real-time-protection --value enabled +fi + +if [[ "$(field definitions_status)" != up_to_date ]]; then + log "Definitions are not up to date; updating them." + "${MDATP}" definitions update +fi + +healthy="$(field healthy)" +log "healthy=${healthy} licensed=$(field licensed) org_id=$(field org_id) rtp=$(field real_time_protection_enabled) definitions=$(field definitions_status)" +[[ "${healthy}" == true ]]