<?php
namespace Modules\Admin\Server\Logs; class ModelGenerateReportLoginStats { public function generate($outputFile, $listingParams) { $model = new Model(); $listingParams->setCurrentPage(0); $listingParams->setResultsPerPage(PHP_INT_MAX); $model->list_execute($listingParams); $listRows = $model->list_getRecordsList(); $fileHandle = fopen($outputFile, 'w'); fwrite($fileHandle, <<<'HTML'
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>TINA auth logs</title>

	<script src="https://code.jquery.com/jquery-3.6.0.slim.min.js"></script>

	<style>
		pre.output {
			margin-top: 2em;
		}

		.autoselect {
			cursor: pointer;
			user-select: all;
		}
		.noselect {
			user-select: none;
			cursor: default;
		}

		span.id {
			float: right;
			color: #88f;
			padding: 0 0.5em;
		}
		span.addTime {
			float: right;
			color: #888;
			padding: 0 0.5em;
		}
		div.line {
			line-height: 1;
			border-bottom: 1px solid #444;
			padding-bottom: 0.5em;
		}
		div.line >div.message >span.IP {
			color: hsl(5, 75%, 40%);
		}
		div.line >div.message >span.username {
			color: hsl(55, 75%, 30%);
		}

		div.HTTP_USER_AGENT >div.HTTP_USER_AGENT {
			color: hsl(180, 50%, 25%);
		}

		section.log-header {
			position: fixed;
			width: 100%;
			top: 0;
			left: 0;
			background-color: hsl(180,  25%, 95%);
			border-bottom: 2px solid hsl(180,  25%, 90%);
			z-index: 1;
		}

		.log-header >#stats {
			float: right;
			margin-left: 0.5em;
			padding-left: 0.5em;
			border-left: 1px solid #ccc;

			font-size: 0.75em;
			color: hsl(180,  25%, 33%);
		}
	</style>
</head>
<body>

<section class="log-header">
	<span 	id="stats">loading ...</span>
		<button onClick="deleteDuplicates()">delete duplicates</button>
	<button onClick="sortByDate()">sort everything by date</button>

	<script>
		function deleteDuplicates() {
			let uniqueHashes = [...new Set($('pre.output >div.line').map((i, a) => {
				return $(a).attr('data-hash');
			}))];

			let newOutput = '';
			uniqueHashes.forEach((h) => {
				newOutput+= $(`pre.output >div.line[data-hash=${h}]`)[0].outerHTML;
			});
			// seems it's faster to remove the whole element and add it back
			$(`pre.output`).remove();
			$('.log-header').after(`<pre class="output">${newOutput}</pre>`);

			generateStats();
		}

		function sortByDate() {
			let rows = $('pre.output >div.line')
				.sort( (a, b) => {
					let a1 = $(a).find('span.addTime').html();
					let b1 = $(b).find('span.addTime').html();

					if (a1==null) {
						a1 = '';
					}

					if (b1==null) {
						b1 = '';
					}

					return a1.localeCompare(b1);
				}
			);

			$('pre.output').append(rows);
		}

		function generateStats() {
			let totalRows = new Intl.NumberFormat().format($('pre.output >div.line').length);
			$('#stats').html(`got ${totalRows} rows`);
		}

		$(window).on( "load", function() {
			generateStats();
		});
	</script>
</section>

<pre class="output">
HTML
); $rowIndex = 0; $reportedGroupedData = [ 'HTTP_USER_AGENT' => [], 'authCredentials.username' => [], 'REMOTE_ADDR' => [], ]; $JSONReport = [ 'rawData' => [], ]; foreach ($listRows as $listRow) { if (!preg_match("/^\[Modules\\\\Authenticate\\\\Controller::actionIndex\].+/", $listRow['message'])) { continue; } $logRow = $model->getFullLogDetails($listRow['id']); if (!$logRow) { continue; } if (!$logRow['context']) { continue; } $SERVER_value = ($logRow['context']['$_SERVER']?$logRow['context']['$_SERVER']->getValue():[]); $messageHash = md5($listRow['message'] . $logRow['context']['authCredentials.username'] . $SERVER_value['HTTP_USER_AGENT'] . $SERVER_value['REMOTE_ADDR']); $reportedGroupedData['HTTP_USER_AGENT'][$SERVER_value['HTTP_USER_AGENT']]++; $reportedGroupedData['authCredentials.username'][$logRow['context']['authCredentials.username']]++; $reportedGroupedData['REMOTE_ADDR'][$SERVER_value['REMOTE_ADDR']]++; $JSONReport['rawData'][] = [ 'addTime' => $listRow['addTime'], 'HTTP_USER_AGENT' => $SERVER_value['HTTP_USER_AGENT'], 'authCredentials.username' => $logRow['context']['authCredentials.username'], 'REMOTE_ADDR' => $SERVER_value['REMOTE_ADDR'], ]; $colorIndex = strlen($listRow['message']??'') + crc32($logRow['context']['authCredentials.username']??''); $colorIndex = (round($colorIndex/10) * 10) % 360; $fileContent = sprintf("<div style='background-color: hsl(%d, %d%%, %d%%);' class='line %s' data-hash='%s' ><span class='addTime'>%s</span><span class='id'>%s</span><div class='message'>%s</div><div class='data'>%s</div></div>", $colorIndex, 66, 92, $logRow['context']['$errorLevel'], $messageHash, $listRow['addTime'], $listRow['id'], $this->cleanupMessage($listRow['message'], $logRow['context']['authCredentials.username']), $this->extractBrowserInfo($logRow), ); fwrite($fileHandle, $fileContent); $rowIndex++; } fwrite($fileHandle, <<<HTML
		</pre>
HTML
); fwrite($fileHandle, '<pre class="stats">'); fwrite($fileHandle, '<div class="HTTP_USER_AGENT">'); fwrite($fileHandle, '<h2>HTTP_USER_AGENT</h2>'); uksort($reportedGroupedData['HTTP_USER_AGENT'], function($a, $b) { return strcasecmp($b, $a); }); foreach ($reportedGroupedData['HTTP_USER_AGENT'] as $UA=>$count) { fwrite($fileHandle, sprintf("<div class='HTTP_USER_AGENT'><span class='autoselect'>%s</span> <span class='count'>(%d times)</span> <a target='_blank' href='%s'>online check</a></div>", $UA, $count, "https://udger.com/resources/online-parser?" . http_build_query([ 'action' => 'analyze', 'Fheaders' => $UA, 'ip' => '', 'test' => '2280', ]) )); } fwrite($fileHandle, '</div>'); fwrite($fileHandle, '<div class="username">'); fwrite($fileHandle, '<h2>username</h2>'); uksort($reportedGroupedData['authCredentials.username'], function($a, $b) { return strcasecmp($b, $a); }); foreach ($reportedGroupedData['authCredentials.username'] as $username=>$count) { fwrite($fileHandle, sprintf("<div class='username'>%s <span class='count'>(%d times)</span></div>", $username, $count)); } fwrite($fileHandle, '</div>'); fwrite($fileHandle, '<div class="REMOTE_ADDR">'); fwrite($fileHandle, '<h2>REMOTE_ADDR</h2>'); uksort($reportedGroupedData['REMOTE_ADDR'], function($a, $b) { return strnatcmp($b, $a); }); foreach ($reportedGroupedData['REMOTE_ADDR'] as $REMOTE_ADDR=>$count) { fwrite($fileHandle, sprintf("<div class='REMOTE_ADDR'>%s <span class='count'>(%d times)</span><a target='_blank' href='%s'>online check</a></div>", $REMOTE_ADDR, $count, "https://www.abuseipdb.com/check/".$REMOTE_ADDR )); } fwrite($fileHandle, '</div>'); fwrite($fileHandle, sprintf('<a download="loginsStats.json" href="data:application/json;base64,%s" style="font-size: 2em; margin-top: 5em; display: block;">download report data</a>', base64_encode(json_encode($JSONReport)))); fwrite($fileHandle, <<<HTML
</body>
</html>
HTML
); fclose($fileHandle); return $this; } protected function cleanupMessage($message, $username) { $message = preg_replace("/(IP:[0-9\.]+)/", "<span class='IP'>$1</span>", $message); $message.= sprintf(" (<span class='username'>%s</span>)", $username); return $message; } protected function extractBrowserInfo($logRow) { $SERVER_value = ($logRow['context']['$_SERVER']?$logRow['context']['$_SERVER']->getValue():[]); return sprintf("<div class=''>%s</div>", $SERVER_value['HTTP_USER_AGENT']); } } 