Continuous Microsoft 365 Security Monitoring with Maester and GitHub Actions
Modern Microsoft 365 environments change constantly. New policies are added, configurations drift, and security settings evolve as teams deploy new services. Without continuous monitoring, it’s easy for misconfigurations to creep in unnoticed.
Maester is an open‑source framework designed to test Microsoft 365 security configurations automatically. It runs a suite of checks against your tenant and reports configuration issues across services such as Entra ID, Exchange Online, Teams, and Azure.
One of the most powerful ways to use Maester is to run it automatically with GitHub Actions, giving you scheduled, repeatable security checks and a historical record of results.
In this guide we will cover:
- Setting up Maester for automation
- Creating the Entra application and permissions
- Creating the GitHub Actions workflow
- Setting the required GitHub organisation permissions
- Ensuring repositories remain private
- Retaining six months of security reports automatically
Setting Up Maester for Automation
Maester supports running through GitHub Actions with workload identity federation, meaning GitHub can authenticate to Azure without storing client secrets.
The architecture looks like this:
GitHub Actions → Entra App Registration → Microsoft Graph / Exchange / Teams / Azure APIs → Maester Security Tests
The workflow will:
- Authenticate to Azure using GitHub OIDC
- Run Maester tests
- Store the results in the repository
- Retain six months of reports automatically
Creating the Entra Application
- Open Microsoft Entra admin center
- Navigate to Identity → Applications → App registrations
- Select New registration
Enter:
Name: Maester DevOps Account
Supported account types: Single tenant
Select Register.
Make note of:
- Application (client) ID
- Directory (tenant) ID
These will later be added to GitHub as secrets.
Granting Microsoft Graph Permissions
Open the application:
API Permissions → Add Permission
Choose:
Microsoft Graph → Application permissions
Add the following permissions:
- Directory.Read.All
- DirectoryRecommendations.Read.All
- Policy.Read.All
- Policy.Read.ConditionalAccess
- Reports.Read.All
- ReportSettings.Read.All
- RoleManagement.Read.All
- RoleEligibilitySchedule.Read.Directory
- PrivilegedAccess.Read.AzureAD
- IdentityRiskEvent.Read.All
- UserAuthenticationMethod.Read.All
- DeviceManagementConfiguration.Read.All
- DeviceManagementManagedDevices.Read.All
- DeviceManagementRBAC.Read.All
- SecurityIdentitiesHealth.Read.All
- SecurityIdentitiesSensors.Read.All
- ThreatHunting.Read.All
- SharePointTenantSettings.Read.All
- OnPremDirectorySynchronization.Read.All
Then select Grant admin consent.
Optional permission:
- ReportSettings.ReadWrite.All
Granting Exchange Online Permissions
Add:
Office 365 Exchange Online → Application permissions → Exchange.ManageAsApp
Grant admin consent.
Then run:
New-ServicePrincipal -AppId <Application ID> -ObjectId <Service Principal Object ID> -DisplayName "Maester DevOps Account"
New-ManagementRoleAssignment -Role "View-Only Configuration" -App "Maester DevOps Account"
⚠️ Common Issue: Exchange Object ID
If you see:
AADServicePrincipalNotFound
You are likely using the App Registration Object ID instead of the Enterprise Application Object ID.
Use:
Enterprise Applications → Maester App → Object ID
Granting Microsoft Teams Permissions
- Go to Roles and administrators
- Search for Teams Reader
- Select Add assignment
- Select the Maester application
- Assign permanently
Granting Azure Permissions
Run:
$servicePrincipal = "<Enterprise Application Object ID>"
Install-Module Az.Accounts -Force
Install-Module Az.Resources -Force
Connect-AzAccount
Invoke-AzRestMethod -Path "/providers/Microsoft.Authorization/elevateAccess?api-version=2015-07-01" -Method POST
New-AzRoleAssignment -ObjectId $servicePrincipal -Scope "/" -RoleDefinitionName "Reader"
New-AzRoleAssignment -ObjectId $servicePrincipal -Scope "/providers/Microsoft.aadiam" -RoleDefinitionName "Reader"
This grants the application tenant‑wide read visibility.
Configure GitHub Federation
In the Entra App:
Certificates & secrets → Federated credentials → Add credential
Scenario:
GitHub Actions deploying Azure resources
Fill:
Organisation: your-org
Repository: maester-security
Entity type: Branch
Branch: main
Credential name: maester-github
Save.
Adding GitHub Secrets
In GitHub:
Settings → Secrets and variables → Actions
Add:
- AZURE_TENANT_ID
- AZURE_CLIENT_ID
Creating the GitHub Actions Workflow
Create:
.github/workflows/run-maester.yml
Example:
name: Run Maester 🔥
on:
push:
branches:
- main
paths-ignore:
- "reports/**"
schedule:
- cron: "30 7 * * *"
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
steps:
- name: Checkout repo
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Run Maester 🔥
id: maester
uses: maester365/maester-action@main
with:
tenant_id: $
client_id: $
include_public_tests: true
include_private_tests: true
include_exchange: true
include_teams: true
maester_version: latest
disable_telemetry: false
step_summary: true
- name: Write status 📃
shell: bash
run: |
echo "The result of the test run is: $"
echo "Total tests: $"
echo "Passed tests: $"
echo "Failed tests: $"
echo "Skipped tests: $"
- name: Archive results into weekly folder
shell: bash
run: |
set -euo pipefail
YEAR="$(date -u +%G)"
WEEK="$(date -u +%V)"
RUNSTAMP="$(date -u +%Y-%m-%dT%H-%M-%SZ)"
DEST="reports/${YEAR}-W${WEEK}/${RUNSTAMP}"
mkdir -p "$DEST"
if [ ! -d "test-results" ]; then
echo "test-results folder not found"
exit 1
fi
cp -R test-results/. "$DEST"/
cat > "$DEST/run-summary.txt" <<EOF
Run timestamp (UTC): ${RUNSTAMP}
Result: $
Total tests: $
Passed tests: $
Failed tests: $
Skipped tests: $
Workflow run: https://github.com/$/actions/runs/$
Commit: $
EOF
- name: Update latest pointer
shell: bash
run: |
set -euo pipefail
YEAR="$(date -u +%G)"
WEEK="$(date -u +%V)"
RUNSTAMP="$(date -u +%Y-%m-%dT%H-%M-%SZ)"
DEST="reports/${YEAR}-W${WEEK}/${RUNSTAMP}"
mkdir -p reports/latest
find reports/latest -mindepth 1 -maxdepth 1 -exec rm -rf {} +
cp -R "$DEST"/. reports/latest/
- name: Prune report history to 26 weeks
shell: bash
run: |
set -euo pipefail
mkdir -p reports
mapfile -t WEEK_DIRS < <(
find reports -mindepth 1 -maxdepth 1 -type d -name '20*-W*' -printf '%f\n' | sort
)
COUNT="${#WEEK_DIRS[@]}"
KEEP=26
if [ "$COUNT" -le "$KEEP" ]; then
echo "Nothing to prune. Found $COUNT weekly folders."
exit 0
fi
DELETE_COUNT=$((COUNT - KEEP))
for ((i=0; i<DELETE_COUNT; i++)); do
echo "Deleting reports/${WEEK_DIRS[$i]}"
rm -rf "reports/${WEEK_DIRS[$i]}"
done
- name: Commit weekly reports
shell: bash
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add reports
if git diff --cached --quiet; then
echo "No report changes to commit."
exit 0
fi
git commit -m "Add Maester reports [skip ci]"
git push
Retaining 26 Weeks of Reports
Reports are stored like:
reports/YYYY-Www/<timestamp>{=html}/
Example:
reports/ latest/ 2026-W11/ 2026-03-11T07-30Z/ 2026-W12/ 2026-03-18T07-30Z/
The workflow automatically deletes folders older than 26 weeks.
Setting GitHub Organisation Permissions
Organisation Settings → Actions → General → Workflow permissions
Set:
Read and write permissions
⚠️ Common Issue: contents: write
If report commits fail, check your workflow contains:
permissions:
contents: write
If your organisation enforces read‑only workflow tokens, you must request an admin to enable write access or use a GitHub App / PAT.
Ensuring the Repository is Private
Set:
Settings → General → Change repository visibility → Private
Security test reports should never be stored in public repositories.
Final Thoughts
Automating Maester with GitHub Actions provides a lightweight but powerful security monitoring pipeline for Microsoft 365.
Benefits include:
- Automated daily security validation
- Historical configuration reporting
- Git-based audit trail
- No stored secrets (OIDC authentication)
- Automatic report retention
For organisations managing Microsoft 365 environments, this approach provides an effective way to implement continuous security configuration monitoring.