comit-audit-defender

Paquet WAPT Enterprise 2.6 d'audit en lecture seule de Microsoft Defender. Il ne déploie pas d'agent supplémentaire, ne modifie aucune préférence Defender, ne supprime aucune quarantaine et n'effectue aucun accès réseau externe. Le hook natif audit() retourne exclusivement OK, WARNING ou ERROR.

Le paquet est planifié toutes les 15 minutes par WAPT/control. La collecte PowerShell est locale et bornée à 90 secondes; elle utilise Get-MpComputerStatus, Get-MpThreat et Get-MpThreatDetection. Une erreur PowerShell, un timeout, une réponse vide ou un JSON invalide produit ERROR et un état collection_complete=false, jamais un poste sain.

Classification 0–16

La gravité Defender intrinsèque est conservée dans severity_id et le risque calculé dans event_level. Les identifiants canoniques MPTHREAT_SEVERITY sont 0 Unknown, 1 Low, 2 Moderate, 4 High, 5 Severe; WMI SeverityID (0..4) est normalisé (3 High vers 4, 4 Severe vers 5) avant classification. Les niveaux de base sont respectivement 5, 4, 6, 9 et 12.

Niveau Catégorie
0 Aucun événement
1–3 Information
4–6 Faible
7–9 Modéré
10–12 Élevé
13–14 Critique
15–16 Urgence

Règles par défaut, définies dans DEFAULT_RULES et surchargeables par les fonctions pures pour les tests :

Contexte Defender Modificateur
Cleaned, Quarantined, Removed -3
Blocked -2
Detected / état non résolu +2
Failed remediation +3
Allowed +3
Répétition de la même menace dans la fenêtre +2 à correlation_level
Menaces distinctes corrélées dans la fenêtre +2 à correlation_level
Temps réel désactivé en mode Normal +2 au risque courant; +2 à la corrélation si menace grave
Antivirus / service désactivé en mode Normal (chaque signal) +2
Surveillance comportementale ou IOAV désactivée +1 chacun
Signatures anciennes de plus de 3 jours / âge inconnu +1

Le résultat est borné à [0,16]. event_level utilise la gravité et l'état de remédiation de cette détection; ni la configuration actuelle du poste ni le nombre d'événements ne la réécrivent. correlation_level agrège les événements associés sur six heures. current_risk_level combine les menaces non résolues, les investigations requises, les corrélations et l'état de protection. Ces poids sont des règles métier Comitari inspirées de Wazuh; ils ne reproduisent pas son moteur. Le statut WAPT utilise le risque courant : 0–6 OK, 7–9 WARNING, 10–16 ERROR. Un échec explicite de remédiation force aussi ERROR, même si le niveau numérique est inférieur à 10. En mode Normal, antivirus, service ou temps réel désactivé force ERROR indépendamment du score. Passive, SxS Passive Mode et EDR Block Mode ne déclenchent pas à eux seuls de faux incident; les menaces restent classifiées.

Quarantined reste non résolu et distinct de Cleaned ou Removed. DidThreatExecute est un signal historique : il ne maintient pas seul une détection correctement supprimée dans active_threats; il positionne execution_observed et requires_investigation. Pour CurrentThreatExecutionStatusID, seuls 3 = Executing signifie une exécution en cours; 2 = Allowed signifie que l'exécution a été autorisée et exige une investigation, mais ne prouve pas qu'elle est en cours. 1 = Blocked n'est pas une preuve de compromission. Le IsActive de Get-MpThreat est une valeur agrégée par ThreatID; il ne rouvre pas un enregistrement DetectionID explicitement résolu, mais le conflit est signalé par requires_investigation. Aucun nom de menace (y compris « ransomware ») n'est interprété comme preuve d'exécution.

Fenêtres et corrélations

  • Fenêtre glissante par défaut : six heures; une détection y entre si sa première détection ou son dernier changement de statut se trouve dans cette fenêtre.
  • Les événements résolus sont conservés dans la collecte courante pendant 30 jours. Toute menace non résolue est conservée sans limite d'ancienneté, donc une menace active de plus de six heures ne disparaît pas.
  • max_event_level_6h porte sur les événements récents, max_active_level sur les menaces non résolues, max_investigation_level sur les incidents demandant une investigation, correlation_level sur les règles de corrélation et current_risk_level sur le risque actuel du poste.
  • Corrélations sur six heures : répétition d'une même menace, menaces distinctes multiples, menace High/Severe avec temps réel désactivé, échecs de remédiation répétés et menace explicitement autorisée.
  • DetectionID est la clé de déduplication documentée, combinée à ThreatID. En son absence, un composite stable de ThreatID, dates, statut et empreinte interne des ressources est utilisé; l'identité incomplète est signalée dans le résumé. Les événements distincts avec des DetectionID différents ne sont pas fusionnés parce qu'ils partagent un ThreatID.
  • Il n'y a plus de limite arbitraire à 100 détections. La collecte transmet tous les événements de moins de 30 jours ainsi que les états non résolus plus anciens. Les données étant complètes ou la collecte échouant explicitement, aucun incident critique n'est silencieusement tronqué.

Audit WAPT et Reporting

Le paquet utilise WAPT.write_audit_data_if_changed(section, key, value, ptype=None, value_date=None, expiration_date=None, max_count=2, keep_days=None) et WAPT.read_audit_data(section, key, default=None, ptype=None, include_expired_data=True), documentées pour WAPT 2.6. Le schéma v3 évite un grand snapshot dupliqué : la clé summary contient le dernier résumé léger (historique limité à 128 changements/90 jours), correlations contient le dernier agrégat de corrélations, et chaque incident a sa clé event.<sha256> avec jusqu'à 16 évolutions/90 jours. Les écritures identiques ne créent pas une nouvelle valeur WAPT. Les événements utilisent un format JSON v1 indépendant de Reporting; aucune requête HTTP ou intégration Vector n'est incluse. Les anciennes clés v2 cessent d'être mises à jour et expirent selon leur rétention d'origine.

Exemple synthétique de structure :

{
  "schema_version": 3,
  "collection_complete": true,
  "defender": {
    "running_mode": "Normal",
    "antivirus_enabled": true,
    "realtime_enabled": true,
    "signature_age": 0
  },
  "summary": {
    "max_event_level_6h": 0,
    "max_active_level": 0,
    "max_investigation_level": 0,
    "correlation_level": 0,
    "current_risk_level": 0,
    "current_risk_category": "Aucun événement",
    "audit_status": "OK",
    "collection_complete": true,
    "detection_count_6h": 0,
    "active_count": 0,
    "investigation_count": 0,
    "remediation_failed": false,
    "resolved_count_6h": 0,
    "identity_incomplete_count": 0,
    "unknown_severity_count": 0,
    "protection_issues": [],
    "correlation_count": 0
  }
}

Chaque clé event.<sha256> conserve un événement compact, par exemple :

{
  "schema_version": 1,
  "event_id": "sha256-stable-par-DetectionID",
  "source": "comi-audit-defender",
  "event_type": "threat.resolved",
  "event_level": 9,
  "remediation_state": "removed",
  "execution_observed": true,
  "requires_investigation": true,
  "active": false,
  "timestamp": "2026-10-27T11:30:00Z"
}

Données et limites de collecte

  • Get-MpThreatDetection fournit le ThreatID, DetectionID, ThreatStatusID, dates de détection/changement/remédiation, code d'erreur, réussite d'action, état d'exécution et ressources. Get-MpThreat est joint en Python par ThreatID pour ThreatName, sévérité, IsActive et DidThreatExecute. Le booléen ActionSuccess=False seul n'est pas un échec : la cmdlet documente sa valeur par défaut à false; le paquet exige un statut d'échec explicite ou un code d'erreur non nul.
  • Les chemins complets des ressources, noms d'utilisateurs et noms de processus ne sont ni sérialisés ni transmis. Seuls le nombre et les catégories file, process, registry, service, url ou other sont conservés.
  • Defender peut ne pas exposer une propriété sur une version donnée. Les valeurs absentes sont traitées comme inconnues; une détection de sévérité inconnue reçoit le niveau de base Unknown. Une panne de la cmdlet entière force ERROR.
  • Les valeurs de ThreatStatusID utilisées viennent de MSFT_MpThreatDetection : 0 Unknown, 1 Detected, 2 Cleaned, 3 Quarantined, 4 Removed, 5 Allowed, 6 Blocked, 102 QuarantineFailed, 103 RemoveFailed, 104 AllowFailed, 105 Abandoned, 107 BlockedFailed. Les différents enums Microsoft peuvent différer selon l'API; ce paquet cible les objets des cmdlets Defender.
  • Defender ne livre pas une chronologie complète des actions humaines ni une preuve d'exécution malveillante. DidThreatExecute est agrégé au niveau ThreatID et peut s'appliquer à plusieurs DetectionID joints au même ThreatID; les enregistrements restent distincts, mais cette attribution agrégée peut nécessiter une investigation. Sans DetectionID, l'identifiant d'événement utilise les dates/ressources disponibles et le résumé signale l'identité incomplète.
  • La réponse PowerShell contient l'ensemble des événements récents et actifs pertinents; le timeout de 90 s provoque un état ERROR, sans limite de 100 qui masquerait une détection.
  • La table WAPT garde les transitions par clé d'incident. Les rapports doivent sélectionner la dernière valeur de chaque event.<sha256>; value_date donne l'âge de la dernière observation enregistrée.

Tests locaux

Les exemples ci-dessous sont également conservés dans docs/WAPT-REPORTING.md.

Les tests utilisent uniquement des données synthétiques et des mocks Python; aucune commande Defender réelle ni WAPT n'est invoquée :

python -m unittest discover -s tests -v

Exemples SQL pour WAPT Reporting

Ces requêtes de lecture ciblent l'onglet Reporting de WAPT Enterprise 2.6 et la table HostAuditData. Elles utilisent le schéma v3 du paquet comit-audit-defender, dans la section Microsoft Defender.

Le schéma v3 ne duplique plus chaque détection dans un snapshot complet. Il conserve :

  • value_key = 'summary' : état léger et historisé, schema_version: 3;
  • value_key = 'correlations' : corrélations courantes et correlation_level;
  • value_key LIKE 'event.%' : derniers états et transitions de chaque incident, chacun avec un event_id stable et schema_version: 1.

Les requêtes sélectionnent le dernier état par poste pour summary, et le dernier état par incident pour event.%. La requête d'événements graves conserve les anciennes transitions récentes afin qu'une résolution ultérieure ne masque pas la détection grave initiale.

1. Tableau de bord — tous les postes

WITH latest AS (
    SELECT DISTINCT ON (host_id)
        host_id,
        value::jsonb AS data,
        value_date
    FROM HostAuditData
    WHERE value_section = 'Microsoft Defender'
      AND value_key = 'summary'
    ORDER BY host_id, value_date DESC, ctid DESC
)
SELECT
    h.uuid AS host_id,
    h.computer_fqdn,
    l.data #>> '{summary,audit_status}' AS audit_status,
    (l.data #>> '{summary,current_risk_level}')::int AS risk_level,
    l.data #>> '{summary,current_risk_category}' AS risk_category,
    (l.data #>> '{summary,active_count}')::int AS active_threats,
    (l.data #>> '{summary,detection_count_6h}')::int AS detections_6h,
    (l.data #>> '{summary,investigation_count}')::int AS investigations,
    l.value_date AS last_data_change
FROM latest l
JOIN hosts h ON h.uuid = l.host_id
ORDER BY risk_level DESC NULLS LAST, h.computer_fqdn;

2. Postes en statut ERROR

WITH latest AS (
    SELECT DISTINCT ON (host_id)
        host_id,
        value::jsonb AS data,
        value_date
    FROM HostAuditData
    WHERE value_section = 'Microsoft Defender'
      AND value_key = 'summary'
    ORDER BY host_id, value_date DESC, ctid DESC
)
SELECT
    h.uuid AS host_id,
    h.computer_fqdn,
    (l.data #>> '{summary,current_risk_level}')::int AS risk_level,
    (l.data #>> '{summary,active_count}')::int AS active_count,
    (l.data #>> '{collection_complete}')::boolean AS collection_complete,
    l.data #> '{summary,protection_issues}' AS protection_issues,
    l.value_date AS last_data_change
FROM latest l
JOIN hosts h ON h.uuid = l.host_id
WHERE l.data #>> '{summary,audit_status}' = 'ERROR'
ORDER BY risk_level DESC NULLS LAST, h.computer_fqdn;

3. Menaces non résolues ou demandant une investigation

Les enregistrements sont dédupliqués au dernier état par incident. Une exécution historique peut exiger une investigation sans faire apparaître la détection comme active.

WITH latest_events AS (
    SELECT DISTINCT ON (host_id, value_key)
        host_id,
        value_key,
        value::jsonb AS data,
        value_date
    FROM HostAuditData
    WHERE value_section = 'Microsoft Defender'
      AND value_key LIKE 'event.%'
    ORDER BY host_id, value_key, value_date DESC, ctid DESC
)
SELECT
    h.uuid AS host_id,
    h.computer_fqdn,
    e.data ->> 'event_id' AS event_id,
    e.data ->> 'event_type' AS event_type,
    (e.data ->> 'event_level')::int AS event_level,
    e.data ->> 'remediation_state' AS remediation_state,
    (e.data ->> 'active')::boolean AS active,
    (e.data ->> 'execution_observed')::boolean AS execution_observed,
    (e.data ->> 'requires_investigation')::boolean AS requires_investigation,
    e.data ->> 'timestamp' AS event_time,
    e.value_date AS last_reported
FROM latest_events e
JOIN hosts h ON h.uuid = e.host_id
WHERE COALESCE((e.data ->> 'active')::boolean, false)
   OR COALESCE((e.data ->> 'requires_investigation')::boolean, false)
ORDER BY event_level DESC NULLS LAST, h.computer_fqdn;

La colonne last_reported permet d'identifier un poste dont l'état n'a pas été actualisé récemment.

4. Événements de niveau élevé observés dans les six dernières heures

La requête conserve une ligne par incident parmi les transitions dont l'heure d'événement est récente. Une transition résolue de faible score ne masque donc pas une transition grave précédente dans la fenêtre.

WITH recent_events AS (
    SELECT DISTINCT ON (host_id, value::jsonb ->> 'event_id')
        host_id,
        value::jsonb AS data,
        value_date
    FROM HostAuditData
    WHERE value_section = 'Microsoft Defender'
      AND value_key LIKE 'event.%'
      AND (value::jsonb ->> 'event_level')::int >= 10
      AND NULLIF(value::jsonb ->> 'timestamp', '')::timestamptz
          >= NOW() - INTERVAL '6 hours'
    ORDER BY host_id, value::jsonb ->> 'event_id', value_date DESC, ctid DESC
)
SELECT
    h.uuid AS host_id,
    h.computer_fqdn,
    e.data ->> 'event_id' AS event_id,
    e.data ->> 'event_type' AS event_type,
    (e.data ->> 'event_level')::int AS event_level,
    e.data ->> 'remediation_state' AS remediation_state,
    (e.data ->> 'active')::boolean AS active,
    (e.data ->> 'timestamp')::timestamptz AS event_time,
    e.value_date AS recorded_at
FROM recent_events e
JOIN hosts h ON h.uuid = e.host_id
ORDER BY event_level DESC, event_time DESC;

L'heure de fenêtre est celle du serveur PostgreSQL. Le paquet conserve les événements sans nom de menace ni chemin utilisateur afin de limiter les données transmises.

5. État des protections

WITH latest AS (
    SELECT DISTINCT ON (host_id)
        host_id,
        value::jsonb AS data,
        value_date
    FROM HostAuditData
    WHERE value_section = 'Microsoft Defender'
      AND value_key = 'summary'
    ORDER BY host_id, value_date DESC, ctid DESC
)
SELECT
    h.uuid AS host_id,
    h.computer_fqdn,
    l.data #>> '{defender,running_mode}' AS running_mode,
    (l.data #>> '{defender,antivirus_enabled}')::boolean AS antivirus_enabled,
    (l.data #>> '{defender,realtime_enabled}')::boolean AS realtime_enabled,
    (l.data #>> '{defender,signature_age}')::int AS signature_age_days,
    l.data #> '{summary,protection_issues}' AS protection_issues,
    l.value_date AS last_data_change
FROM latest l
JOIN hosts h ON h.uuid = l.host_id
WHERE jsonb_array_length(COALESCE(l.data #> '{summary,protection_issues}', '[]'::jsonb)) > 0
ORDER BY h.computer_fqdn;

Le paquet ne signale pas à tort le mode passif comme une panne, notamment lorsqu'un autre antivirus assure la protection.

6. Corrélations courantes

WITH latest AS (
    SELECT DISTINCT ON (host_id)
        host_id,
        value::jsonb AS data,
        value_date
    FROM HostAuditData
    WHERE value_section = 'Microsoft Defender'
      AND value_key = 'correlations'
    ORDER BY host_id, value_date DESC, ctid DESC
)
SELECT
    h.uuid AS host_id,
    h.computer_fqdn,
    l.data ->> 'correlation_level' AS correlation_level,
    c.item ->> 'rule' AS correlation_rule,
    (c.item ->> 'count')::int AS related_count,
    (c.item ->> 'correlation_level')::int AS rule_level,
    l.value_date AS last_data_change
FROM latest l
JOIN hosts h ON h.uuid = l.host_id
CROSS JOIN LATERAL jsonb_array_elements(
    COALESCE(l.data -> 'correlations', '[]'::jsonb)
) AS c(item)
ORDER BY (l.data ->> 'correlation_level')::int DESC NULLS LAST,
         h.computer_fqdn;

Règles possibles : same_threat_repeated, multiple_threats, severe_with_protection_disabled, repeated_remediation_failures et explicitly_allowed.

7. Historique du risque d'un poste

Remplacer PC01.example.local par le nom DNS complet du poste.

SELECT
    h.uuid AS host_id,
    h.computer_fqdn,
    a.value_date AS change_date,
    a.value #>> '{summary,audit_status}' AS audit_status,
    (a.value #>> '{summary,current_risk_level}')::int AS risk_level,
    (a.value #>> '{summary,max_event_level_6h}')::int AS max_event_level_6h,
    (a.value #>> '{summary,active_count}')::int AS active_count,
    (a.value #>> '{summary,correlation_level}')::int AS correlation_level
FROM HostAuditData a
JOIN hosts h ON h.uuid = a.host_id
WHERE a.value_section = 'Microsoft Defender'
  AND a.value_key = 'summary'
  AND h.computer_fqdn = 'PC01.example.local'
ORDER BY a.value_date DESC
LIMIT 128;

L'historique utilise write_audit_data_if_changed() : il contient les changements de résumé, pas chaque exécution de l'audit. Le paquet conserve au plus 128 valeurs de résumé et 32 valeurs de corrélation pendant 90 jours; chaque clé d'événement conserve au plus 16 transitions pendant 90 jours.

Notes et validation

  • Les requêtes sont en lecture seule et supposent les colonnes HostAuditData.value_section, value_key, value, value_date, host_id et les champs hosts.uuid, computer_fqdn utilisés par l'exemple fourni.
  • Les identifiants d'hôte dynamiques utilisent hosts.uuid.
  • Le schéma v2 historique n'est pas interrogé par ces exemples. Les anciennes clés v2 cessent d'être alimentées par le paquet v3 et expirent selon la rétention précédemment définie.
  • Les requêtes SQL sont des exemples construits à partir du schéma du paquet et des colonnes communiquées; elles n'ont pas été exécutées sur une instance WAPT réelle.

Références : WAPT 2.6 — audit et Reporting, WAPT Reporting avancé.

Cloner le paquet dans le dossier WAPT de développement

Dans PowerShell, créez C:\waptdev si nécessaire, puis clonez le dépôt en HTTPS dans C:\waptdev\comi-audit-defender :

New-Item -ItemType Directory -Force "C:\waptdev" | Out-Null
git clone https://git.comitari.fr/paquets-publiques/comi-audit-defender.git "C:\waptdev\comi-audit-defender"
S
Description
No description provided
Readme
101 KiB
0 Stars 3 Watchers 0 Forks
Languages
Python 100%