Varovné záznamy v logu IIS
Vyrovné záznamy v logu IIS
Hrajeme si s geolokačními službami    

To jsem takhle jednou měl depku, že se jako manažer málo dostanu k technologiím a že ztrácím praktické IT znalosti. A do toho jsem řešil problém s uživatelem v Office 365, kde mě administrace Office 365 varovala, že uživatel se přihlásil ze dvou vzdálených míst v krátké době, takže mohlo dojít z zneužití účtu. No a já jsem si řekl, jestli bych byl něco podobného schopný zjistit na on-premise Exchange serveru. No a nějak se to změnilo v mentální cvičení a trénink PowerShellu. Tady je výsledek.

DISCLAIMER: Nejsem programátor ani nijak zvlášť dobrý na PowerShell. Ale jsem dobrý v tom, dostat z minimálních znalostí hodně muziky. Spousta mého kódu by šla přepsat mnohem efektivněji. Spousta věcí je řešena komplikovaně, protože to líp neumím. Ten script obsahuje věci, které tam ani být nemusí. Dal jsem je tam, protože jsem chtěl něco vyzkoušet a pak jsem si s tím hrál dál.

Co budeme potřebovat? Určitě PowerShell 7. Ten naštěstí může fungovat na stejném stroji se starší verzí PowerShellu, takže ani na Exchange serveru to nic nerozbije. Link je tady.

Pak potřebujeme registraci na https://ipapi.com/. Registrace je zdarma, dostanete API KEY, který vám umožní online dotazy do databáze. Pošlete IP adresu, dostanete zeměpisnou šířku, délku a nějaké další informace. Ten free klíč má nějaká omezení na počet dotazů, takže nesdílím nikdy můj klíč, ať mi to někdo nevyžere. Abych ho nenasdílel omylem, tak jsem ho uložil do souboru apikey.txt, který pak na začátku scriptu čtu. Aby to fungovalo, tak se musí do toho souboru přidat před ten klíč otazník. Případně to můžete rovnou dát do proměnné:

$apikey = "?řetězec_vašeho_klíče"

Co vlastně budeme dělat.

  • Vezmeme IIS logy z dvou po sobě jdoucích dnů a nalijeme do tabulky
  • Trochu to vyčistíme, vyházíme zbytečné záznamy.
  • Porovnáme IP adresy s vlastní databází, případně s online databází
  • Ze získaných souřadnic vypočítáme vzdálenost a společně s časem z logu nám to dá rychlost pohybu uživatele
  • Co se nám nebude líbit, to nasázíme do tabulky, kterou pak probereme
  • Výsledky exportujeme do CSV

Níže budu komentovat script, ale není tam vše. Celý script najdete zde: http://www.tnx.cz/dls/process_logs_v3.zip. Normálně script nevrátí žádná data. To znamená, že vám nikdo perníček neloupe. Ale pro potřeby testování si najděte nějaký řádek v logu IIS a vyměňte tam adresu za nějakou vzdálenou. Aby to vrátilo nějaká data. Většinou to dávám někam na konec předchozího dne. Nechci zapisovat do logu, do kterého ještě zapisuje IIS.

Proč dva po sobě jdoucí dny? Takový útočník je noční tvor. A log končí a začíná o půlnoci. Takže neuškodí mít přesah. Předpokládám, že budu script pouštět jako plánovanou úlohu každý den v 6 ráno. Takže budu mít k dispozici data za celý den a zbytek noci. Pokud se v noci něco dělo, dostanu hned ráno informaci. Jelikož mají logy standardní jména, která se liší jen datumem, tak je celkem jednoduché vybrat ty správné.

$today = get-date -Format yyMMdd
$yesterday = ((get-date).AddDays(-1)).tostring('yyMMdd')
$logtoday = ".\u_ex" + $today + ".log"
$logyesterday = ".\u_ex" + $yesterday + ".log"

Teď načteme do proměnných obsah logů. Vynecháváme první tři řádky, tam je nějaký svinčík, který je nám k ničemu.

$Log1 = Get-Content $logyesterday | Where-Object {$_ -notLike "#[D,S-V]*" }
$Log2 = Get-Content $logtoday | Where-Object {$_ -notLike "#[D,S-V]*" }

Na prvním řádku máme záhlaví. To potřebujeme, abychom mohli pojmenovat sloupce v objektu, který vytvoříme. Zároveň jsou tam ale znaky, kterých se ještě potřebujeme zbavit. Řádek rozsekáme podle mezery, dostaneme tedy pole, které bude obsahovat jako členy jména sloupců.

$Columns = (($Log1[0].TrimEnd()) -replace "#Fields: ", "" -replace "-","" -replace "\(","" -replace "\)","").Split(" ")

Teď potřebujeme spočítat, kolik je tam vlastně sloupců. Budeme to potřebovat později. Pak si načteme obsah logů do nových proměnných, ale při tom to trochu protřídíme. Chceme, aby tam zůstaly jen služby Exchange serveru a pouze ty řádky, které obsahují úspěšné připojení (Odpověď serveru HTTP 200). A nakonec data z logů spojíme do jedné proměnné.

$Count = $Columns.Length
$Rows1 = $Log1 | Where-Object {($_ -like "*200*") -and (($_ -like "*Microsoft-Server-ActiveSync*") -or ($_ -like "*EWS*") -or ($_ -like "*OWA*"))}
$Rows2 = $Log2 | Where-Object {($_ -like "*200*") -and (($_ -like "*Microsoft-Server-ActiveSync*") -or ($_ -like "*EWS*") -or ($_ -like "*OWA*"))}
$Rows = $Rows1 + $Rows2

Je čas dostat syrová data z logů do něčeho, kde se nám budou hezky zpracovávat. Použiju objekt typu Systemm.Data.DataTable. Z toho, co jsem nastudoval, je to prý ideální objekt, protože se nejvíc podobá tabulce v SQL databázi. No má to pojmenované sloupce a indexované řádky. S tím se dá udělat dost. Takže ho vytvoříme, pojmenujeme IISLog a nalijeme tam záhlaví (jména sloupců).

$IISLog = New-Object System.Data.DataTable "IISLog"
foreach ($Column in $Columns) {
$NewColumn = New-Object System.Data.DataColumn $Column, ([string])
$IISLog.Columns.Add($NewColumn)
}

No a teď tam nalijeme data. Každý řádek roztrháme podle mezer, nasázíme do sloupců a přidáme do tabulky.

foreach ($Row in $Rows) {
$Row = $Row.Split(" ")
$AddRow = $IISLog.newrow()
for($i=0;$i -lt $Count; $i++) {
$ColumnName = $Columns[$i]
$AddRow.$ColumnName = $Row[$i]
}
$IISLog.Rows.Add($AddRow)
}

Připravíme si další tři tabulky, které budeme potřebovat v průběhu zpracování dat.

$res_cols = @("username","IP1","Country1","IP2","Country2","Distance","Time","Speed","Severity")
$results = New-Object System.Data.DataTable "Results"
foreach ($Col in $res_cols) {
$NewColumn = New-Object System.Data.DataColumn $Col, ([string])
$results.Columns.Add($NewColumn)
}

$speed_cols = @("username","IP1","Country1","IP2","Country2","Distance","Time","Speed")
$speed_results = New-Object System.Data.DataTable "Speed_Results"
foreach ($Col in $speed_cols) {
$NewColumn = New-Object System.Data.DataColumn $Col, ([string])
$speed_results.Columns.Add($NewColumn)
}

$distance_cols = @("username","IP1","Country1","IP2","Country2","Distance","Time","Speed")
$distance_results = New-Object System.Data.DataTable "Distance_Results"
foreach ($Col in $distance_cols) {
$NewColumn = New-Object System.Data.DataColumn $Col, ([string])
$distance_results.Columns.Add($NewColumn)
}

Pokud máte hodně uživatelů, může být IIS log dost velký. Když jsem to zkoušel v práci, tak měl jeden log kolem půl GB. Takže z něj chceme vyhodit všechno, co tam nepotřebujeme. A to mluvím jak o řádcích, tak o sloupcích. Vytvoříme tedy proměnnou $shortlog, do které se dostanou jen čtyři sloupce z IISLog a vynecháme řádky, kde je místo uživatele pomlčka a IP adresa je localhost.

$shortlog = $iislog | Where-Object {($_.csusername -ne '-') -and ($_.cip -ne '127.0.0.1') -and ($_.cip -ne '::1')} | Select-Object date, time, csusername, cip

S úpravami jsme neskončili. Uživatelé jsou schopní udělat velká zvěrstva, když nastavují credentials. Někdo použije netbios doménu s lomítkem, někdo UPN, někdo to spojí dohromady, někdo použije DNS doménu s lomítkem. Potřebujeme to všechno dostat pryč, ať zůstane jen samaccountname. A převedeme všechno na malá písmena. Ve scriptu jsou moje domény, musíte si to nahradit za vaše domény. Kromě toho musíme upravit IP adresy. Pokud je někdo ve firmě, tak bude v logu privátní IP adresa, ze které nedostaneme souřadnice. Takže všechny privátní IP adresy změníme na externí veřejnou IP adresu firmy. Pokud používáte VPN, tak rozsah VPN můžete chtít smazat, protože tam stejně nevíte, kde ten uživatel je. Já to neřeším. Změny pak vracíme do objektu $shortlog.

foreach ($row in $shortlog) {
$ip = $row.cip
$user = $row.csusername
$user = $user.tolower()
if (($ip -like '192.*') -or ($ip -like '10.*') -or ($ip -like '172.*')) {
$ip = '188.244.55.168'
$row.cip = $ip
}
$user = $user -replace "@tnx.cz","" -replace "tnx.cz\\","" -replace "tnx\\","" -replace "@jan-kovar.cz",""
$row.csusername = $user
}

Abychom si nevyčerpali quótu na dotazy do online IP databáze, tak si výsledky budeme ukládat lokálně. A vždy je načteme. Napoprvé nic nemáte, ale po prvním zpracování se vám soubor vytvoří, takže podruhé už bude tahat většinu lokálně. Dál si vytvoříme Hash Table, do které si načteme data z lokální databáze. V Hash Table je klíčem IP adresa a jako hodnota budou důležitá data oddělená středníkem. Jakmile budeme zpracovávat nějakou adresu, tak se podíváme, jestli je klíč v Hash Table. Pokud je, neřešíme. Pokud není, ptáme se online databáze a přidáváme do Hash Table. Budeme zjišťovat zeměpisnou šířku, délku a zemi.

foreach ($address in $iplist) {
$ip = $address.cip
if ($ipdb.ContainsKey($ip)) {
}
else {
$uri = "http://api.ipapi.com/" + $ip + $apikey
$request = Invoke-RestMethod -Method Get -Uri $uri
$Lat = $request.Latitude
$Long = $request.Longitude
$co = $request.country_name
$value = "$lat" + ";" + "$long" + ";" + "$co"
$ipdb.add($ip, $value)
}
}

Teď potřebujeme seznam uživatelů, kteří se vyskytují v logu. Budeme pak brát data toho uživatele, hledat, odkud se přihlásil, jak je to daleko a jak rychle po sobě se přihlásil. A podle toho, co dostaneme, to bude buď OK. Nebo to nemohl stihnout a budeme troubit na poplach. Tak ten seznam uživatelů.

$users = $shortlog | Select-Object csusername | sort-object -Property csusername | Get-Unique -AsString

Teď to bude komplikované na vysvětlení. Tipnul bych si, že na to programátoři mají nějaký postup nebo funkci. Já jsem to musel vymyslet sám. O co jde. Jestliže se jednou připojí uživatel z Číny a pak desetkrát z domova, tak vzledem ke vzdálenosti, to vyhodí deset alertů, protože to ani v jednom případě nemohl stihnout. Jenže pro nás jsou zbytečné. Nám stačí jeden alert. Ten první, tam bude největší rychlost přesunu uživatele. Ale po těch deseti domácích připojeních může přijít zase jedno připojení z daleka a tam bude nejvyšší ryhlost přesunu pro poslední hodnotu. Takže pokud bude za sebou několikrát připojení ze stejné adresy, necháváme první a poslední.

Teď to bude komplikované na vysvětlení. Tipnul bych si, že na to programátoři mají nějaký postup nebo funkci. Já jsem to musel vymyslet sám. O co jde. Jestliže se jednou připojí uživatel z Číny a pak desetkrát z domova, tak vzledem ke vzdálenosti, to vyhodí deset alertů, protože to ani v jednom případě nemohl stihnout. Jenže pro nás jsou zbytečné. Nám stačí jeden alert. Ten první, tam bude největší rychlost přesunu uživatele. Ale po těch deseti domácích připojeních může přijít zase jedno připojení z daleka a tam bude nejvyšší ryhlost přesunu pro poslední hodnotu. Takže pokud bude za sebou několikrát připojení ze stejné adresy, necháváme první a poslední. Následuje problém s tím, že nemůžeme mazat hodnoty v poli, protože pole má fixní velikost. Takže měním formát a vracím zpět. Řádky pole musím mazat od zadu, protože smazáním řádku se mění indexy řádků za tím smazaným a mazal bych nesprávné řádky. No a dál porovnávám řádky ze stejného pole a nechci porovnávat každý s každým. Jestliže jsem první řádek porovnal s ostatními, tak už druhý řádek nechci porovnávat s prvním. Chci porovnávat A-B, A-C, B-C. Takže porovnávám řádek s řádkem, který má index o jeden vyšší. A musím dát pozor na poslední řádek, protože o index výš už nic není. Není to úplně srozumitelné.

Pojďme na část kódu, kde se už opravdu řeší místo připojení a vzdálenost. Máme dvě IP adresy a dva datumy a časy připojení. Pokud je mezi nimi víc než 12 hodin, tak to neřešíme. Vytáhneme si z naší IP databáze data pro obě IP adresy. Data máme oddělené středníkem, takže to musíme roztrhat a načíst do proměnných.

$process1 = $ipdb[$ip1]
$processed1 = $process1 -split ";"
$latitude1 = $processed1[0]
$longitude1 = $processed1[1]
$country1 = $processed1[2]

$process2 = $ipdb[$ip2]
$processed2 = $process2 -split ";"
$latitude2 = $processed2[0]
$longitude2 = $processed2[1]
$country2 = $processed2[2]

Přichází čas matematiky. My jsme dostali zeměpisnou šířku a délku v stupních. Jenže vzorec je potřebuje v radiánech. Takže to přepočítáme.

$latitude1 = ([math]::PI /180) * $latitude1
$latitude2 = ([math]::PI /180) * $latitude2
$longitude1 = ([math]::PI /180) * $longitude1
$longitude2 = ([math]::PI /180) * $longitude2

No a teď můžeme vypočítat vzdálenost a s pomocí času mezi jednotlivými připojeními i rychlost, jakou se musel uživatel pohybovat, aby to stihl.

$distance = [math]::acos([math]::cos($latitude1)*[math]::cos($longitude1)*[math]::cos($latitude2)*[math]::cos($longitude2) + [math]::cos($latitude1)*[math]::sin($longitude1)*[math]::cos($latitude2)*[math]::sin($longitude2) + [math]::sin($latitude1)*[math]::sin($latitude2)) * 6371

$speed = $distance / $howlong

Cokoliv do rychlosti 100km /hod a vzdálenosti 300km ignoruju. Ještě si tam hraju s tím, jak moc je co závažné podle rychlosti, jakou se uživatel pohyboval. Pokud se uživatel pohyboval rychleji než 100km/hod a vzdálenost byla větší než 300km, tak výsledky zaokrouhlím a vložím do datatable results, kterou jsem vytvořil výše.

$distance = [math]::Round($distance)
$howlong = [math]::Round($howlong,4)
$speed = [math]::Round($speed)

$AddRow = $results.newrow()
$AddRow.username = $name
$AddRow.IP1 = $ip1
$AddRow.Country1 = $country1
$AddRow.IP2 = $ip2
$AddRow.Country2 = $country2
$AddRow.Distance = $distance
$AddRow.Time = $howlong
$AddRow.Speed = $speed
$AddRow.Severity = $sev
$results.Rows.Add($AddRow)

Uložíme výsledky a databázi IP adress.

$ipdb.GetEnumerator() | Select-Object -Property key, value | Export-csv -NoTypeInformation -Delimiter ',' -Path .\ip_database.txt

$results | export-csv -NoTypeInformation $results_path

I když jsme se snažili vyházet některé záznamy, tak bude ve výsledcích spousta redundadních záznamů. Tak se to pokusíme ještě trochu vytřídit. Nejdřív vybereme uživatele, kteří měli nějaký vroubek. Pak vytvoříme dvě prázdné Hash Table.

$users = $results | Select-Object username | sort-object -Property username | Get-Unique -AsString

$distance_db = @{}
$speed_db = @{}

Pro každého uživatele si vytáhneme jeho záznamy. Je tam hodně záznamů se stejnou kombinací IP adres. Nepotřebujeme všechny záznamy. Co se týká vzdálenosti, tak pro kombinaci dvou stejných adres bude vždycky stejná. Ale podle toho, jak rychle po sobě vznikly záznamy, se bude lišit rychlost. Nepotřebujeme tam mít všechny, stačí, když tam necháme řádek s nejvyšší rychlostí. Takže vybereme záznamy se stejnou kombinací IP adres, seřadíme podle rychlosti od nejvyšší a pak vezmemme řádek s indexem 0. Bacha, máme všechny hodnoty jako string, takže musíme říct, že Distance a Speed jsou čísla.

$small_users = $results | Where-Object {$_.username -eq $who}

foreach ($line in $small_users) {
....
$sort = @($small_users | Where-Object {(($_.IP1 -eq $ip1) -and ($_.IP2 -eq $ip2)) -or (($_.IP2 -eq $ip1) -and ($_.IP1 -eq $ip2))} | Sort-Object {[int]$_.Speed} -Descending)

Bude to trochu čuňárna. Abychom věděli, kterou kombinaci IP adres už jsme řešili, tak ji vložíme jako index do Hash Table. A protože se řeší každý uživatel zvlášť, tak klíčem, Hash Table bude jméno uživatele, IP1 a IP2 oddělené středníkem. A jako hodnota bude jméno uživatele, IP1, země1, IP2, země2, vzdálenost, čas, rychlost. A protože se IP adresy můžou v proměnných IP1 a IP2 prohodit, tak musíme kontrolovat, jestli tam nejsou obě kombinace. Pokud jsou, už jsme to zpracovali a jdeme dál.

$ip_res1 = $sort[0].IP1
$ip_res2 = $sort[0].IP2
$co_res1 = $sort[0].Country1
$co_res2 = $sort[0].Country2
$dis_res = $sort[0].Distance
$time_res = $sort[0].Time
$speed_res = $sort[0].Speed
$value = $who + ";" + $ip_res1 + ";" + $co_res1 + ";" + $ip_res2 + ";" + $co_res2 + ";" + $dis_res + ";" + $time_res + ";" + $speed_res
$speed_db.add($hash_key1, $value)

Stejným způsobem nalijeme data i do Hash Table pro Distance. Obě ale mají data uložená takovým způsobem, že když to exportujeme do CSV, tak to bude čtení za trest. Takže použijeme datatable, které jsme si vytvořili už dříve. A přesuneme do nich data z HashTable pomocí extra proměnné, tedy pole.

$distance_arr = @($distance_db.GetEnumerator() | Select-Object -Property key, value)
foreach ($line in $distance_arr) {
$data = $line.value.Split(";")
$AddRow = $speed_results.newrow()
$AddRow.username = $data[0]
$AddRow.IP1 = $data[1]
$AddRow.Country1 = $data[2]
$AddRow.IP2 = $data[3]
$AddRow.Country2 = $data[4]
$AddRow.Distance = $data[5]
$AddRow.Time = $data[6]
$AddRow.Speed = $data[7]
$speed_results.Rows.Add($AddRow)
}

A stejně naplníme i druhou datatable pro Distance. Pak můžeme obě tabulky exportovat do CSV.

$speed_results | Export-Csv -NoTypeInformation -Delimiter "," $speed_path

$distance_results | Export-Csv -NoTypeInformation -Delimiter "," $distance_path

Poslední aktualizace - Neděle, 26. Března 2023 18:20
 
 
 
Page 1 of 1
© TNX alias Jan Kovář. Původní design stránky byl určen pro CMS Joomla! a vytvořen společností Siteground web hosting