Boosting Your Security Operations: Finding MTTD and MTTR in QRadar and Creating PULSE Visualization

Calculate IBM QRadar Offense Metrices

Zain ul Abidin
11 min readApr 9, 2023

Introduction

In the world of cybersecurity, every second counts. The longer it takes for a security team to detect and respond to a threat, the more damage can be done. That’s why security teams need to understand and measure two critical metrics: Mean Time to Detect (MTTD) and Mean Time to Respond (MTTR). In this blog post, we’ll explore what MTTD and MTTR are and how they relate to QRadar.

What is MTTD?

Mean Time to Detect (MTTD) is the average amount of time it takes for a security team to detect a security incident. The shorter the MTTD, the faster an organization can respond to a threat and minimize the impact.

What is MTTR?

Mean Time to Respond (MTTR) is the average amount of time it takes for a security team to respond to a security incident once it has been detected. The shorter the MTTR, the faster an organization can mitigate the impact of a threat.

How to Calculate MTTD in QRadar:

As QRadar performs real-time collection and processing of logs and forwards them to the third layer “Console”, its MTTD will be relatively zero. Additionally, QRadar’s ability to detect and prioritize security events in real time allows for faster incident response times. This is because security teams can quickly identify and address potential threats as they occur, minimizing the impact and scope of any security incidents.

IBM QRadar Architecture

How to Calculate/Find and Visualize MTTR in QRadar:

Following are the main steps to calculate the MTTR and its PULSE visualization:

  • Ingestion of Offense metrics logs
  • Create a Log Source
  • Parsing/Mapping events and extracting the index properties
  • Create a PULSE visualization
  1. Ingestion of Offense metrics logs:
  • We can get a list of columns in the “OFFENSE_VIEW” table using the following psql query:
psql -U qradar -c "\d+ OFFENSE_VIEW"
  • We will use the necessary columns from “OFFENSE_VIEW” in the psql query for the script.

To create a shell script, we will perform the following step:

  1. Create a directory named “scripts” in the “store” directory. You can also choose to create it anywhere with any name you prefer.
mkdir /scripts

2. We will now create a shell file called “offense_logs.sh” in the “/store/scripts” directory:

touch offense_logs.sh

3. The script will retrieve data from the specified column and write it to a temporary file. To implement this, open the script and copy/paste the following code:

#to open the offense_logs.sh
vim offense_logs.sh
#!/bin/bash
#Version: 1.1
#Owner: Cristian Ruvalcaba
#Log Model: Custom Log Source - Offenses

## This line below sets up the variables used in the script: buffer file, and EPS at which to send logs into the event pipeline.
tmpFile="/store/scripts/offense_buffer" # Ensure a buffer file exists
EPS=100 # Set EPS limit for log source


## This line below queries the DB and adds it all to a temporary file.
psql -A -U qradar -c "select now(),'data_source=offense_detail.saluca.net',concat('id=',a.id), concat('start=',to_timestamp(a.start_time/1000)) as startTime, concat('offenseCreate=',to_timestamp(a.first_persisted_time/1000)) as offense_created, concat('lastevent=',to_timestamp(a.last_persisted_time/1000)) as LatestEvent, concat('source=',a.source), concat('eventcount=',a.event_count), concat('flowcount=',a.flow_count), concat('offense_name=',a.naming_contributions) as rules, concat('categories=',a.category_naming_contributions), concat('assignee=',b.username), concat('assigntime=',to_timestamp(b.last_persisted_time/1000)) as AssignTime, concat('v=',b.version) as Version, concat('timetoassign=',concat(floor((b.last_persisted_time - a.first_persisted_time)/1000/86400),'d',floor(mod((b.last_persisted_time - a.first_persisted_time)/1000,86400)/3600),':',mod(((b.last_persisted_time - a.first_persisted_time)/60000),60),':',MOD((b.last_persisted_time - a.first_persisted_time)/1000,60))) as timetoassign, concat('secondstoassign=',(b.last_persisted_time - a.first_persisted_time)/1000) as secondsToAssign, concat('timetoclose=',concat(floor((b.closed_date - a.first_persisted_time/1000)/86400),'d',floor(mod((b.closed_date - (a.first_persisted_time/1000)),86400)/3600),':',mod(((b.closed_date - (a.first_persisted_time)/1000))/60,60),':',MOD((b.closed_date - (a.first_persisted_time)/1000),60))) as timetoclose, concat('secondstoclose=',(b.closed_date - (a.first_persisted_time)/1000)) as secondsToclose from offense a join offense_properties b on (a.id = b.id) order by a.id desc" > $tmpFile

/opt/qradar/bin/logrun.pl -t -f $tmpFile $EPS # Send events to SIEM locally using TCP to prevent truncation

This script is written by “Cristian Ruvalcaba” and his blog link is attached in the “References” section

4. We will now create a cron job to automatically run this script every 15 minutes and update the “offense_buffer” temporary file with the latest offense data.

crontab -e
*/15 * * * * /bin/bash /store/scripts/offense_logs.sh

2. Create a Log Source:

  • Attached are screenshots to guide you in creating a log source type with the specified configuration.

Note: Fill in the highlighted sections in the screenshots. You may name the log source as per your choice, but the other configuration parameters are important and must be the same.

QRadar Log Source

Note: Logs will start being received in a couple of minutes, but the events are currently unmapped/unparsed, and the index properties need to be extracted.

3. Parsing/Mapping events and Extracting the index properties:

  • Once logs start being received, we will observe that the event name and low-level category are shown as “unknown”. This indicates that the events are unable to be parsed and mapped properly.
  • To map or parse these events, we will begin by copying the payload of a single event. Since all events are of the same category and have the same kind of payload, this will suffice.
  • We will use the “DSM editor” to map the event and extract its index properties.

Note: You may skip the following parsing/mapping process by directly exporting the DSM attached in “Related Material” at the end of the blog if your QRadar version is “7.5.0 UpdatePackage 3”, with the help of “Extensions” in QRadar or you may follow the following steps to learn how to parse/map the events in QRadar

  • Now, we will paste the payload in the appropriate section. If the payload causes a “Parsing Failed” error, we will observe it.
  • We will assign an event ID to this event with the help of regex and will see the “parsing status” will change to “parsing but NOT mapped”:
id=(\d+)
  • Now, we have to map the event by specifying the High/Low-level categories as well as creating a new QID against this event:

Note: As this event is only giving as the information about the offense metrices, so will map it as an “Informational”

  • As the event is properly parsed and mapped, we now have to extract the index properties from the payload so that we can use them for rules/searches.
  • Firstly, we will create new index properties and with the help of regex extract relevant data from the payload:
  • We will create the remaining index properties in a similar way to above of the following index names with respective regex expressions:

Important Note: While creating the index property named “offense_seconds_to_assign” and “offense_seconds_to_close”, the “field name” should be “Numeric” else it would cause an issue in creating PULSE widgets.

Index property name: offense_id
Description: Offense number
Parsing: id=(\d+)
Capture group: 1

Index property name: offense_assign
Description: Offense assigned timestamp
Parsing: assigntime=(\d{4}\-\d{2}\-\d{2}\s+\d+\:\d+\:\d+)
Capture group: 1

Index property name: offense_assignee
Description: Assigned analyst
Parsing: assignee=(\w+)
Capture group: 1

Index property name: offense_categories
Description: Categories of events contributing to the offense
Parsing: categories=(.*?)\|assignee
Capture group: 1

Index property name: offense_create
Description: Create timestamp for offense
Parsing: offenseCreate=(\d{4}\-\d{2}\-\d{2}\s+\d+\:\d+\:\d+)
Capture group: 1

Index property name: offense_description
Description: Naming contributions to the offense
Parsing: offense_name=(.*?)\|categories
Capture group: 1

Index property name: offense_eventcount
Total events related to the offense
Parsing: eventcount=(\d+)
Capture group: 1

Index property name: offense_flowcount
Description: Total flows related to the offense
Parsing: flowcount=(\d+)
Capture group: 1

Index property name: offense_lastevent
Description: Most recent flow or event timestamp for offense
Parsing: lastevent=(\d{4}\-\d{2}\-\d{2}\s+\d+\:\d+\:\d+)
Capture group: 1

Index property name: offense_seconds_to_assign
Description: Total seconds between offense creation and latest assigning, or re-assigning, of offense
Parsing: secondstoassign=(\d+)
Capture group: 1

Index property name: offense_seconds_to_close
Description: Total seconds between offense creation and closing of offense
Parsing: secondstoclose=(\d+)
Capture group: 1

Index property name: offense_time_to_assign
Description: Structured human readable time to assign for offense
Parsing: timetoassign=(\d+d\d+\:\d+\:\d+)
Capture group: 1

Index property name: offense_time_to_close
Description: Structured human readable time to close for offense
Parsing: timetoclose=(\d+d\d+\:\d+\:\d+)
Capture group: 1

4. Create a PULSE visualization

  1. We’ll explore how to leverage the PULSE platform to improve performance tracking with the help of various widgets we’ll create. These widgets will offer enhanced visualization capabilities, empowering us to gain valuable insights into our metrics:
  • Average time to assign an offense
  • Mean time to response
  • Open offense assignment count per analyst
  • Most recent offenses and their assigned analysts
  • Top offense descriptions
  • Total open unassigned offenses
  • Total open assigned offenses

2. To directly import the PULSE dashboard which can be downloaded from the “Related Material” section at the end of the blog, you may follow the following steps to avoid the hustle:

  • You may create widgets on your own with the help of the following AQL queries:
Widget Name: Average Time to Assign Offenses
AQL Query:
select

SUBSTRING(STR(AVG(offense_seconds_to_assign)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_assign)/86400), '.')) as 'Days',

SUBSTRING(STR((AVG(offense_seconds_to_assign)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_assign)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_assign)/86400), '.'))))*86400/3600), 0, STRPOS(STR((AVG(offense_seconds_to_assign)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_assign)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_assign)/86400),'.'))))*86400/3600), '.')) as 'Hours' ,

substring(
(double((AVG(offense_seconds_to_assign)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_assign)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_assign)/86400), '.'))))*86400/3600) - double(SUBSTRING(STR((AVG(offense_seconds_to_assign)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_assign)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_assign)/86400), '.'))))*86400/3600), 0, STRPOS(STR((AVG(offense_seconds_to_assign)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_assign)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_assign)/86400),'.'))))*86400/3600), '.'))))*60, 0, 2) as 'Minutes',

substring((((double((AVG(offense_seconds_to_assign)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_assign)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_assign)/86400), '.'))))*86400/3600) - double(SUBSTRING(STR((AVG(offense_seconds_to_assign)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_assign)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_assign)/86400), '.'))))*86400/3600), 0, STRPOS(STR((AVG(offense_seconds_to_assign)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_assign)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_assign)/86400),'.'))))*86400/3600), '.'))))*60)-double(substring(
(double((AVG(offense_seconds_to_assign)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_assign)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_assign)/86400), '.'))))*86400/3600) - double(SUBSTRING(STR((AVG(offense_seconds_to_assign)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_assign)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_assign)/86400), '.'))))*86400/3600), 0, STRPOS(STR((AVG(offense_seconds_to_assign)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_assign)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_assign)/86400),'.'))))*86400/3600), '.'))))*60, 0, 2)))*60,0,2) as 'Seconds',

concat(
SUBSTRING(STR(AVG(offense_seconds_to_assign)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_assign)/86400), '.')), ' Days ',
SUBSTRING(STR((AVG(offense_seconds_to_assign)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_assign)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_assign)/86400), '.'))))*86400/3600), 0, STRPOS(STR((AVG(offense_seconds_to_assign)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_assign)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_assign)/86400),'.'))))*86400/3600), '.')), ' Hours ',
substring(
(double((AVG(offense_seconds_to_assign)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_assign)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_assign)/86400), '.'))))*86400/3600) - double(SUBSTRING(STR((AVG(offense_seconds_to_assign)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_assign)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_assign)/86400), '.'))))*86400/3600), 0, STRPOS(STR((AVG(offense_seconds_to_assign)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_assign)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_assign)/86400),'.'))))*86400/3600), '.'))))*60, 0, 2), ' Minutes ',
substring((((double((AVG(offense_seconds_to_assign)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_assign)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_assign)/86400), '.'))))*86400/3600) - double(SUBSTRING(STR((AVG(offense_seconds_to_assign)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_assign)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_assign)/86400), '.'))))*86400/3600), 0, STRPOS(STR((AVG(offense_seconds_to_assign)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_assign)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_assign)/86400),'.'))))*86400/3600), '.'))))*60)-double(substring(
(double((AVG(offense_seconds_to_assign)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_assign)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_assign)/86400), '.'))))*86400/3600) - double(SUBSTRING(STR((AVG(offense_seconds_to_assign)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_assign)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_assign)/86400), '.'))))*86400/3600), 0, STRPOS(STR((AVG(offense_seconds_to_assign)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_assign)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_assign)/86400),'.'))))*86400/3600), '.'))))*60, 0, 2)))*60,0,2), ' Seconds '
) as "Time To Assign"


from events where logsourcename(logsourceid) ilike '%offense detail%' and offense_seconds_to_assign > '0' and offense_assignee is not null last 15 minutes
Widget Name: Mean time to response
AQL Query:
select

SUBSTRING(STR(AVG(offense_seconds_to_close)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_close)/86400), '.')) as 'Days',

SUBSTRING(STR((AVG(offense_seconds_to_close)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_close)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_close)/86400), '.'))))*86400/3600), 0, STRPOS(STR((AVG(offense_seconds_to_close)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_close)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_close)/86400),'.'))))*86400/3600), '.')) as 'Hours' ,

substring(
(double((AVG(offense_seconds_to_close)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_close)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_close)/86400), '.'))))*86400/3600) - double(SUBSTRING(STR((AVG(offense_seconds_to_close)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_close)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_close)/86400), '.'))))*86400/3600), 0, STRPOS(STR((AVG(offense_seconds_to_close)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_close)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_close)/86400),'.'))))*86400/3600), '.'))))*60, 0, 2) as 'Minutes',

substring((((double((AVG(offense_seconds_to_close)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_close)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_close)/86400), '.'))))*86400/3600) - double(SUBSTRING(STR((AVG(offense_seconds_to_close)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_close)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_close)/86400), '.'))))*86400/3600), 0, STRPOS(STR((AVG(offense_seconds_to_close)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_close)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_close)/86400),'.'))))*86400/3600), '.'))))*60)-double(substring(
(double((AVG(offense_seconds_to_close)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_close)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_close)/86400), '.'))))*86400/3600) - double(SUBSTRING(STR((AVG(offense_seconds_to_close)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_close)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_close)/86400), '.'))))*86400/3600), 0, STRPOS(STR((AVG(offense_seconds_to_close)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_close)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_close)/86400),'.'))))*86400/3600), '.'))))*60, 0, 2)))*60,0,2) as 'Seconds',

concat(
SUBSTRING(STR(AVG(offense_seconds_to_close)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_close)/86400), '.')), ' Days ',
SUBSTRING(STR((AVG(offense_seconds_to_close)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_close)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_close)/86400), '.'))))*86400/3600), 0, STRPOS(STR((AVG(offense_seconds_to_close)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_close)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_close)/86400),'.'))))*86400/3600), '.')), ' Hours ',
substring(
(double((AVG(offense_seconds_to_close)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_close)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_close)/86400), '.'))))*86400/3600) - double(SUBSTRING(STR((AVG(offense_seconds_to_close)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_close)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_close)/86400), '.'))))*86400/3600), 0, STRPOS(STR((AVG(offense_seconds_to_close)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_close)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_close)/86400),'.'))))*86400/3600), '.'))))*60, 0, 2), ' Minutes ',
substring((((double((AVG(offense_seconds_to_close)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_close)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_close)/86400), '.'))))*86400/3600) - double(SUBSTRING(STR((AVG(offense_seconds_to_close)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_close)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_close)/86400), '.'))))*86400/3600), 0, STRPOS(STR((AVG(offense_seconds_to_close)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_close)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_close)/86400),'.'))))*86400/3600), '.'))))*60)-double(substring(
(double((AVG(offense_seconds_to_close)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_close)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_close)/86400), '.'))))*86400/3600) - double(SUBSTRING(STR((AVG(offense_seconds_to_close)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_close)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_close)/86400), '.'))))*86400/3600), 0, STRPOS(STR((AVG(offense_seconds_to_close)/86400 - double(SUBSTRING(STR(AVG(offense_seconds_to_close)/86400), 0, STRPOS(STR(AVG(offense_seconds_to_close)/86400),'.'))))*86400/3600), '.'))))*60, 0, 2)))*60,0,2), ' Seconds '
) as "Time To Close"


from events where logsourcename(logsourceid) ilike '%offense detail%' and offense_seconds_to_close > '0' last 15 minutes
Widget Name: Total Unassigned Offenses
AQL Query:
select count(offense_id) as 'Total Unassigned Offenses' from events where logsourcename(logsourceid) ilike '%offense detail%' and offense_assignee IS NULL and offense_seconds_to_close is Null last 15 minutes
Widget Name: Total Assigned Offenses
AQL Query:
select count(offense_id) as 'Total Assigned Offenses' from events where logsourcename(logsourceid) ilike '%offense detail%' and offense_assignee IS NOT NULL and offense_seconds_to_close is null last 15 minutes
Widget Name: Most Recent Offenses
AQL Query:
select offense_id as "Offense ID", offense_assignee as "Assigned Analyst", offense_time_to_assign as "Time to Assign" from events where logsourcename(logsourceid) ilike '%offense detail%' ORDER BY offense_id desc limit 5 last 15 minutes
Widget Name: Offense Count by Assignee
AQL Query:
select offense_assignee as 'Assigned Analyst', offense_id, count() as 'Offense Count' from events where logsourcename(logsourceid) ilike '%offense detail%' and offense_assignee IS NOT NULL GROUP BY offense_assignee order by count() desc last 15 minutes
Widget Name: Top Offense Descriptions
AQL Query:
select offense_description as 'Offense Name', count() as 'Total Count' from events where logsourcename(logsourceid) ilike '%offense detail%' and offense_description is not null GROUP BY offense_description ORDER BY COUNT() desc last 15 MINUTES

How QRadar can help improve MTTD and MTTR

QRadar provides several features and tools that can help security teams improve both MTTD and MTTR. For example, QRadar’s real-time monitoring capabilities allow security teams to quickly detect threats as they occur, reducing MTTD. Additionally, QRadar’s automated response capabilities can help speed up the incident response process, reducing MTTR. QRadar also provides detailed analytics and reporting capabilities that can help security teams identify areas for improvement and track their progress over time.

Conclusion

MTTD and MTTR are critical metrics for any security team. By measuring and improving these metrics, organizations can better protect themselves from cyber threats and minimize the impact of security incidents. QRadar provides several features and tools that can help improve MTTD and MTTR, making it an essential tool for any cybersecurity program.

Credits:

I would like to acknowledge Cristian Ruvalcaba for his brilliant idea that served as the foundation of this blog. His valuable insights and guidance were instrumental in troubleshooting and enhancing the content. Without his support, this blog would not have been possible.

--

--

Zain ul Abidin

Sr. Security Engineer | Linkedin Profile: /in/zainulabidin7