Qualche pensiero su Audit Collection System

[Traduzione del mio post originale in inglese http://www.muscetta.com/2010/03/18/a-few-thoughts-on-sizing-audit-collection-system/ ]

Già da diversi anni, molti clienti collezionavano gli event logs con MOM – perchè non anche i Security log? Alcuni in realtà lo facevano, ma non era abbastanza scalabile; per questo motivo, qualche anno fa Eric Fitzgerald announciò che stava lavorando a Microsoft Audit Collection System. Tuttavia, il tool dal lui scritto non aveva interfaccia grafica… e il resto è storia: esso è stato integrato in System Center Operations Manager. Eppure, ACS resta un componente poco conosciuto di OpsMgr.

Ci sono una serie di risorse sul web che vale la pena ricordare e linkare:

e molti, molti altri – non posso linkarli tutti!

Per quanto mi riguarda, ho iniziato a giocare con ACS fin dai tempi delle prime beta (prima di iniziare a lavorare in Microsoft e ritornare ad usare MOM, nel periodo in cui mi occupavo di sicurezza informatica), ma non ho mai bloggato relativamente a questo pezzo di software.

Dal momento che, di recente, ho svolto parecchio lavoro su ACS (per questioni legate alle normative del Garante della Privacy), ho pensato che valesse la pena raccogliere alcuni pensieri a riguardo in un post – ed eccolo qui.

Anatomia di un calcolo delle dimensioni effettuato “online”

Ciò che vorrei spiegare qui è la strategia e il processo che seguo per analizzare i dati raccolti in un database ACS allo scopo di determinare la migliore strategia di filtraggio: cosa tenere e cosa no – applicando un filtro sul Collector ACS.

La prima cosa con cui parto è di solito uno dei vari fogli Excel che esistono in rete e promettono di aiutare con il sizing del database…. che tuttavia fanno in genere un calcolo del caso peggiore, suggerendo di allocare molto più spazio di quello che poi si dimostra effettivamente necessario. Non riesco a capacitarmi di come alcune persone possano effettivamente calcolare lo spazio richiesto PRIMA dell’implementazione e solo sulla base di formule teoriche (basate su frequenza di eventi al secondo, ruoli dei server, etc), ma io preferisco un approccio molto più “terra terra”: osservo i dati che ACS sta collezionando in assenza di filtri (raccogliendo quindi tutto) per alcuni giorni, e parto da questo dato per rendermi conto di quanto sarebbe la dimensione “a regime” su un periodo più ampio.

Nel caso di una nuova istallazione questo è facile: si accende ACS, si imposta la retention a qualche giorno (una o due settimane massimo), si alloca una dimensione iniziale del db che dovrebbe essere sufficiente a contenere dati per quei giorni, si agganciano tutti i forwarders (agenti)….. e ci si siede ad aspettare di vedere cosa succede.

Quando si torna a vedere 2 settimana dopo avremo un’idea molto chiara del tipo e quantità di eventi che i miei forwarder normalmente tracciano.

Quali e quanti dati stiamo raccogliendo?

Prima di tutto, se non abbiamo cambiato i parametri di grooming dal default, le procedure di grooming creeranno una nuova partizione ogni giorno. Pertanto, il mio primo passo è quello di analizzare quando è grande tale “partizione”. Ma che cos’è esattamente una “partizione” in ACS? E’ un set di 4 tabelle unite da una JOIN:

  1. dtEvent_GUID
  2. dtEventData_GUID
  3. dtPrincipal_GUID
  4. dtSTrings_GUID

dove GUID rappresenta un nuoco  GUID per ogni nuovo giorni, e le 4 tabelle che hanno lo stesso GUIDE, una volta JOINate assieme permettono di accedere a tutti I dati raccolti in una determinata giornata.

Nel database, la tabella dtPartition contiene invece la lista di tutte le partizioni e dei loro GUID, assieme ad informazioni quali la data e ora in cui tali partizioni sono state “aperte” (iniziate) e “chiuse”.

Tanto per avere una stima di massima, possiamo in genere ignorare lo spazio occupato dalle ultime tre tabelle – che sono di solito di modesta dimensione – e solo tenere conto della tabella dtEvent_GUID per ricavare il numero di eventi per quella partizione/giorno: usando la  stored procedure “sp_spaceused”  su quella tabella otteniamo anche la dimensione di quella partizione nel database. In questo modo finisco per avere una tavola come la seguente:

Partition ID Status Partition Start Time Partition Close Time Rows Reserved KB Total GB
9b45a567_c848_4a32_9c35_39b402ea0ee2 0 2/1/2010 2:00 2/1/2010 2:00 29,749,366 7,663,488 7,484
8d8c8ee1_4c5c_4dea_b6df_82233c52e346 2 1/31/2010 2:00 2/1/2010 2:00 28,067,438 9,076,904 8,864
34ce995b_689b_46ae_b9d3_c644cfb66e01 2 1/30/2010 2:00 1/31/2010 2:00 30,485,110 9,857,896 9,627
bb7ea5d3_f751_473a_a835_1d1d42683039 2 1/29/2010 2:00 1/30/2010 2:00 48,464,952 15,670,792 15,304
ee262692_beae_4d81_8079_470a54567946 2 1/28/2010 2:00 1/29/2010 2:00 48,980,178 15,836,416 15,465
7984b5b8_ddea_4e9c_9e51_0ee7a413b4c9 2 1/27/2010 2:00 1/28/2010 2:00 51,295,777 16,585,408 16,197
d93b9f0e_2ec3_4f61_b5e0_b600bbe173d2 2 1/26/2010 2:00 1/27/2010 2:00 53,385,239 17,262,232 16,858
8ce1b69a_7839_4a05_8785_29fd6bfeda5f 2 1/25/2010 2:00 1/26/2010 2:00 55,997,546 18,105,840 17,681
19aeb336_252d_4099_9a55_81895bfe5860 2 1/24/2010 2:00 1/24/2010 2:00 28,525,304 7,345,120 7,173
1cf70e01_3465_44dc_9d5c_4f3700dc408a 2 1/23/2010 2:00 1/23/2010 2:00 26,046,092 6,673,472 6,517
f5ec207f_158c_47a8_b15f_8aab177a6305 2 1/22/2010 2:00 1/22/2010 2:00 47,818,322 12,302,208 12,014
b48dabe6_a483_4c60_bb4d_93b7d3549b3e 2 1/21/2010 2:00 1/21/2010 2:00 55,060,150 14,155,392 13,824
efe66c10_0cf2_4327_adbf_bebb97551c93 2 1/20/2010 2:00 1/20/2010 2:00 58,322,217 15,029,216 14,677
0231463e_8d50_4a42_a834_baf55e6b4dcd 2 1/19/2010 2:00 1/19/2010 2:00 61,257,393 15,741,248 15,372
510acc08_dc59_482e_a353_bfae1f85e648 2 1/18/2010 2:00 1/18/2010 2:00 64,579,122 16,612,512 16,223

Se ACS è stato istallato da poco e sta girando senza filtri con tutti gli agenti per un paio di settimane, potreste ottenere dei grafici che somigliano a quelli mostrati qui sotto (ricavati dai valori nella tavola qui sopra), che mostrano il trend "settimanale”:

Trend: Space user by day

Trend: Number of events by day

Nell’esempio mostrato si riconosce senza dubbio un pattern settimanale (lunedì-venerdì hanno molti più eventi rispetto ai fine settimana) e possiamo osservare che, sebbene variabili in dimensione, la partizione più grande osservata è di circa 17GB. Arrotondandola a 20GB (tenendo presente che non abbiamo preso in considerazione le tre tabelle aggiuntive ma solo la prima – e principale – della partizione) possiamo predire  20*7 = 140GB a settimana. Dal momento che abbiamo arrotondato per eccesso e che I fine settimana sono più “tranquilli”, questo dovrebbe darci un giosto “cuscinetto” per permettere al sistema di sopportare eventuali “event storms” che dovessero avvenire (in assenza di filtri, la quantità di dati giornaliera è molto più variable – immaginate ad esempio cosa succede con worm che provano a forzare password, e simili “pesti”). E’ oltretutto sempre suggerito avere una certa quantità di spazio libero nel db per permettere alle operazioni di manutenzione e re-indicizzazione di funzionare correttamente.

Ad ogni modo, nell’esempio di cui sopra, questo cliente voleva conservare gli eventi online del database per sei mesi (180giorni) che avrebbero occupato 20*180 = 3600GB = TRE TERABYTE e MEZZO! In questi casi abbiamo veramente bisogno di una strategia di filtraggio per mantenere la dimensione del database sotto controllo e ridurre i costi di gestione (e backup!) dello stesso.

[Nota – se volete automatizzare l’analisi appena spiegata e produrre una tavola e grafici come quelli appena mostrati, visitate l’altro mio post successivo, dove ho condiviso un report pronto per essere usato]

Strategie di Filtering

Una volta osservata la quantità totale degli eventi, bisogna iniziare a guardare di quali eventi si tratta: da quali macchine, per quali utenti, per quali operazione, etc. Questo tipo di analisi va fatta tramite query, o report… ma in genere io preferisco le query perchè posso inventarne variazioni in modo molto veloce.

Non mi sofferemrò sul COME scrivere un filtro in questo articolo: un filtro per il collector è una WMI Notification query, e come scriverlo ed impostarlo è stato già discusso e spiegato molto bene altrove: nella documentazione di SecureVantage sopracitata, ad esempio, e anche nella documentazione ufficiale Microsoft.

Quello su cui mi voglio soffermare qui è, invece, il processo che io seguo e le query che uso per capire da dove viene il “rumore” e quali eventi potrebbero essere filtrati – in modo da ottenere delle stime di quanto spazio disco riusciremmo a risparmiare con l’implementazione di un certo tipo di filtro, o di un altro.

Numero di eventi per utente

--event count by User (with Percentages)
declare @total float
select @total = count(HeaderUser) from AdtServer.dvHeader
select count(HeaderUser),HeaderUser, cast(convert(float,(count(HeaderUser)) / (convert(float,@total)) * 100) as decimal(10,2))
from AdtServer.dvHeader
group by HeaderUser
order by count(HeaderUser) desc

Nel nostro esempio di sopra, nei 14 giorni di osservazione, otteniamo le percentuali seguenti:

Numero di Eventi HeaderUser Account Percentuale
204,904,332 SYSTEM 40.79 %
18,811,139 LOCAL SERVICE 3.74 %
14,883,946 ANONYMOUS LOGON 2.96 %
10,536,317 appintrauser 2.09 %
5,590,434 mossfarmusr

Ci basta guardare questo dati per capire che, filtrando gli eventi tracciati dai soli account “SYSTEM”, “LOCAL SERVICE” e “ANONYMOUS”, risparmieremmo più del 45% di spazio disco!

Numero di Events per EventID

Allo stesso modo, possiamo osservare come diversi Event IDs abbiano diversi “pesi” sul totale degil eventi tracciati nel database.

--event count by ID (with Percentages)
declare @total float
select @total = count(EventId) from AdtServer.dvHeader
select count(EventId),EventId, cast(convert(float,(count(EventId)) / (convert(float,@total)) * 100) as decimal(10,2))
from AdtServer.dvHeader
group by EventId
order by count(EventId) desc

Otteniamo le seguenti informazioni:

Event ID Significato/Descrizione Totale di eventi Percentuale
538 A user logged off 99,494,648 27.63
540 Successful Network Logon 97,819,640 27.16
672 Authentication Ticket Request 52,281,129 14.52
680 Account Used for Logon by (Windows 2000) 35,141,235 9.76
576 Specified privileges were added to a user's access token. 26,154,761 7.26
8086 Custom Application ID 18,789,599 5.21
673 Service Ticket Request 10,641,090 2.95
675 Pre-Authentication Failed 7,890,823 2.19
552 Logon attempt using explicit credentials 4,143,741 1.15
539 Logon Failure - Account locked out 2,383,809 0.66
528 Successful Logon 1,764,697 0.49

Io amo le query, ma non dimentichiamo che ACS mette anche a disposizione alcuni report per fare questo tipo di analisi – anche se per quanto sperimentato personalmente, sono di solito più lenti – specialmente su grandi quantità di dati. Inoltre ci sono stati una serie di bug relativamente ai report nelle varie versioni, pertanto preferisco continuare ad usare le mie query. Sotto è mostrato uno di questi report (sebbene lo screenshot è di un  ambiente diverso da quello di sopra – nel caso vi steste domandando come mai i numeri non corrispondono).

Event Counts ACS Default Report

Questi numeri e percentuali che abbiamo ottenuto dalle prime due query dovrebbero già orientarci in qualche direzione e farci pensare se dobbiamo cambiare/ritoccare la politica di audit direttamente su Windows, o se vogliamo interevenire con un filtro – qui dobbiamo domandarci “se non lo vogliamo collezionare, perchè dovremmo tracciarlo sui sistemi?” – tuttavia non sempre è possibile ottenere la granularità desiderata su Windows, specialmente nelle versioni più vecchie (Windows 2008 ha portato diversi cambiamenti in questo senso).

Inoltre, con una piccola permutazione delle query mostrate prima, possiamo osservare quali utenti stiamo generando la maggior parte del “rumore”, e ottenere il dettaglio di quali EventID, ad esempio la seguente:

--event distribution for a specific user (change the @user) - with percentages for the user and compared with the total #events in the DB
declare @user varchar(255)
set @user = 'SYSTEM'
declare @total float
select @total = count(Id) from AdtServer.dvHeader
declare @totalforuser float
select @totalforuser = count(Id) from AdtServer.dvHeader where HeaderUser = @user
select count(Id), EventID, cast(convert(float,(count(Id)) / convert(float,@totalforuser) * 100) as decimal(10,2)) as PercentageForUser, cast(convert(float,(count(Id)) / (convert(float,@total)) * 100) as decimal(10,2)) as PercentageTotal
from AdtServer.dvHeader
where HeaderUser = @user
group by EventID
order by count(Id) desc

La query appena mostrata è particolarmente importante poichè, ad esempio, potremmo voler filtrare una serie di eventi dall’utente SYSTEM account (per esempio i logon che avvengono quando servizi vengono avviati e fermati sui server) ma potremmo invece voler mantenere altri eventi tracciati da SYSTEM che riteniamo più importanti ai fini di audit, come ad esempio l’evento generato quando il security log viene svuotato da un amministratore:

Event ID 517 Audit Log was cleared

andando quindi ad osservare la quantità di event ID 517 sul totale degli eventi tracciati da SYSTEM in generale, questi non saranno poi così tanti – possiamo decidere di tenere questi (che sono utili) e scartare gli altri.

Numero di Events per EventID e per User

Possiamo quindi combinare I due approcci: per utente  e per EventID

select count(Id),HeaderUser, EventId
from AdtServer.dvHeader
group by HeaderUser, EventId
order by count(Id) desc

Questo produrrà risultati come i seguenti:

SQL Query: Events by EventID and by User

che possiamo agilmente copiare e incollare in Excel e produrre una tabella Pivot:

Pivot Table

Cluster EventLog Replication

Un ulteriore aspetto che credo non sia molto noto – pertanto ritengo utile mostrare – è il modo in cui i cluster si “comportano” in relazione ad ACS. In realtà non tutti i cluster, ma se la feature di “eventlog replication” è abilitata (che in realtà dovrebbe essere disabilitata, anche per il monitoring….)ciascun nodo del cluster avrà (ed invierà ad ACS collector) eventi non solo per sé stesso ma anche per tutti gli altri nodi.

Non ho ancora trovato un modo per filtrare questi eventi duplicati – a meno di disabilitare del tutto l’eventlog replication – comunque è interessante conoscere questo fenomeno e rendersi conto di quanto sia il peso di questi eventi sul totale. Per fare questo io uso la query seguente che evidenzia gli eventi tracciati da una macchina per conto di un’altra macchina (macchina == network name).

 

--to spot machines that are cluster nodes with eventlog repliation and write duplicate events (slow)

select Count(Id) as Total,replace(right(AgentMachine, (len(AgentMachine) - patindex('%\%',AgentMachine))),'$','') as ForwarderMachine, EventMachine
from AdtServer.dvHeader
--where ForwarderMachine <> EventMachine
group by EventMachine,replace(right(AgentMachine, (len(AgentMachine) - patindex('%\%',AgentMachine))),'$','')
order by ForwarderMachine,EventMachine

Cluster Events

Conclusioni

Quanto mostrato fin’ora è solo una serie di tecniche e “trucchi del mestiere” che uso per analizzare il sizing di un database ACS. Ci sono ovviamente altri modi di osservare questi dati. Di seguito includo tutta una serie di altre query che mi tornano utili in questo tipo di ingaggi presso i clienti. Ce ne potranno essere certamente altre – con piccole modifiche alle query è possibile “osservare” i dati secondo una prospettiva diversa, a seconda delle necessità.

Ho anche pensato che fosse comodo avere tutta una serie di query raccolte/collezionate in una unica pagina, un poco come quella di Kevin Holman per OpsMgr (tra parentesi, alcune di quelle query le abbiamo scritte assieme per l’OpsMgr Health Check!). Enjoy!

Query utilil

--top event ids
select count(EventId), EventId
from AdtServer.dvHeader
group by EventId
order by count(EventId) desc

--event count by ID (with Percentages)
declare @total float
select @total = count(EventId) from AdtServer.dvHeader
select count(EventId),EventId, cast(convert(float,(count(EventId)) / (convert(float,@total)) * 100) as decimal(10,2))
from AdtServer.dvHeader
group by EventId
order by count(EventId) desc

--which machines have ever written event 538
select distinct EventMachine, count(EventId) as total
from AdtServer.dvHeader
where EventID = 538
group by EventMachine

--machines
select * from dtMachine

--machines (more readable)
select replace(right(Description, (len(Description) - patindex('%\%',Description))),'$','')
from dtMachine

--events by machine
select count(EventMachine), EventMachine
from AdtServer.dvHeader
group by EventMachine

--rows where EventMachine field not available (typically events written by ACS itself for chekpointing)
select *
from AdtServer.dvHeader
where EventMachine = 'n/a'

--event count by day
select convert(varchar(20), CreationTime, 102) as Date, count(EventMachine) as total
from AdtServer.dvHeader
group by convert(varchar(20), CreationTime, 102)
order by convert(varchar(20), CreationTime, 102)

--event count by day and by machine
select convert(varchar(20), CreationTime, 102) as Date, EventMachine, count(EventMachine) as total
from AdtServer.dvHeader
group by EventMachine, convert(varchar(20), CreationTime, 102)
order by convert(varchar(20), CreationTime, 102)

--event count by machine and by date (distinuishes between AgentMachine and EventMachine
select convert(varchar(10),CreationTime,102),Count(Id),EventMachine,AgentMachine
from AdtServer.dvHeader
group by convert(varchar(10),CreationTime,102),EventMachine,AgentMachine
order by convert(varchar(10),CreationTime,102) desc ,EventMachine

--event count by User
select count(Id),HeaderUser
from AdtServer.dvHeader
group by HeaderUser
order by count(Id) desc

--event count by User (with Percentages)
declare @total float
select @total = count(HeaderUser) from AdtServer.dvHeader
select count(HeaderUser),HeaderUser, cast(convert(float,(count(HeaderUser)) / (convert(float,@total)) * 100) as decimal(10,2))
from AdtServer.dvHeader
group by HeaderUser
order by count(HeaderUser) desc

--event distribution for a specific user (change the @user) - with percentages for the user and compared with the total #events in the DB
declare @user varchar(255)
set @user = 'SYSTEM'
declare @total float
select @total = count(Id) from AdtServer.dvHeader
declare @totalforuser float
select @totalforuser = count(Id) from AdtServer.dvHeader where HeaderUser = @user
select count(Id), EventID, cast(convert(float,(count(Id)) / convert(float,@totalforuser) * 100) as decimal(10,2)) as PercentageForUser, cast(convert(float,(count(Id)) / (convert(float,@total)) * 100) as decimal(10,2)) as PercentageTotal
from AdtServer.dvHeader
where HeaderUser = @user
group by EventID
order by count(Id) desc

--to spot machines that write duplicate events (such as cluster nodes with eventlog replication enabled)
select Count(Id),EventMachine,AgentMachine
from AdtServer.dvHeader
group by EventMachine,AgentMachine
order by EventMachine

--to spot machines that are cluster nodes with eventlog repliation and write duplicate events (better but slower)
select Count(Id) as Total,replace(right(AgentMachine, (len(AgentMachine) - patindex('%\%',AgentMachine))),'$','') as ForwarderMachine, EventMachine
from AdtServer.dvHeader
--where ForwarderMachine <> EventMachine
group by EventMachine,replace(right(AgentMachine, (len(AgentMachine) - patindex('%\%',AgentMachine))),'$','')
order by ForwarderMachine,EventMachine

--which user and from which machine is target of elevation (network service doing "runas" is a 552 event)
select count(Id),EventMachine, TargetUser
from AdtServer.dvHeader
where HeaderUser = 'NETWORK SERVICE'
and EventID = 552
group by EventMachine, TargetUser
order by count(Id) desc

--by hour, minute and user
--(change the timestamp)... this query is useful to search which users are active in a given time period...
--helpful to spot "peaks" of activities such as password brute force attacks, or other activities limited in time.
select datepart(hour,CreationTime) as Hours, datepart(minute,CreationTime) as Minutes, HeaderUser, count(Id) as total
from AdtServer.dvHeader
where CreationTime < '2010-02-22T16:00:00.000'
and CreationTime > '2010-02-22T15:00:00.000'
group by datepart(hour,CreationTime), datepart(minute,CreationTime),HeaderUser
order by datepart(hour,CreationTime), datepart(minute,CreationTime),HeaderUser