Automation
Microsoft Sentinel : valider une watchlist avant qu'elle ne pilote une réponse automatisée
Un runbook de production pour prouver fraîcheur, schéma, SearchKey, suppressions et comportement KQL d'une watchlist Sentinel avant de la laisser influencer une réponse automatisée.
Une règle Microsoft Sentinel cesse soudain de créer des incidents pour certains comptes privilégiés. Ailleurs, un playbook bloque une adresse IP qui ne devrait plus figurer dans la liste de surveillance. La requête KQL s’exécute, la watchlist existe et l’automatisation est au vert : le défaut se trouve dans les données de référence qui pilotent la décision.
Le cas fil rouge est une watchlist privileged_assets_prod alimentée par un export d’inventaire. Une analytics rule l’utilise pour enrichir les signaux, puis une règle d’automatisation transmet les incidents à un playbook. Le runbook doit décider si la nouvelle version peut piloter la production, rester en observation ou revenir au snapshot précédent sans élargir silencieusement les détections ni agir sur une cible périmée.
Figer le contrat de décision
Une watchlist n’est pas seulement un fichier CSV. C’est une dépendance de détection avec un propriétaire, une source, une clé de jointure, un sens métier et un impact en cas d’absence. Décrivez ce contrat avant de regarder le portail.
watchlist:
alias: privileged_assets_prod
source: cmdb-export
owner: secops-platform
search_key: AssetId
source_snapshot: cmdb-20261001T051500Z
generated_at_utc: 2026-10-01T05:15:00Z
expected_rows: 1842
decision:
used_by:
- analytics-rule-privileged-signin
- automation-rule-high-risk-identity
match_means: asset_is_in_scope
no_match_means: do_not_auto-remediate
duplicate_key: reject_candidate
expired_row: ignore_and_alert
rollback:
previous_alias: privileged_assets_20260930
automation_mode: observe_only Le sens du no match est critique. Une liste d’autorisation, une liste de cibles sensibles et une liste d’indicateurs malveillants ne doivent pas utiliser le même fallback. Si le contrat ne dit pas si l’absence autorise, bloque ou suspend la décision, la watchlist ne doit pas déclencher d’action.
Séparer fraîcheur technique et fraîcheur métier
Commencez par prouver l’alias réellement appelé. _GetWatchlistAlias permet d’inventorier les alias disponibles ; _GetWatchlist('alias') retourne les lignes utilisées par KQL. Vérifiez les deux plans : le statut de gestion de la watchlist et son contenu interrogeable peuvent diverger pendant une ingestion ou un incident de service.
N’utilisez pas TimeGenerated comme date de fraîcheur de la source. Microsoft Sentinel rafraîchit périodiquement les watchlists dans le workspace et met ce champ à jour. Une ligne peut donc avoir un TimeGenerated récent tout en provenant d’un export métier ancien. Ajoutez dans la source des colonnes explicites comme SourceUpdatedAt, SnapshotId et ExpiresAt.
let ExpectedAlias = "privileged_assets_prod";
let MaxSourceAge = 6h;
let Rows =
_GetWatchlist(ExpectedAlias)
| extend SourceUpdatedAt = todatetime(SourceUpdatedAt),
ExpiresAt = todatetime(ExpiresAt),
Key = tostring(SearchKey);
Rows
| summarize
Rows=count(),
DistinctKeys=dcount(Key),
MissingKeys=countif(isempty(Key)),
OldestSource=min(SourceUpdatedAt),
NewestSource=max(SourceUpdatedAt),
ExpiredRows=countif(ExpiresAt < now()),
FutureRows=countif(SourceUpdatedAt > now() + 5m),
Snapshots=dcount(tostring(SnapshotId))
| extend SourceIsFresh = NewestSource > ago(MaxSourceAge) Un résultat sain n’est pas seulement Rows > 0. Il faut un snapshot attendu, aucune clé vide, une ancienneté compatible avec le contrat et une seule génération logique de données. Une horloge source dans le futur, plusieurs SnapshotId ou un volume très éloigné de la baseline imposent un arrêt avant l’automatisation.
Traiter les suppressions comme une opération explicite
Une mise à jour en masse ajoute les lignes puis déduplique les lignes identiques. Retirer une entrée du nouveau CSV ne supprime pas nécessairement l’entrée déjà présente dans la watchlist. C’est un piège majeur pour une denylist, une allowlist temporaire ou une liste de cibles de remédiation : une valeur retirée à la source peut continuer à influencer KQL.
Construisez le candidat comme un snapshot contrôlé. Comparez clés ajoutées, modifiées et supprimées avec la version actuellement active. Pour de nombreuses suppressions, préférez une nouvelle watchlist versionnée, validez-la, puis changez l’alias référencé par la règle. Gardez l’ancienne version intacte pendant la fenêtre de rollback.
let Current =
_GetWatchlist('privileged_assets_20260930')
| project Key=tostring(SearchKey), CurrentPresent=true,
CurrentOwner=tostring(Owner), CurrentPolicy=tostring(ActionPolicy);
let Candidate =
_GetWatchlist('privileged_assets_20261001')
| project Key=tostring(SearchKey), CandidatePresent=true,
CandidateOwner=tostring(Owner), CandidatePolicy=tostring(ActionPolicy);
Current
| join kind=fullouter Candidate on Key
| extend Change = case(
isnull(CurrentPresent), "added",
isnull(CandidatePresent), "removed",
CurrentOwner != CandidateOwner or CurrentPolicy != CandidatePolicy, "modified",
"unchanged")
| summarize Rows=count(), Sample=make_set(Key, 20) by Change
| order by Change asc La comparaison doit être revue par le propriétaire de la source, pas seulement par l’équipe Sentinel. Une suppression peut être attendue parce qu’un actif a été décommissionné ; elle peut aussi signaler un export partiel, un filtre cassé ou une permission perdue sur la CMDB.
Qualifier la clé avant de qualifier la règle
Le SearchKey doit correspondre à la colonne la plus utilisée pour les jointures. Cela n’assure toutefois ni unicité ni normalisation. Des espaces, différences de casse, formats UPN, adresses IPv6 ou identifiants de ressource Azure non canoniques peuvent produire des faux no match.
Normalisez des deux côtés de la jointure et mesurez son résultat. Ne masquez pas les doublons avec distinct tant que leur origine n’est pas comprise.
let Assets =
_GetWatchlist('privileged_assets_20261001')
| extend JoinKey=tolower(trim(' ', tostring(SearchKey)))
| summarize WatchlistRows=count(), Owners=make_set(tostring(Owner), 5),
Policies=make_set(tostring(ActionPolicy), 5) by JoinKey;
SigninLogs
| where TimeGenerated > ago(2h)
| extend JoinKey=tolower(trim(' ', tostring(UserPrincipalName)))
| lookup kind=leftouter Assets on JoinKey
| extend MatchState = case(
isempty(JoinKey), "event-key-missing",
isnull(WatchlistRows), "not-matched",
WatchlistRows > 1, "ambiguous",
"matched")
| summarize Events=count(), SampleUsers=make_set(UserPrincipalName, 10) by MatchState Relisez aussi le sens de la jointure. Un lookup d’enrichissement conserve les événements non reconnus, alors qu’un inner join les élimine. Une règle qui doit détecter les comptes inconnus et une règle qui cible uniquement les comptes privilégiés exigent des comportements opposés. Le choix doit être visible dans les tests.
Tester la règle avec un jeu de cas borné
La validation doit couvrir la watchlist et ses consommateurs. Rejouez une fenêtre d’événements approuvée avec le candidat, sans créer d’incident ni appeler le playbook. Conservez pour chaque cas la clé source, la ligne trouvée, la branche de décision et l’action qui aurait été proposée.
cases:
- name: current_privileged_account
key: admin-api@contoso.example
expected: match_and_enrich
- name: removed_account
key: legacy-admin@contoso.example
expected: no_match_and_no_action
- name: normalized_case
key: ADMIN-API@CONTOSO.EXAMPLE
expected: one_canonical_match
- name: duplicate_key
key: shared-ops@contoso.example
expected: reject_candidate
- name: expired_exception
key: breakglass-test@contoso.example
expected: ignore_and_alert
promotion_requires:
- expected snapshot id
- row delta reviewed
- zero blank or duplicate keys
- removed rows absent from candidate
- expected join outcome for every case
- automation held in observe-only Une comparaison globale du nombre d’alertes ne suffit pas. Le même volume peut cacher des cibles différentes. Comparez l’ensemble des identifiants appariés, les non-correspondances et les décisions proposées entre ancienne et nouvelle version.
Basculer la référence, pas réécrire la preuve
Canarisez la nouvelle watchlist sur une copie désactivée de l’analytics rule ou dans une requête de shadow evaluation. Lorsque les cas passent, modifiez uniquement la référence de watchlist de la règle, conservez le playbook en observe-only, puis vérifiez au moins un cycle complet d’exécution.
Pendant la fenêtre, corrélez version de règle, alias, SnapshotId, incident, run Logic Apps et cible proposée. Une exécution verte prouve que le workflow a terminé ; elle ne prouve pas que la liste était complète ni que la cible était correcte.
Activez ensuite l’action sur un périmètre borné avec approbation humaine. Stoppez si le volume dérive, si une clé devient ambiguë, si le snapshot change sans changement approuvé ou si le playbook reçoit une cible qui n’existait pas dans le résultat de shadow evaluation.
Décider et rollbacker
Promouvez le candidat lorsque l’alias et le snapshot sont identifiés, que le delta est expliqué, que les suppressions sont réelles, que la clé est unique, que la jointure produit les branches attendues et que le canari relie chaque décision à sa ligne de référence.
Restez en observation si les données sont utiles mais que leur fraîcheur ou leur propriétaire n’est pas prouvé. Refusez la bascule si l’export est partiel, si une mise à jour a conservé des lignes supprimées ou si le no match mène à une action permissive ou destructive.
Pour rollbacker, désactivez d’abord l’action automatisée, remettez la règle sur l’alias précédent, exécutez les mêmes cas de contrôle puis vérifiez les incidents créés pendant la fenêtre. Une action déjà exécutée doit être annulée depuis son identifiant d’opération ; revenir à l’ancienne watchlist ne restaure pas un compte, un hôte ou une règle réseau.
Conclusion
Une watchlist Sentinel est du code de décision sous forme de données. Son existence dans le portail et un TimeGenerated récent ne prouvent ni son origine, ni sa complétude, ni la disparition des entrées obsolètes.
La mise en production devient défendable lorsque snapshot, suppressions, SearchKey, normalisation et comportement de jointure sont testés ensemble. À la fin, la décision est explicite : promouvoir la référence, maintenir l’observation ou revenir au snapshot précédent avant qu’une donnée périmée ne devienne une action de production.