Files

381 lines
17 KiB
Markdown

# Self-Service Security Cadence
> *What you run between our engagements. When something in here surprises you, that's when you call us.*
**Last updated:** June 2026
**Produced by:** [engagement name / consultant name]
**For:** [client name] — [named admin / IT lead]
**Next full engagement:** [date or "TBD"]
**Next review of this document:** January 2027
---
## What this is
We ran the adversarial validation. We fixed the structural issues we found. The work does not stop when we leave.
This document is your recurring checklist — things you can run yourself, with the tools we set up, on a regular cadence. None of it requires a security background. Most of it takes under an hour per month. The point is to catch drift before it becomes a problem, and to know when to call us before it becomes a crisis.
**The most important thing:** when something in here produces a result that surprises you, do not sit on it. Log it, screenshot it, and send it to us. The earlier we see a problem the cheaper it is to fix.
---
## Tools you need (all installed during the engagement)
| Tool | What it does | Where to get it |
|------|-------------|-----------------|
| **PingCastle** | Scans Active Directory and produces a security report with a score and specific findings | [pingcastle.com](https://www.pingcastle.com) — free Community edition |
| **Purple Knight** | Scans Active Directory for indicators of exposure — simpler output than PingCastle, good complement | [purple-knight.com](https://www.purple-knight.com) — free |
| **CAExporter** | Exports all Conditional Access policies to JSON files you can compare over time | [github.com/vibecoding/CAExporter](https://github.com/vibecoding/CAExporter) |
| **Microsoft Graph PowerShell** | The PowerShell module for the scripts in this document | `Install-Module Microsoft.Graph` |
| **Microsoft 365 Defender portal** | alerts.microsoft.com — your alert queue and Secure Score | |
| **Microsoft Entra portal** | entra.microsoft.com — your identity dashboard | |
The scripts in this document are saved in `[location agreed during engagement — e.g., C:\SecurityRunbook\Scripts\]`.
---
## Monthly checks — 30 to 45 minutes, portal-based
Do these on the first working day of each month. They require no special tools — just a browser logged in as a Global Admin or Security Reader.
---
### M1. Microsoft Secure Score
**Where:** [Microsoft 365 Defender portal](https://security.microsoft.com) > Secure Score
**What to do:**
1. Note the current score.
2. Compare to last month's score (the history graph shows it).
3. Look at the "Recommended actions" tab — filter to "Not addressed."
4. Any new items that appeared since last month? Note them.
**What you are looking for:** Score going down month-over-month without a known reason. New recommended actions you did not create. Completed actions that have reverted to "not addressed" (this means configuration drifted back).
**Call us if:** Score drops more than 5 points in a month without a documented reason, or if a completed action you remember implementing shows as "not addressed."
---
### M2. Entra ID Recommendations
**Where:** [Entra portal](https://entra.microsoft.com) > Overview > Recommendations
**What to do:**
1. Look at all open recommendations.
2. Note any that are new since last month.
3. Note the impact rating (High / Medium / Low) on new ones.
**What you are looking for:** New high-impact recommendations that appeared since last month. Specifically watch for anything related to admin accounts, Conditional Access, legacy authentication, or risky sign-ins.
**Call us if:** Any new High-impact recommendation appears. We will help you assess whether to act immediately or schedule it.
---
### M3. Sign-in risk review
**Where:** Entra portal > Identity Protection > Risky sign-ins
**What to do:**
1. Filter to the last 30 days.
2. Look at sign-ins with risk level "High" that were not dismissed or remediated.
3. For any admin account (Global Admin, Exchange Admin, Security Admin) with any risky sign-in event — investigate before dismissing.
**What you are looking for:** Admin accounts appearing in the risky sign-in list. Any high-risk sign-in that auto-remediated (meaning the user passed an MFA challenge) where the geography or device does not make sense.
**Call us if:** Any admin account has a risky sign-in event. Any high-risk event that was remediated from an unexpected location.
---
### M4. Alert queue health
**Where:** Microsoft 365 Defender portal > Incidents & alerts > Alerts
**What to do:**
1. Filter to "New" and "In progress" alerts.
2. How many are sitting open for more than 48 hours?
3. Are there categories of alert that appear repeatedly? (Recurring alerts on the same user or asset are a pattern, not noise.)
**What you are looking for:** Alert queue growing over time without being worked. The same alert firing repeatedly on the same account or resource. Any alert tagged as "High severity" that is more than 24 hours old without assignment.
**Call us if:** A High-severity alert is more than 24 hours old and you do not know what to do with it. Or if the same alert keeps firing on the same account.
---
### M5. New admin assignments
**Where:** Entra portal > Identity > Roles & admins > All roles > Global Administrator > Assignments
**What to do:**
1. Check the current member list against last month's.
2. Any new members? Were they expected?
3. Check at minimum: Global Administrator, Exchange Administrator, Security Administrator, SharePoint Administrator.
**What you are looking for:** Anyone in a privileged role who should not be, or who appeared without a formal request.
**Call us if:** Any new privileged role assignment you did not authorize or do not recognize.
---
### M6. Break-glass confirmation (30 seconds)
**What to do:**
1. Confirm the break-glass account credentials are still in the agreed storage location.
2. Confirm the contact for "break-glass alert fired" is still the right person.
Do not log in to the break-glass account during this check — any sign-in triggers an alert. Just confirm the credentials are accessible.
**Call us if:** Credentials cannot be found. Or if the break-glass alert fires without a drill scheduled.
---
## Quarterly checks — 2 to 3 hours, tools required
Do these in the first week of each quarter (January, April, July, October). These require running the installed tools and saving the output.
---
### Q1. PingCastle AD scan
**How to run:**
1. Log in to the domain controller (or any domain-joined machine) as a Domain Admin.
2. Run `PingCastle.exe --healthcheck --server <your-domain-FQDN>`.
3. It produces an HTML report. Save it to `[agreed location]` with the date in the filename: `PingCastle-2026-Q3.html`.
4. Open the report and note the score and any findings marked "Critical" or "High."
5. Compare to the previous quarter's report — is the score going up or down?
**What you are looking for:** Score trending down quarter-over-quarter. New Critical or High findings that were not present last quarter. Specifically watch the "Stale Objects" section (accounts nobody uses) and the "Privileged Access" section.
**Call us if:** The score drops more than 10 points since last quarter. Any new Critical finding. Any finding in the "Privileged Access" category that was clean last quarter.
---
### Q2. Purple Knight AD scan
**How to run:**
1. Download and run Purple Knight on a domain-joined machine with Domain Admin credentials.
2. It is a GUI tool — click through the scan, wait for it to finish.
3. Save the PDF report with the date: `PurpleKnight-2026-Q3.pdf`.
4. Look at the "Identity Security Indicators" with status "Exposed" or "Critical."
5. Compare to the previous quarter.
**What you are looking for:** New exposed indicators that did not appear last quarter. Any indicator flagged as Critical. The tool is organized by MITRE ATT&CK category — pay particular attention to "Credential Access" and "Privilege Escalation."
**Call us if:** Any new Critical indicator. Or if the same Medium indicators keep appearing quarter after quarter without being resolved (this means the fix did not stick).
---
### Q3. KRBTGT and AZUREADSSOACC age check
**How to run:** Open PowerShell as Domain Admin and run the following:
```powershell
Write-Host "=== KRBTGT ===" -ForegroundColor Cyan
Get-ADUser krbtgt -Properties PasswordLastSet |
Select-Object @{N="Account";E={"krbtgt"}},
PasswordLastSet,
@{N="AgeDays";E={((Get-Date) - $_.PasswordLastSet).Days}}
Write-Host "=== AZUREADSSOACC ===" -ForegroundColor Cyan
Get-ADComputer AZUREADSSOACC -Properties PasswordLastSet -ErrorAction SilentlyContinue |
Select-Object @{N="Account";E={"AZUREADSSOACC"}},
PasswordLastSet,
@{N="AgeDays";E={((Get-Date) - $_.PasswordLastSet).Days}}
```
Record the age in days in your tracking spreadsheet.
**What you are looking for:** KRBTGT older than 365 days = P1 (schedule rotation with us). KRBTGT older than 180 days = note and plan. AZUREADSSOACC never rotated since initial sync setup = note.
**Call us if:** KRBTGT is over 365 days old and there is no scheduled rotation. Or if either account shows a password age younger than expected (meaning someone rotated it without telling you — that is a finding too).
---
### Q4. Cloud-only Global Admins check
**How to run:**
```powershell
Connect-MgGraph -Scopes "Directory.Read.All"
$gaRoleId = (Get-MgDirectoryRole -Filter "displayName eq 'Global Administrator'").Id
$gaMembers = Get-MgDirectoryRoleMember -DirectoryRoleId $gaRoleId
Write-Host "=== Global Admins ===" -ForegroundColor Cyan
$gaMembers | ForEach-Object {
$user = Get-MgUser -UserId $_.Id -Property DisplayName,UserPrincipalName,OnPremisesSyncEnabled
[PSCustomObject]@{
Name = $user.DisplayName
UPN = $user.UserPrincipalName
SyncedFromAD = $user.OnPremisesSyncEnabled
}
} | Format-Table -AutoSize
```
Any row where `SyncedFromAD` is `True` is a P0 — call us immediately.
**What you are looking for:** Any Global Admin that is synced from on-prem AD. Any new GA you did not create.
**Call us if:** Any synced GA appears. Any GA you do not recognize.
---
### Q5. Service principal secrets check — expiring and never-expiring
**How to run:**
```powershell
Connect-MgGraph -Scopes "Application.Read.All"
$today = Get-Date
$warningDays = 60
Write-Host "=== Non-expiring secrets ===" -ForegroundColor Red
Get-MgApplication -All | ForEach-Object {
$app = $_
$app.PasswordCredentials | Where-Object { $_.EndDateTime -eq $null } | ForEach-Object {
[PSCustomObject]@{ App = $app.DisplayName; Secret = $_.DisplayName; Expires = "NEVER" }
}
} | Format-Table
Write-Host "=== Secrets expiring within $warningDays days ===" -ForegroundColor Yellow
Get-MgApplication -All | ForEach-Object {
$app = $_
$app.PasswordCredentials | Where-Object {
$_.EndDateTime -ne $null -and $_.EndDateTime -lt $today.AddDays($warningDays)
} | ForEach-Object {
[PSCustomObject]@{ App = $app.DisplayName; Secret = $_.DisplayName; Expires = $_.EndDateTime }
}
} | Sort-Object Expires | Format-Table
```
**What you are looking for:** Non-expiring secrets on any app registration. Secrets about to expire (these will break an application if not rotated — but they also need reviewing: is the app still needed?).
**Call us if:** You find a non-expiring secret on an app you do not recognize. Or if you find an expiring secret and do not know which application or service it belongs to.
---
### Q6. Stale guest review
**How to run:**
```powershell
Connect-MgGraph -Scopes "User.Read.All", "AuditLog.Read.All"
$cutoff = (Get-Date).AddDays(-90)
Get-MgUser -Filter "userType eq 'Guest'" -All -Property DisplayName,Mail,CreatedDateTime,SignInActivity |
ForEach-Object {
$lastSignIn = $_.SignInActivity.LastSignInDateTime
[PSCustomObject]@{
Name = $_.DisplayName
Email = $_.Mail
Created = $_.CreatedDateTime
LastSignIn = $lastSignIn
DaysSinceSignIn = if ($lastSignIn) { ((Get-Date) - $lastSignIn).Days } else { "Never" }
}
} |
Sort-Object DaysSinceSignIn -Descending |
Format-Table -AutoSize
```
**What you are looking for:** Guests who have not signed in for 90+ days. Guests you do not recognize (external parties from concluded projects or former vendors).
**Call us if:** The count of stale guests is growing quarter-over-quarter and nobody is pruning them. Or if a guest account appears that belongs to an external party from a concluded engagement and still has active access.
---
### Q7. Anonymous link count
**How to run:** Connect using PnP PowerShell (installed during engagement):
```powershell
Connect-PnPOnline -Url "https://[tenant]-admin.sharepoint.com" -Interactive
$sites = Get-PnPTenantSite -IncludeOneDriveSites
$anonLinks = foreach ($site in $sites) {
Connect-PnPOnline -Url $site.Url -Interactive
Get-PnPSharingLinks | Where-Object { $_.SharingLinkType -eq "Anonymous" } |
ForEach-Object { [PSCustomObject]@{ Site = $site.Url; Link = $_.ShareLink; Expires = $_.ExpirationDateTime } }
}
Write-Host "Total anonymous links: $($anonLinks.Count)" -ForegroundColor Yellow
$anonLinks | Sort-Object Site | Format-Table
```
Record the count. Save the export.
**What you are looking for:** Count increasing quarter-over-quarter (means new anonymous links are being created despite the policy). Links with no expiration date.
**Call us if:** Count is increasing despite the restriction we put in place. Or if you find anonymous links on sites that hold sensitive data (HR, Finance, M&A).
---
### Q8. CA policy diff — detect drift
**How to run:**
```powershell
# CAExporter is set up from the engagement — run from its directory
.\CAExporter.ps1 -ExportPath "C:\SecurityRunbook\CA-Exports\CA-$(Get-Date -Format 'yyyy-MM-dd')"
```
Then compare this quarter's export folder to last quarter's using any file diff tool (WinMerge, VS Code with the "compare folders" extension, or simply `Compare-Object` in PowerShell):
```powershell
$old = Get-ChildItem "C:\SecurityRunbook\CA-Exports\CA-2026-04-01" -File | Select-Object -ExpandProperty Name
$new = Get-ChildItem "C:\SecurityRunbook\CA-Exports\CA-2026-07-01" -File | Select-Object -ExpandProperty Name
Compare-Object $old $new
```
Then for any policy that changed, open the JSON files and compare manually. The changed lines are the configuration drift.
**What you are looking for:** Policies deleted since last quarter. Policies whose parameters changed (exclusions added, scope narrowed, MFA grant changed to "grant without controls"). New policies in report-only mode that should have been enabled.
**Call us if:** Any CA policy has changed without a corresponding change record. A policy that was enforcing is now in report-only mode. A new exclusion was added to a critical policy (legacy auth block, admin MFA, device compliance).
---
## "Call us" trigger list
These are the situations where you stop, take a screenshot, and contact us — even outside a scheduled check:
| What you see | How urgent | What to do first |
|---|---|---|
| Break-glass alert fires unexpectedly | Immediate | Disable any active sessions for the break-glass account, then call us |
| New Global Admin you did not create | Immediate | Do not remove it yet — screenshot first, then call us |
| Synced account in Global Admin role | Same day | Do not change anything — screenshot and call us |
| DCSync alert from Defender for Identity | Immediate | Isolate the source host from the network if possible, then call us |
| External auto-forward rule found on any executive mailbox | Same day | Disable the rule, check for mail forwarded, call us |
| PingCastle score drops more than 10 points | Within 48 hours | Send us the report alongside the previous quarter's |
| Any alert sitting at High severity for more than 24 hours you do not know how to triage | Within 24 hours | Screenshot, note what the alert says, call us |
| Backup restore fails or produces corrupt data | Same day | Do not delete anything — call us |
| Something that feels wrong but is not on this list | Use your judgement | A wrong feeling is data. Document what you noticed and send it. We will tell you if it is nothing. |
---
## Tracking spreadsheet columns
Keep a simple spreadsheet (Excel or SharePoint list) with one row per check per quarter:
| Date | Check | Result / Count | vs. Last Quarter | Action taken | Escalated to consultant? |
|------|-------|---------------|-----------------|--------------|--------------------------|
The trend matters more than any individual value. A metric that is consistently getting worse is a finding even if no single value crosses a threshold.
---
## When to schedule the next full engagement
Use this as a rule of thumb:
- **Annual:** Full adversarial validation (the engagement that produced this document). Recommended even if the monthly and quarterly checks are clean — they catch drift, not adversarial paths.
- **Triggered:** Any time a "call us immediately" event fires, or PingCastle / Purple Knight produces a new Critical finding.
- **Project-triggered:** Before any major change to the estate — AD migration, new cloud service onboarding, M365 license change, acquisition or merger, significant IT staff change.
---
*Self-service cadence for [client name]. Produced June 2026. Review and update January 2027 alongside the field guide update.*