Implementing User Activity Monitoring (UAM) in Laravel 5.x

Introduction

User activity monitoring (UAM) enables companies to monitor and track end user behavior on devices, networks, and other company-owned IT resources. Many organizations implement user activity monitoring tools to help detect and stop insider threats, whether unintentional or with malicious intent. The range of monitoring and methods utilized depends on the objectives of the company.

By implementing user activity monitoring, enterprises can more readily identify suspicious behavior and mitigate risks before they result in data breaches, or at least in time to minimize damages. Sometimes called user activity tracking, user activity monitoring is a form of surveillance, but serves as a proactive review of end user activity to determine misuse of access privileges or data protection policies either through ignorance or malicious intent.

UAM is also used to obtain meaningful statements on how users interact with a web application, the collected information needs to be more detailed and fine-grained than that provided by classical log files.

In recent years, the web has constantly been gaining importance as a platform for applications. Whereas earlier web applications were simple and used straightforward page layouts, now websites offer applications with sophisticated user interfaces. Additionally, there is a noticeable tendency to move more of the application to the client: Earlier web applications followed the HTTP protocol’s request/response paradigm, but many newer applications are JavaScript-based and only contact the server in order to load or save data.

Implementation in Laravel

We will be using Laravel’s Middleware approach. Below is the flow diagram.

Image for post
Image for post

Step 1: Setup Amazon Kinesis Data Stream

Step 2: Create a middleware in Laravel with command

php artisan make:middleware UserActivityMonitoringMiddleware

Step 3: Add following code in UserActivityMonitoringMiddleware class

<?php

namespace
App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;
use Aws\Kinesis\KinesisClient;
use Aws\Kinesis\Exception\KinesisException;

class UserActivityMonitoringMiddleware
{
/**
*
*/
const PARTITION_KEY = 'USER_ACTIVITY';

/**
* Handle an incoming request.
*
*
@param \Illuminate\Http\Request $request
*
@param \Closure $next
*
@return mixed
*/
public function handle($request, Closure $next)
{
$response = $next($request);

if (Auth::check()) {
$this->track($request, $response);
}
# You can add more methods herereturn $response;
}

/**
*
@param Request $request
*
@param Response $response
*/
protected function track(Request $request, Response $response)
{
$user = Auth::user();

$activity['request_token'] = trim(str_ireplace('Bearer', '', $request->header('Authorization')));
$activity['user_id'] = intval($user->id);
$activity['url'] = $request->getUri();
$activity['response_http_code'] = intval($response->getStatusCode());
$activity['response_time'] = $this->getResponseDispatchTimeHumanized();
$activity['response'] = $response->getContent();
$activity['timestamp'] = date('Y-m-d H:i:s', strtotime('now'));
$activity['payload'] = $this->getRequestPayload();

# The maximum size of the data payload of a record before base64-encoding is up to 1 MiB.
# https://docs.aws.amazon.com/streams/latest/dev/service-sizes-and-limits.html
##################################################################### AWS Kinesis data streaming ###################################################################
try {

$kinesisClient = new KinesisClient([
'region' => env('AWS_REGION'),
'version' => '2013-12-02',
'credentials' => [
'key' => env('AWS_ACCESS_KEY'),
'secret' => env('AWS_SECRET_KEY'),
]
]);

$result = $kinesisClient->putRecord([
'StreamName' => env('USER_ACTIVITY_STREAM'),
'Data' => json_encode($activity),
'PartitionKey' => self::PARTITION_KEY
]);

} catch (KinesisException $e) {
if (env('APP_DEBUG')) {
dd($e->getMessage());
}
}
########################################################################
########################################################################
}

/**
*
@return string
*/
protected function getResponseDispatchTimeHumanized()
{
$timeTaken = microtime(true) - LARAVEL_START;
return number_format($timeTaken, 2) . ' seconds';
}
}
} catch (KinesisException $e) {
if (env('APP_DEBUG')) {
dd($e->getMessage());
}
}
########################################################################################################################################
}
/**
* @return string
*/
protected function getResponseDispatchTimeHumanized()
{
$timeTaken = microtime(true) - LARAVEL_START;
return number_format($timeTaken, 2) . ' seconds';
}
/**
*
@return array
*/
protected function getRequestPayload()
{
$payload = [];

if (\Request::isMethod('GET')) {
$payload = \Request::query();
}

if (\Request::isMethod('POST')) {
$payload = array_merge($payload, \Request::input());
}

return $payload;
}
}

Step 4: Connecting Amazon Kinesis User Activity Data Stream to Amazon Lambda

Step 5: Configure your Amazon Lambda function

In this use case, I am using Amazon DynamoDB as storage, because it is a managed service provided by AWS.

var AWS = require('aws-sdk');
var uuid = require('uuid');// Set the region
AWS.config.update({ region: 'ap-south-1'});
var documentClient = new AWS.DynamoDB.DocumentClient({ apiVersion: '2012–08–10'});
exports.handler = (event, context, callback) => {

var result = {}
result.recordsToProcess = event.Records.length
result.recordsProcessed = 0
for (var i = 0; i < result.recordsToProcess; i++) { let record = JSON.parse(Buffer.from(event.Records[0].kinesis.data, ‘base64’).toString());

let activity = {}
activity.TableName = process.env.TableName
activity.Item = {
Id: uuid.v4(),
UserId: Number(record.user_id),
Timestamp: record.timestamp,
RequestUrl: record.url,
RequestPayload: record.payload,
ResponseTime: record.response_time,
ResponseCode: Number(record.response_http_code),
SessionToken: record.request_token,
ResponseBody: record.response
}
console.log(activity) documentClient.put(activity, function(err, data) {
if (err) {
console.log(“Error”, err);
callback(err, null);
} else {
console.log(“Success”, data);
callback(null, data);
}
});

}
};

Thats it! You will now see the logs flowing in from Kinesis data stream into your DynamoDB table

Written by

Product Lead at StegoSOC

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store