Lab 05 β SQL Security Analytics Lab
Mission Information
Section titled βMission InformationβDifficulty: Intermediate
Estimated Time: 120β150 minutes
Primary Language: SQL
Security Domain: SOC / Vulnerability Management / Identity Security / GRC / Security Analytics
Environment: Local SQLite lab
Automation Type: Defensive Security Data Analysis
Mission
Section titled βMissionβYour task is to build and investigate a synthetic enterprise security database.
You will create tables representing:
USERS
ASSETS
LOGIN EVENTS
VULNERABILITIES
INCIDENTSThen you will use SQL to answer practical cybersecurity questions.
Examples:
WHO HAS THE MOST FAILED LOGINS?
WHICH SOURCE IP TARGETEDTHE MOST ACCOUNTS?
WHICH ADMINISTRATORSDO NOT HAVE MFA?
WHICH CRITICAL ASSETSHAVE OPEN CRITICAL VULNERABILITIES?
WHICH ASSETSHAVE NO OWNER?
WHICH INCIDENTSREMAIN UNASSIGNED?The workflow will be:
SECURITY QUESTION βIDENTIFY TABLES βSELECT βFILTER βJOIN βGROUP βAGGREGATE βINTERPRET βREPORTWhy This Lab Matters
Section titled βWhy This Lab MattersβSecurity platforms store enormous amounts of structured information.
Examples include:
SIEM EVENTS
IDENTITY DATA
ASSET INVENTORIES
VULNERABILITY FINDINGS
INCIDENT RECORDS
CLOUD SECURITY FINDINGS
COMPLIANCE EVIDENCESQL allows security professionals to move from:
MILLIONS OF RECORDSto:
ONE ANSWERFor example:
Which privileged accountsdo not have MFA?or:
Which critical production assetshave unresolved critical findings?These are security questions.
SQL is simply the tool used to answer them.
Learning Objectives
Section titled βLearning ObjectivesβBy completing this lab, you should be able to:
CREATE A SECURITY DATABASE
CREATE TABLES
INSERT SYNTHETIC DATA
USE SELECT
USE WHERE
USE AND / OR
USE IN
USE LIKE
USE ORDER BY
USE LIMIT
USE DISTINCT
USE COUNT
USE GROUP BY
USE HAVING
USE INNER JOIN
USE LEFT JOIN
USE CASE
USE SUBQUERIES
USE COMMON TABLE EXPRESSIONS
CORRELATE USERS AND EVENTS
CORRELATE ASSETS AND VULNERABILITIES
BUILD SECURITY METRICS
GENERATE ANALYST FINDINGSFinal Architecture
Section titled βFinal Architectureβ SECURITY DATABASE β βββββββββββββββΌββββββββββββββ β β β USERS ASSETS LOGIN EVENTS β β β βββββββββββββββΌββββββββββββββ β VULNERABILITIES β INCIDENTS β SQL β FILTER / JOIN / GROUP β SECURITY INSIGHT β ANALYST REPORTAuthorization and Safety
Section titled βAuthorization and SafetyβThis lab uses:
SYNTHETIC SECURITY DATADo not run modification queries against production security databases unless you are specifically authorized.
For learning and investigation, prefer:
READ-ONLY QUERIES01 β Choose the Database Engine
Section titled β01 β Choose the Database EngineβFor this lab, use:
SQLiteWhy?
FREE
LIGHTWEIGHT
LOCAL
NO SERVER REQUIRED
PORTABLE
EASY TO RESET02 β Verify SQLite
Section titled β02 β Verify SQLiteβRun:
sqlite3 --versionIf available, you are ready.
Alternatively, you can use:
DB Browser for SQLite
VS Code SQLite extension
Python sqlite3 module03 β Create the Lab Workspace
Section titled β03 β Create the Lab WorkspaceβCreate:
sql-security-analytics/|+-- database/|+-- queries/|+-- reports/|+-- README.mdLinux/macOS:
mkdir -p sql-security-analytics/{database,queries,reports}cd sql-security-analyticsPowerShell:
mkdir sql-security-analytics
cd sql-security-analytics
mkdir databasemkdir queriesmkdir reports04 β Create the Database
Section titled β04 β Create the DatabaseβRun:
sqlite3 database/security-lab.dbYou should enter the SQLite prompt:
sqlite>05 β Enable Headers
Section titled β05 β Enable HeadersβInside SQLite:
.headers on06 β Improve Display
Section titled β06 β Improve DisplayβRun:
.mode column07 β Create the Users Table
Section titled β07 β Create the Users TableβRun:
CREATE TABLE users ( user_id INTEGER PRIMARY KEY, username TEXT NOT NULL UNIQUE, department TEXT, role TEXT, privileged INTEGER NOT NULL DEFAULT 0, mfa_enabled INTEGER NOT NULL DEFAULT 0, account_status TEXT NOT NULL);08 β Understand the Users Schema
Section titled β08 β Understand the Users SchemaβFields:
user_idUnique user identifier
usernameAccount name
departmentBusiness department
roleJob role
privileged0 = standard1 = privileged
mfa_enabled0 = no1 = yes
account_statusactive / disabled / locked09 β Insert Synthetic Users
Section titled β09 β Insert Synthetic UsersβRun:
INSERT INTO users( user_id, username, department, role, privileged, mfa_enabled, account_status)VALUES(1, 'admin01', 'IT', 'Systems Administrator', 1, 1, 'active'),(2, 'admin02', 'IT', 'Cloud Administrator', 1, 0, 'active'),(3, 'analyst01', 'Security', 'SOC Analyst', 1, 1, 'active'),(4, 'user01', 'Finance', 'Accountant', 0, 1, 'active'),(5, 'user02', 'HR', 'HR Specialist', 0, 0, 'active'),(6, 'user03', 'Sales', 'Sales Executive', 0, 1, 'active'),(7, 'service_backup', 'IT', 'Service Account', 1, 0, 'active'),(8, 'former_user', 'Sales', 'Former Employee', 0, 0, 'disabled');10 β Verify Users
Section titled β10 β Verify UsersβRun:
SELECT *FROM users;11 β Create the Assets Table
Section titled β11 β Create the Assets TableβRun:
CREATE TABLE assets ( asset_id INTEGER PRIMARY KEY, hostname TEXT NOT NULL UNIQUE, ip_address TEXT NOT NULL, operating_system TEXT, environment TEXT, criticality TEXT, owner TEXT, internet_exposed INTEGER NOT NULL DEFAULT 0);12 β Insert Synthetic Assets
Section titled β12 β Insert Synthetic AssetsβRun:
INSERT INTO assets( asset_id, hostname, ip_address, operating_system, environment, criticality, owner, internet_exposed)VALUES(1, 'DC01', '10.10.10.10', 'Windows Server', 'Production', 'Critical', 'Infrastructure Team', 0),(2, 'WEB01', '10.10.20.10', 'Ubuntu Linux', 'Production', 'Critical', 'Web Team', 1),(3, 'APP01', '10.10.20.20', 'Ubuntu Linux', 'Production', 'High', 'Application Team', 0),(4, 'DB01', '10.10.30.10', 'Windows Server', 'Production', 'Critical', 'Database Team', 0),(5, 'DEV01', '10.10.40.10', 'Ubuntu Linux', 'Development', 'Medium', 'Development Team', 0),(6, 'OLD01', '10.10.50.10', 'Windows Server', 'Production', 'High', NULL, 0);13 β Verify Assets
Section titled β13 β Verify AssetsβRun:
SELECT *FROM assets;14 β Create the Login Events Table
Section titled β14 β Create the Login Events TableβRun:
CREATE TABLE login_events ( event_id INTEGER PRIMARY KEY, event_time TEXT NOT NULL, username TEXT NOT NULL, source_ip TEXT NOT NULL, target_asset_id INTEGER, status TEXT NOT NULL, authentication_type TEXT, FOREIGN KEY (target_asset_id) REFERENCES assets(asset_id));15 β Insert Authentication Events
Section titled β15 β Insert Authentication EventsβRun:
INSERT INTO login_events( event_id, event_time, username, source_ip, target_asset_id, status, authentication_type)VALUES(1, '2026-08-29T09:00:01Z', 'admin01', '10.10.99.10', 1, 'failed', 'password'),(2, '2026-08-29T09:00:15Z', 'admin01', '10.10.99.10', 1, 'failed', 'password'),(3, '2026-08-29T09:00:31Z', 'admin01', '10.10.99.10', 1, 'failed', 'password'),(4, '2026-08-29T09:01:05Z', 'admin01', '10.10.99.10', 1, 'success', 'password'),(5, '2026-08-29T09:02:10Z', 'user01', '10.10.88.20', 2, 'failed', 'password'),(6, '2026-08-29T09:02:15Z', 'user02', '10.10.88.20', 2, 'failed', 'password'),(7, '2026-08-29T09:02:20Z', 'user03', '10.10.88.20', 2, 'failed', 'password'),(8, '2026-08-29T09:02:25Z', 'admin02', '10.10.88.20', 2, 'failed', 'password'),(9, '2026-08-29T09:03:10Z', 'user01', '10.10.60.30', 3, 'failed', 'password'),(10, '2026-08-29T09:03:20Z', 'user01', '10.10.60.30', 3, 'failed', 'password'),(11, '2026-08-29T09:03:30Z', 'user01', '10.10.60.30', 3, 'failed', 'password'),(12, '2026-08-29T09:04:00Z', 'analyst01', '10.10.70.20', 1, 'success', 'mfa'),(13, '2026-08-29T09:05:00Z', 'admin02', '10.10.70.21', 4, 'success', 'password'),(14, '2026-08-29T09:06:00Z', 'service_backup', '10.10.30.50', 4, 'success', 'service'),(15, '2026-08-29T09:07:00Z', 'former_user', '10.10.90.50', 2, 'failed', 'password');16 β Create the Vulnerabilities Table
Section titled β16 β Create the Vulnerabilities TableβRun:
CREATE TABLE vulnerabilities ( vulnerability_id INTEGER PRIMARY KEY, asset_id INTEGER NOT NULL, finding_name TEXT NOT NULL, severity TEXT NOT NULL, cvss REAL, status TEXT NOT NULL, discovered_date TEXT, remediation_due_date TEXT, FOREIGN KEY (asset_id) REFERENCES assets(asset_id));17 β Insert Vulnerability Data
Section titled β17 β Insert Vulnerability DataβRun:
INSERT INTO vulnerabilities( vulnerability_id, asset_id, finding_name, severity, cvss, status, discovered_date, remediation_due_date)VALUES(1, 2, 'Outdated Web Framework', 'Critical', 9.8, 'Open', '2026-08-01', '2026-08-15'),(2, 2, 'Weak TLS Configuration', 'High', 8.1, 'Open', '2026-08-02', '2026-08-20'),(3, 1, 'Missing Security Update', 'Critical', 9.5, 'Open', '2026-08-03', '2026-08-17'),(4, 3, 'Outdated Application Library', 'High', 7.8, 'Open', '2026-08-05', '2026-08-25'),(5, 4, 'Database Configuration Review', 'Medium', 5.3, 'Open', '2026-08-06', '2026-09-05'),(6, 5, 'Development Package Finding', 'Medium', 5.0, 'Closed', '2026-08-07', '2026-09-07'),(7, 6, 'Legacy Service Exposure', 'Critical', 9.0, 'Open', '2026-07-15', '2026-07-30');18 β Create the Incidents Table
Section titled β18 β Create the Incidents TableβRun:
CREATE TABLE incidents ( incident_id INTEGER PRIMARY KEY, title TEXT NOT NULL, severity TEXT NOT NULL, status TEXT NOT NULL, assigned_to TEXT, related_asset_id INTEGER, opened_time TEXT NOT NULL, FOREIGN KEY (related_asset_id) REFERENCES assets(asset_id));19 β Insert Incident Data
Section titled β19 β Insert Incident DataβRun:
INSERT INTO incidents( incident_id, title, severity, status, assigned_to, related_asset_id, opened_time)VALUES(1, 'Repeated Administrative Login Failures', 'High', 'Open', 'analyst01', 1, '2026-08-29T09:01:00Z'),(2, 'Web Server Security Alert', 'Critical', 'Open', NULL, 2, '2026-08-29T09:10:00Z'),(3, 'Database Authentication Review', 'Medium', 'Investigating', 'analyst01', 4, '2026-08-29T09:15:00Z'),(4, 'Legacy Server Risk Review', 'High', 'Open', NULL, 6, '2026-08-29T09:20:00Z');20 β Verify Tables
Section titled β20 β Verify TablesβRun:
.tablesExpected:
assets
incidents
login_events
users
vulnerabilities21 β Your First Security Query
Section titled β21 β Your First Security QueryβAsk:
Which users are privileged?Run:
SELECT username, department, roleFROM usersWHERE privileged = 1;22 β Find Privileged Users Without MFA
Section titled β22 β Find Privileged Users Without MFAβSecurity question:
Which privileged accountsdo not have MFA?Run:
SELECT username, department, role, account_statusFROM usersWHERE privileged = 1AND mfa_enabled = 0;Expected candidates include:
admin02
service_backup23 β Important Interpretation
Section titled β23 β Important InterpretationβDo not automatically conclude:
SECURITY INCIDENTInstead ask:
SHOULD THIS ACCOUNT SUPPORT MFA?
IS IT INTERACTIVE?
IS IT A SERVICE ACCOUNT?
WHAT OTHER CONTROLS EXIST?
IS THERE A DOCUMENTED EXCEPTION?24 β Active Privileged Users Without MFA
Section titled β24 β Active Privileged Users Without MFAβImprove the query:
SELECT username, roleFROM usersWHERE privileged = 1AND mfa_enabled = 0AND account_status = 'active';25 β Count Privileged Accounts
Section titled β25 β Count Privileged AccountsβRun:
SELECT COUNT(*) AS privileged_accountsFROM usersWHERE privileged = 1;26 β Calculate MFA Coverage
Section titled β26 β Calculate MFA CoverageβRun:
SELECT COUNT(*) AS active_users, SUM( CASE WHEN mfa_enabled = 1 THEN 1 ELSE 0 END ) AS mfa_enabled_usersFROM usersWHERE account_status = 'active';27 β Calculate MFA Percentage
Section titled β27 β Calculate MFA PercentageβRun:
SELECT COUNT(*) AS active_users,
SUM( CASE WHEN mfa_enabled = 1 THEN 1 ELSE 0 END ) AS mfa_enabled_users,
ROUND( 100.0 * SUM( CASE WHEN mfa_enabled = 1 THEN 1 ELSE 0 END ) / COUNT(*), 2 ) AS mfa_percentage
FROM usersWHERE account_status = 'active';28 β Identify Disabled Accounts
Section titled β28 β Identify Disabled AccountsβRun:
SELECT username, department, roleFROM usersWHERE account_status = 'disabled';29 β Why Disabled Accounts Matter
Section titled β29 β Why Disabled Accounts MatterβA disabled account is not necessarily a problem.
But review:
DOES IT STILL HAVE APPLICATION ACCESS?
DOES IT OWN RESOURCES?
IS IT STILL IN PRIVILEGED GROUPS?
SHOULD IT BE REMOVED?30 β Analyze Failed Logins
Section titled β30 β Analyze Failed LoginsβRun:
SELECT username, COUNT(*) AS failed_loginsFROM login_eventsWHERE status = 'failed'GROUP BY usernameORDER BY failed_logins DESC;31 β Find Repeated Failures
Section titled β31 β Find Repeated FailuresβUse:
SELECT username, COUNT(*) AS failed_loginsFROM login_eventsWHERE status = 'failed'GROUP BY usernameHAVING COUNT(*) >= 3ORDER BY failed_logins DESC;32 β Why Use HAVING?
Section titled β32 β Why Use HAVING?βWHERE filters:
ROWSbefore aggregation.
HAVING filters:
GROUPSafter aggregation.
Mental model:
WHERE βGROUP βHAVING33 β Analyze Failures by Source IP
Section titled β33 β Analyze Failures by Source IPβRun:
SELECT source_ip, COUNT(*) AS failed_loginsFROM login_eventsWHERE status = 'failed'GROUP BY source_ipORDER BY failed_logins DESC;34 β Find High-Volume Sources
Section titled β34 β Find High-Volume SourcesβRun:
SELECT source_ip, COUNT(*) AS failed_loginsFROM login_eventsWHERE status = 'failed'GROUP BY source_ipHAVING COUNT(*) >= 3ORDER BY failed_logins DESC;35 β Count Unique Users per Source
Section titled β35 β Count Unique Users per SourceβRun:
SELECT source_ip, COUNT( DISTINCT username ) AS unique_usersFROM login_eventsWHERE status = 'failed'GROUP BY source_ipORDER BY unique_users DESC;36 β Identify One Source Targeting Many Accounts
Section titled β36 β Identify One Source Targeting Many AccountsβRun:
SELECT source_ip, COUNT(*) AS failed_logins, COUNT( DISTINCT username ) AS unique_usersFROM login_eventsWHERE status = 'failed'GROUP BY source_ipHAVING COUNT( DISTINCT username) >= 3ORDER BY unique_users DESC;37 β Security Interpretation
Section titled β37 β Security InterpretationβOne source targeting multiple accounts may deserve review.
However, legitimate explanations can include:
VPN GATEWAY
NAT DEVICE
PROXY
SHARED APPLICATION
AUTOMATIONDetection:
PATTERNis not the same as:
CONCLUSION38 β Join Login Events with Assets
Section titled β38 β Join Login Events with AssetsβRun:
SELECT l.event_time, l.username, l.source_ip, l.status, a.hostname, a.criticalityFROM login_events AS lINNER JOIN assets AS a ON l.target_asset_id = a.asset_idORDER BY l.event_time;39 β Why Joins Matter
Section titled β39 β Why Joins MatterβBefore:
target_asset_id = 1After:
DC01CriticalSecurity context becomes much stronger.
40 β Failed Logins Against Critical Assets
Section titled β40 β Failed Logins Against Critical AssetsβRun:
SELECT l.username, l.source_ip, a.hostname, a.criticality, COUNT(*) AS failed_loginsFROM login_events AS lINNER JOIN assets AS a ON l.target_asset_id = a.asset_idWHERE l.status = 'failed'AND a.criticality = 'Critical'GROUP BY l.username, l.source_ip, a.hostname, a.criticalityORDER BY failed_logins DESC;41 β Join Users with Login Events
Section titled β41 β Join Users with Login EventsβRun:
SELECT l.event_time, l.username, u.role, u.privileged, u.mfa_enabled, l.source_ip, l.statusFROM login_events AS lLEFT JOIN users AS u ON l.username = u.usernameORDER BY l.event_time;42 β Why LEFT JOIN?
Section titled β42 β Why LEFT JOIN?βLEFT JOIN retains every login event even if:
USERNAME IS NOTIN THE USER INVENTORYThat can be security-relevant.
43 β Find Unknown Users in Authentication Logs
Section titled β43 β Find Unknown Users in Authentication LogsβRun:
SELECT DISTINCT l.usernameFROM login_events AS lLEFT JOIN users AS u ON l.username = u.usernameWHERE u.user_id IS NULL;In a larger dataset this might identify:
UNMANAGED ACCOUNTS
DATA QUALITY ISSUES
OLD IDENTITIES
UNEXPECTED IDENTITIES44 β Find Disabled Accounts Attempting Login
Section titled β44 β Find Disabled Accounts Attempting LoginβRun:
SELECT l.event_time, l.username, l.source_ip, l.statusFROM login_events AS lINNER JOIN users AS u ON l.username = u.usernameWHERE u.account_status = 'disabled';45 β Why This Deserves Review
Section titled β45 β Why This Deserves ReviewβPossible explanations include:
STALE APPLICATION CREDENTIAL
OLD DEVICE
ACCOUNT CLEANUP ISSUE
EXPECTED DENIED LOGIN
UNAUTHORIZED ATTEMPT46 β Successful Privileged Logins Without MFA
Section titled β46 β Successful Privileged Logins Without MFAβRun:
SELECT l.event_time, l.username, u.role, l.source_ip, l.authentication_type, a.hostnameFROM login_events AS lINNER JOIN users AS u ON l.username = u.usernameLEFT JOIN assets AS a ON l.target_asset_id = a.asset_idWHERE l.status = 'success'AND u.privileged = 1AND u.mfa_enabled = 0;47 β Interpret Carefully
Section titled β47 β Interpret CarefullyβFor:
service_backupMFA may not be applicable if it is:
NON-INTERACTIVEThis is why security analytics needs:
BUSINESS CONTEXT48 β Successful Admin Logins
Section titled β48 β Successful Admin LoginsβRun:
SELECT l.event_time, l.username, l.source_ip, a.hostname, l.authentication_typeFROM login_events AS lINNER JOIN users AS u ON l.username = u.usernameLEFT JOIN assets AS a ON l.target_asset_id = a.asset_idWHERE l.status = 'success'AND u.privileged = 1ORDER BY l.event_time;49 β Failure Followed by Success Concept
Section titled β49 β Failure Followed by Success ConceptβA useful investigation pattern:
MULTIPLE FAILURES βSUCCESSSQL can identify candidates.
50 β Find Users with Both Failure and Success
Section titled β50 β Find Users with Both Failure and SuccessβRun:
SELECT username, SUM( CASE WHEN status = 'failed' THEN 1 ELSE 0 END ) AS failures,
SUM( CASE WHEN status = 'success' THEN 1 ELSE 0 END ) AS successes
FROM login_eventsGROUP BY usernameHAVING failures >= 3AND successes >= 1;51 β Important Limitation
Section titled β51 β Important LimitationβThis query does not prove:
FAILURES OCCURREDBEFORETHE SUCCESSIt only proves both statuses exist.
Timeline matters.
52 β Timeline Query for a User
Section titled β52 β Timeline Query for a UserβRun:
SELECT event_time, username, source_ip, status, authentication_typeFROM login_eventsWHERE username = 'admin01'ORDER BY event_time;Now manually review sequence.
53 β Security Data Principle
Section titled β53 β Security Data PrincipleβAggregation gives:
SUMMARYTimeline gives:
SEQUENCEBoth matter.
54 β Vulnerability Inventory
Section titled β54 β Vulnerability InventoryβRun:
SELECT vulnerability_id, finding_name, severity, cvss, statusFROM vulnerabilitiesORDER BY cvss DESC;55 β Open Vulnerabilities
Section titled β55 β Open VulnerabilitiesβRun:
SELECT finding_name, severity, cvss, statusFROM vulnerabilitiesWHERE status = 'Open'ORDER BY cvss DESC;56 β Critical Open Findings
Section titled β56 β Critical Open FindingsβRun:
SELECT vulnerability_id, finding_name, cvssFROM vulnerabilitiesWHERE severity = 'Critical'AND status = 'Open'ORDER BY cvss DESC;57 β Join Vulnerabilities to Assets
Section titled β57 β Join Vulnerabilities to AssetsβRun:
SELECT a.hostname, a.environment, a.criticality, v.finding_name, v.severity, v.cvss, v.statusFROM vulnerabilities AS vINNER JOIN assets AS a ON v.asset_id = a.asset_idORDER BY v.cvss DESC;58 β Critical Findings on Critical Assets
Section titled β58 β Critical Findings on Critical AssetsβRun:
SELECT a.hostname, a.criticality, a.environment, a.internet_exposed, v.finding_name, v.cvss, v.remediation_due_dateFROM vulnerabilities AS vINNER JOIN assets AS a ON v.asset_id = a.asset_idWHERE v.status = 'Open'AND v.severity = 'Critical'AND a.criticality = 'Critical'ORDER BY v.cvss DESC;59 β Why Asset Context Changes Priority
Section titled β59 β Why Asset Context Changes PriorityβCompare:
Critical vulnerabilityon isolated development VMwith:
Critical vulnerabilityon internet-facing production serverTechnical severity may be the same.
Risk context is not.
60 β Internet-Exposed Assets with Open Findings
Section titled β60 β Internet-Exposed Assets with Open FindingsβRun:
SELECT a.hostname, a.ip_address, a.criticality, v.finding_name, v.severity, v.cvssFROM assets AS aINNER JOIN vulnerabilities AS v ON a.asset_id = v.asset_idWHERE a.internet_exposed = 1AND v.status = 'Open'ORDER BY v.cvss DESC;61 β Count Findings per Asset
Section titled β61 β Count Findings per AssetβRun:
SELECT a.hostname, COUNT( v.vulnerability_id ) AS finding_countFROM assets AS aLEFT JOIN vulnerabilities AS v ON a.asset_id = v.asset_idGROUP BY a.hostnameORDER BY finding_count DESC;62 β Why LEFT JOIN Here?
Section titled β62 β Why LEFT JOIN Here?βIt includes assets with:
ZERO FINDINGSThose assets are still part of inventory.
63 β Open Finding Count per Asset
Section titled β63 β Open Finding Count per AssetβRun:
SELECT a.hostname, COUNT( v.vulnerability_id ) AS open_findingsFROM assets AS aLEFT JOIN vulnerabilities AS v ON a.asset_id = v.asset_id AND v.status = 'Open'GROUP BY a.hostnameORDER BY open_findings DESC;64 β Identify Assets Without Owners
Section titled β64 β Identify Assets Without OwnersβRun:
SELECT hostname, ip_address, environment, criticalityFROM assetsWHERE owner IS NULLOR TRIM(owner) = '';Expected:
OLD0165 β Why Asset Ownership Matters
Section titled β65 β Why Asset Ownership MattersβWithout an owner:
WHO PATCHES IT?
WHO APPROVES CHANGES?
WHO RESPONDS TO ALERTS?
WHO ACCEPTS RISK?Asset ownership is a security control.
66 β Assets Without Owners and Open Findings
Section titled β66 β Assets Without Owners and Open FindingsβRun:
SELECT a.hostname, a.criticality, v.finding_name, v.severity, v.statusFROM assets AS aINNER JOIN vulnerabilities AS v ON a.asset_id = v.asset_idWHERE ( a.owner IS NULL OR TRIM(a.owner) = '')AND v.status = 'Open';67 β Overdue Findings
Section titled β67 β Overdue FindingsβBecause the lab uses:
2026-08-29as the reference date, query:
SELECT a.hostname, v.finding_name, v.severity, v.remediation_due_dateFROM vulnerabilities AS vINNER JOIN assets AS a ON v.asset_id = a.asset_idWHERE v.status = 'Open'AND v.remediation_due_date < '2026-08-29'ORDER BY v.remediation_due_date;68 β Production Date Logic
Section titled β68 β Production Date LogicβIn a live SQLite database, you might use:
date('now')Example:
WHERE remediation_due_date < date('now')Use the labβs fixed date when you want reproducible results.
69 β Overdue Critical Findings
Section titled β69 β Overdue Critical FindingsβRun:
SELECT a.hostname, a.owner, v.finding_name, v.cvss, v.remediation_due_dateFROM vulnerabilities AS vINNER JOIN assets AS a ON v.asset_id = a.asset_idWHERE v.status = 'Open'AND v.severity = 'Critical'AND v.remediation_due_date < '2026-08-29'ORDER BY v.cvss DESC;70 β Vulnerability Prioritization Query
Section titled β70 β Vulnerability Prioritization QueryβRun:
SELECT a.hostname, a.criticality, a.internet_exposed, v.finding_name, v.severity, v.cvss,
CASE WHEN v.severity = 'Critical' AND a.criticality = 'Critical' AND a.internet_exposed = 1 THEN 'Priority 1'
WHEN v.severity = 'Critical' AND a.criticality IN ( 'Critical', 'High' ) THEN 'Priority 2'
WHEN v.severity = 'High' THEN 'Priority 3'
ELSE 'Standard Review' END AS remediation_priority
FROM vulnerabilities AS vINNER JOIN assets AS a ON v.asset_id = a.asset_idWHERE v.status = 'Open'ORDER BY remediation_priority, v.cvss DESC;71 β Risk Scoring Warning
Section titled β71 β Risk Scoring WarningβThis is a:
TRAINING MODELA production prioritization process may include:
EXPLOITABILITY
THREAT INTELLIGENCE
BUSINESS IMPACT
DATA SENSITIVITY
EXPOSURE
COMPENSATING CONTROLS
REMEDIATION AGE72 β Incident Inventory
Section titled β72 β Incident InventoryβRun:
SELECT *FROM incidentsORDER BY opened_time;73 β Open Incidents
Section titled β73 β Open IncidentsβRun:
SELECT incident_id, title, severity, assigned_toFROM incidentsWHERE status = 'Open';74 β Find Unassigned Incidents
Section titled β74 β Find Unassigned IncidentsβRun:
SELECT incident_id, title, severity, opened_timeFROM incidentsWHERE assigned_to IS NULL;75 β Why IS NULL?
Section titled β75 β Why IS NULL?βDo not use:
assigned_to = NULLUse:
assigned_to IS NULLbecause NULL represents:
UNKNOWN / ABSENTrather than a normal value.
76 β Critical Unassigned Incidents
Section titled β76 β Critical Unassigned IncidentsβRun:
SELECT incident_id, title, severity, opened_timeFROM incidentsWHERE assigned_to IS NULLAND severity = 'Critical';77 β Join Incidents to Assets
Section titled β77 β Join Incidents to AssetsβRun:
SELECT i.incident_id, i.title, i.severity, i.status, i.assigned_to, a.hostname, a.criticalityFROM incidents AS iLEFT JOIN assets AS a ON i.related_asset_id = a.asset_idORDER BY i.opened_time;78 β Unassigned Incidents on Critical Assets
Section titled β78 β Unassigned Incidents on Critical AssetsβRun:
SELECT i.incident_id, i.title, i.severity, a.hostname, a.criticalityFROM incidents AS iINNER JOIN assets AS a ON i.related_asset_id = a.asset_idWHERE i.assigned_to IS NULLAND a.criticality = 'Critical';79 β Security Analytics Correlation
Section titled β79 β Security Analytics CorrelationβNow start combining:
ASSET CRITICALITY
VULNERABILITIES
INCIDENTS
AUTHENTICATIONThis is where SQL becomes very powerful.
80 β Assets with Both Incidents and Open Vulnerabilities
Section titled β80 β Assets with Both Incidents and Open VulnerabilitiesβRun:
SELECT DISTINCT a.hostname, a.criticalityFROM assets AS aINNER JOIN vulnerabilities AS v ON a.asset_id = v.asset_idINNER JOIN incidents AS i ON a.asset_id = i.related_asset_idWHERE v.status = 'Open'AND i.status IN ( 'Open', 'Investigating');81 β Count Open Findings and Incidents per Asset
Section titled β81 β Count Open Findings and Incidents per AssetβRun:
WITH finding_counts AS ( SELECT asset_id, COUNT(*) AS open_findings FROM vulnerabilities WHERE status = 'Open' GROUP BY asset_id),
incident_counts AS ( SELECT related_asset_id AS asset_id, COUNT(*) AS active_incidents FROM incidents WHERE status IN ( 'Open', 'Investigating' ) GROUP BY related_asset_id)
SELECT a.hostname, a.criticality, COALESCE( f.open_findings, 0 ) AS open_findings, COALESCE( i.active_incidents, 0 ) AS active_incidents
FROM assets AS a
LEFT JOIN finding_counts AS f ON a.asset_id = f.asset_id
LEFT JOIN incident_counts AS i ON a.asset_id = i.asset_id
ORDER BY active_incidents DESC, open_findings DESC;82 β What Is a CTE?
Section titled β82 β What Is a CTE?βA:
COMMON TABLE EXPRESSIONuses:
WITHto create temporary named query results.
Mental model:
COMPLEX QUESTION βBREAK INTO SMALLER QUESTIONS βCOMBINE RESULTS83 β Why Use COALESCE?
Section titled β83 β Why Use COALESCE?βIf an asset has no incident:
NULLcan become:
0using:
COALESCE(value, 0)This makes reports easier to interpret.
84 β Security Dashboard Query
Section titled β84 β Security Dashboard QueryβBuild a high-level security summary:
SELECT (SELECT COUNT(*) FROM users) AS total_users,
( SELECT COUNT(*) FROM users WHERE privileged = 1 ) AS privileged_users,
( SELECT COUNT(*) FROM users WHERE mfa_enabled = 0 AND account_status = 'active' ) AS active_users_without_mfa,
( SELECT COUNT(*) FROM login_events WHERE status = 'failed' ) AS failed_logins,
( SELECT COUNT(*) FROM vulnerabilities WHERE status = 'Open' ) AS open_vulnerabilities,
( SELECT COUNT(*) FROM vulnerabilities WHERE status = 'Open' AND severity = 'Critical' ) AS open_critical_vulnerabilities,
( SELECT COUNT(*) FROM incidents WHERE status IN ( 'Open', 'Investigating' ) ) AS active_incidents;85 β Why Metrics Need Context
Section titled β85 β Why Metrics Need ContextβSuppose:
Open vulnerabilities = 6That alone does not tell you:
SEVERITY
ASSET CRITICALITY
AGE
EXPOSURE
OWNERSHIPMetrics are starting points.
Not conclusions.
86 β Authentication Dashboard Query
Section titled β86 β Authentication Dashboard QueryβRun:
SELECT status, COUNT(*) AS event_countFROM login_eventsGROUP BY status;87 β Authentication by Type
Section titled β87 β Authentication by TypeβRun:
SELECT authentication_type, status, COUNT(*) AS event_countFROM login_eventsGROUP BY authentication_type, statusORDER BY authentication_type, status;88 β Failed Logins by Department
Section titled β88 β Failed Logins by DepartmentβRun:
SELECT u.department, COUNT(*) AS failed_loginsFROM login_events AS lINNER JOIN users AS u ON l.username = u.usernameWHERE l.status = 'failed'GROUP BY u.departmentORDER BY failed_logins DESC;89 β Failed Logins by Privilege
Section titled β89 β Failed Logins by PrivilegeβRun:
SELECT CASE WHEN u.privileged = 1 THEN 'Privileged' ELSE 'Standard' END AS account_type,
COUNT(*) AS failed_logins
FROM login_events AS l
INNER JOIN users AS u ON l.username = u.username
WHERE l.status = 'failed'
GROUP BY account_type;90 β Security Question: Privileged Accounts with Failures
Section titled β90 β Security Question: Privileged Accounts with FailuresβRun:
SELECT l.username, COUNT(*) AS failed_loginsFROM login_events AS lINNER JOIN users AS u ON l.username = u.usernameWHERE l.status = 'failed'AND u.privileged = 1GROUP BY l.usernameORDER BY failed_logins DESC;91 β Security Question: Critical Assets with Authentication Failures
Section titled β91 β Security Question: Critical Assets with Authentication FailuresβRun:
SELECT a.hostname, COUNT(*) AS failed_loginsFROM login_events AS lINNER JOIN assets AS a ON l.target_asset_id = a.asset_idWHERE l.status = 'failed'AND a.criticality = 'Critical'GROUP BY a.hostnameORDER BY failed_logins DESC;92 β Security Question: Internet-Exposed Critical Assets
Section titled β92 β Security Question: Internet-Exposed Critical AssetsβRun:
SELECT hostname, ip_address, operating_system, ownerFROM assetsWHERE internet_exposed = 1AND criticality = 'Critical';93 β Security Question: Internet-Exposed Assets with Critical Findings
Section titled β93 β Security Question: Internet-Exposed Assets with Critical FindingsβRun:
SELECT a.hostname, a.ip_address, a.owner, v.finding_name, v.cvssFROM assets AS aINNER JOIN vulnerabilities AS v ON a.asset_id = v.asset_idWHERE a.internet_exposed = 1AND v.status = 'Open'AND v.severity = 'Critical';94 β Security Question: High-Risk Asset Without Owner
Section titled β94 β Security Question: High-Risk Asset Without OwnerβRun:
SELECT hostname, criticality, environment, ip_addressFROM assetsWHERE criticality IN ( 'Critical', 'High')AND ( owner IS NULL OR TRIM(owner) = '');95 β Subquery Example
Section titled β95 β Subquery ExampleβFind users whose usernames appear in failed login events:
SELECT username, department, roleFROM usersWHERE username IN ( SELECT DISTINCT username FROM login_events WHERE status = 'failed');96 β NOT IN Caution
Section titled β96 β NOT IN CautionβNULL values can make:
NOT INbehave unexpectedly.
For many anti-match operations, consider:
NOT EXISTS97 β NOT EXISTS Example
Section titled β97 β NOT EXISTS ExampleβFind users with no authentication events:
SELECT u.username, u.department, u.account_statusFROM users AS uWHERE NOT EXISTS ( SELECT 1 FROM login_events AS l WHERE l.username = u.username);98 β Why This Is Useful
Section titled β98 β Why This Is UsefulβAn account with no observed usage may deserve review if it is:
PRIVILEGED
ENABLED
OLD
UNOWNEDBut your login table may cover only a short time period.
Do not overinterpret limited data.
99 β Asset Data Quality Check
Section titled β99 β Asset Data Quality CheckβFind duplicated IP addresses:
SELECT ip_address, COUNT(*) AS asset_countFROM assetsGROUP BY ip_addressHAVING COUNT(*) > 1;100 β User Data Quality Check
Section titled β100 β User Data Quality CheckβFind duplicate usernames:
SELECT username, COUNT(*) AS user_countFROM usersGROUP BY usernameHAVING COUNT(*) > 1;The schemaβs:
UNIQUEconstraint should already prevent this.
101 β NULL Ownership Metric
Section titled β101 β NULL Ownership MetricβRun:
SELECT COUNT(*) AS unowned_assetsFROM assetsWHERE owner IS NULLOR TRIM(owner) = '';102 β Findings by Severity
Section titled β102 β Findings by SeverityβRun:
SELECT severity, COUNT(*) AS finding_countFROM vulnerabilitiesWHERE status = 'Open'GROUP BY severityORDER BY finding_count DESC;103 β Use CASE for Severity Ordering
Section titled β103 β Use CASE for Severity OrderingβAlphabetical ordering is not security ordering.
Use:
SELECT severity, COUNT(*) AS finding_countFROM vulnerabilitiesWHERE status = 'Open'GROUP BY severityORDER BY CASE severity WHEN 'Critical' THEN 1 WHEN 'High' THEN 2 WHEN 'Medium' THEN 3 WHEN 'Low' THEN 4 ELSE 5 END;104 β Incident Severity Summary
Section titled β104 β Incident Severity SummaryβRun:
SELECT severity, COUNT(*) AS incident_countFROM incidentsWHERE status IN ( 'Open', 'Investigating')GROUP BY severity;105 β Incident Assignment Summary
Section titled β105 β Incident Assignment SummaryβRun:
SELECT CASE WHEN assigned_to IS NULL THEN 'Unassigned' ELSE assigned_to END AS assignment,
COUNT(*) AS incident_count
FROM incidentsGROUP BY assignmentORDER BY incident_count DESC;106 β Create a Security Review Query
Section titled β106 β Create a Security Review QueryβBuild:
SELECT a.hostname, a.criticality, a.owner,
COUNT( DISTINCT v.vulnerability_id ) AS open_findings,
COUNT( DISTINCT i.incident_id ) AS active_incidents
FROM assets AS a
LEFT JOIN vulnerabilities AS v ON a.asset_id = v.asset_id AND v.status = 'Open'
LEFT JOIN incidents AS i ON a.asset_id = i.related_asset_id AND i.status IN ( 'Open', 'Investigating' )
GROUP BY a.asset_id, a.hostname, a.criticality, a.owner
ORDER BY active_incidents DESC, open_findings DESC;107 β Important Join Pitfall
Section titled β107 β Important Join PitfallβWhen joining:
ONE ASSETto:
MULTIPLE VULNERABILITIESand:
MULTIPLE INCIDENTSrows may multiply.
That is why the previous query uses:
COUNT(DISTINCT ...)This is an important analytics skill.
108 β Save Your Queries
Section titled β108 β Save Your QueriesβCreate:
queries/|+-- 01_identity_review.sql|+-- 02_authentication_analysis.sql|+-- 03_vulnerability_analysis.sql|+-- 04_asset_hygiene.sql|+-- 05_incident_analysis.sql|+-- 06_security_metrics.sql109 β Identity Review File
Section titled β109 β Identity Review FileβPlace queries such as:
Privileged accounts
Users without MFA
Privileged users without MFA
Disabled accounts
Unused accountsinside:
01_identity_review.sql110 β Authentication Analysis File
Section titled β110 β Authentication Analysis FileβInclude:
Failed logins by user
Failed logins by IP
Unique users per source
Privileged login failures
Disabled-account attempts
Successful admin logins111 β Vulnerability Analysis File
Section titled β111 β Vulnerability Analysis FileβInclude:
Open findings
Critical findings
Critical assets with critical findings
Internet-exposed assets with findings
Overdue vulnerabilities
Prioritization query112 β Asset Hygiene File
Section titled β112 β Asset Hygiene FileβInclude:
Assets without owners
Duplicate IPs
Internet-exposed critical assets
Assets without findings
High-criticality unowned assets113 β Incident Analysis File
Section titled β113 β Incident Analysis FileβInclude:
Open incidents
Unassigned incidents
Critical unassigned incidents
Incidents by asset
Incidents on critical systems114 β Security Metrics File
Section titled β114 β Security Metrics FileβInclude:
MFA percentage
Failed login count
Open vulnerability count
Critical vulnerability count
Active incident count
Unowned asset count115 β Export Query Results to CSV
Section titled β115 β Export Query Results to CSVβInside SQLite:
.headers on.mode csv.output reports/privileged-users-without-mfa.csvThen run:
SELECT username, roleFROM usersWHERE privileged = 1AND mfa_enabled = 0AND account_status = 'active';Restore output:
.output stdout116 β Export Failed Login Summary
Section titled β116 β Export Failed Login SummaryβUse:
.output reports/failed-login-summary.csvThen:
SELECT username, COUNT(*) AS failed_loginsFROM login_eventsWHERE status = 'failed'GROUP BY usernameORDER BY failed_logins DESC;Return:
.output stdout117 β Export Vulnerability Priority Report
Section titled β117 β Export Vulnerability Priority ReportβCreate:
reports/vulnerability-priority.csvusing the prioritization query.
118 β Generate an Analyst Report
Section titled β118 β Generate an Analyst ReportβCreate:
reports/security-analysis-report.mdSuggested structure:
# SQL Security Analytics Report
## Executive Summary
## Identity Findings
## Authentication Findings
## Vulnerability Findings
## Asset Hygiene Findings
## Incident Findings
## Recommended Follow-Up
## Limitations119 β Example Identity Finding
Section titled β119 β Example Identity FindingβFinding:Privileged accounts without MFA
Evidence:admin02service_backup
Review:Validate whether MFA is technically applicableto each account and confirm documentedcompensating controls for non-interactiveservice identities.120 β Example Authentication Finding
Section titled β120 β Example Authentication FindingβFinding:Repeated failed logins against admin01
Evidence:3 failed events from 10.10.99.10,followed by successful authentication.
Review:Correlate with MFA, endpoint, VPN,identity-provider, and administrativeactivity before determining cause.121 β Example Vulnerability Finding
Section titled β121 β Example Vulnerability FindingβFinding:Critical open vulnerability on WEB01
Context:ProductionCritical assetInternet exposed
Review:Prioritize remediation according to theorganization's vulnerability managementprocess and validate compensating controls.122 β Example Asset Finding
Section titled β122 β Example Asset FindingβFinding:OLD01 has no documented owner.
Context:ProductionHigh criticalityOpen critical vulnerability
Review:Establish accountable ownership and determinewhether the system should remain operational.123 β Example Incident Finding
Section titled β123 β Example Incident FindingβFinding:Critical web-server incident remains unassigned.
Review:Validate incident ownership and escalationaccording to SOC procedures.124 β SQL Investigation Workflow
Section titled β124 β SQL Investigation WorkflowβWhenever you receive a security question:
QUESTION βWHICH TABLE? βWHICH ROWS? βWHICH COLUMNS? βNEED ANOTHER TABLE? βJOIN βGROUP? βAGGREGATE? βINTERPRET125 β Query Quality Principle
Section titled β125 β Query Quality PrincipleβDo not begin with:
SELECT *for every investigation.
Ask:
WHICH COLUMNSDO I ACTUALLY NEED?126 β Why SELECT * Can Be Risky
Section titled β126 β Why SELECT * Can Be RiskyβLarge tables may contain:
SENSITIVE DATA
LARGE PAYLOADS
UNNECESSARY COLUMNSPrefer:
DATA MINIMIZATION127 β Read-Only First
Section titled β127 β Read-Only FirstβIn security analytics, your default should often be:
SELECTnot:
UPDATE
DELETE128 β Dangerous UPDATE Example
Section titled β128 β Dangerous UPDATE ExampleβNever casually run:
UPDATE usersSET account_status = 'disabled';That affects:
EVERY ROW129 β Why WHERE Matters
Section titled β129 β Why WHERE MattersβCompare:
UPDATE usersSET account_status = 'disabled';with:
UPDATE usersSET account_status = 'disabled'WHERE username = 'example-user';Even then, modification should only happen with proper authorization and change control.
130 β Dangerous DELETE Example
Section titled β130 β Dangerous DELETE ExampleβThis:
DELETE FROM login_events;removes:
EVERY EVENTfrom the table.
Do not practice destructive commands against valuable datasets.
131 β Use Transactions for Controlled Changes
Section titled β131 β Use Transactions for Controlled ChangesβConceptually:
BEGIN TRANSACTION;Make an authorized change.
Review.
Then:
COMMIT;or:
ROLLBACK;For this lab, you do not need to modify the dataset after creation.
132 β Parameterized Queries
Section titled β132 β Parameterized QueriesβWhen applications construct SQL using user input, do not concatenate untrusted data into queries.
Conceptually avoid:
"SELECT ... WHERE username = '" + input + "'"Use:
PARAMETERIZED QUERIESthrough your application language.
133 β Why Parameterization Matters
Section titled β133 β Why Parameterization MattersβIt helps prevent:
SQL INJECTIONand improves query safety.
134 β SQL Injection Security Principle
Section titled β134 β SQL Injection Security PrincipleβApplication input should follow:
USER INPUT βVALIDATION βPARAMETERIZED QUERY βDATABASEnot:
USER INPUT βSTRING CONCATENATION βSQL EXECUTION135 β Database Least Privilege
Section titled β135 β Database Least PrivilegeβA reporting application may need:
SELECTbut not:
DROP TABLE
DELETE
CREATE USER
ADMINISTER DATABASE136 β Read-Only Security Analyst Role
Section titled β136 β Read-Only Security Analyst RoleβConceptually:
SECURITY_ANALYST βSELECT βSECURITY TABLESnot:
SECURITY_ANALYST βFULL DATABASE ADMIN137 β Protect Database Credentials
Section titled β137 β Protect Database CredentialsβDo not hard-code:
DATABASE USERNAME
DATABASE PASSWORD
CONNECTION STRING SECRETSinside scripts or Git repositories.
Use approved:
SECRET MANAGEMENT138 β Database Logging
Section titled β138 β Database LoggingβSecurity-sensitive databases should provide appropriate:
ACCESS LOGGING
QUERY AUDITING
ADMINISTRATIVE CHANGE LOGGINGaccording to organizational requirements.
139 β Backup the Lab Database
Section titled β139 β Backup the Lab DatabaseβCreate a backup:
cp database/security-lab.db \database/security-lab-backup.dbPowerShell:
Copy-Item ` ".\database\security-lab.db" ` ".\database\security-lab-backup.db"140 β Why Backups Matter
Section titled β140 β Why Backups MatterβSecurity data can be:
OPERATIONALLY IMPORTANT
AUDIT RELEVANT
INCIDENT RELEVANTBackup design should include:
CONFIDENTIALITY
INTEGRITY
RECOVERY TESTING141 β Add an Index
Section titled β141 β Add an IndexβIndexes can improve query performance.
Example:
CREATE INDEX idx_login_events_usernameON login_events(username);Create another:
CREATE INDEX idx_login_events_source_ipON login_events(source_ip);142 β Vulnerability Index
Section titled β142 β Vulnerability IndexβCREATE INDEX idx_vulnerabilities_assetON vulnerabilities(asset_id);143 β Why Indexes Matter
Section titled β143 β Why Indexes MatterβWithout suitable indexes:
LARGE TABLE βFULL SCAN βSLOW QUERYWith suitable indexing:
QUERY βINDEX βFASTER LOOKUPBut indexes also consume storage and affect writes.
144 β View Query Plan
Section titled β144 β View Query PlanβSQLite supports:
EXPLAIN QUERY PLANSELECT username, COUNT(*)FROM login_eventsWHERE status = 'failed'GROUP BY username;You do not need to become a database administrator.
But understand that query design affects performance.
145 β Create a Security View
Section titled β145 β Create a Security ViewβCreate:
CREATE VIEW privileged_users ASSELECT user_id, username, department, role, mfa_enabled, account_statusFROM usersWHERE privileged = 1;Query:
SELECT *FROM privileged_users;146 β Why Views Are Useful
Section titled β146 β Why Views Are UsefulβViews can:
SIMPLIFY COMPLEX QUERIES
STANDARDIZE REPORTING
LIMIT EXPOSED COLUMNSdepending on database platform and permissions.
147 β Create an Open Vulnerability View
Section titled β147 β Create an Open Vulnerability ViewβRun:
CREATE VIEW open_vulnerability_details ASSELECT a.hostname, a.criticality, a.owner, a.internet_exposed, v.finding_name, v.severity, v.cvss, v.remediation_due_dateFROM vulnerabilities AS vINNER JOIN assets AS a ON v.asset_id = a.asset_idWHERE v.status = 'Open';148 β Query the View
Section titled β148 β Query the ViewβRun:
SELECT *FROM open_vulnerability_detailsORDER BY cvss DESC;149 β Build a Priority Dashboard View
Section titled β149 β Build a Priority Dashboard ViewβCreate:
CREATE VIEW security_priority_dashboard ASSELECT a.asset_id, a.hostname, a.criticality, a.owner, a.internet_exposed,
COUNT( DISTINCT v.vulnerability_id ) AS open_findings,
COUNT( DISTINCT i.incident_id ) AS active_incidents
FROM assets AS a
LEFT JOIN vulnerabilities AS v ON a.asset_id = v.asset_id AND v.status = 'Open'
LEFT JOIN incidents AS i ON a.asset_id = i.related_asset_id AND i.status IN ( 'Open', 'Investigating' )
GROUP BY a.asset_id, a.hostname, a.criticality, a.owner, a.internet_exposed;150 β Query the Dashboard
Section titled β150 β Query the DashboardβRun:
SELECT *FROM security_priority_dashboardORDER BY active_incidents DESC, open_findings DESC;151 β Investigation Challenge 01
Section titled β151 β Investigation Challenge 01βWithout copying an earlier query, determine:
Which active privileged userwithout MFA had a successful login?Expected thinking:
users +login_events βJOIN152 β Investigation Challenge 02
Section titled β152 β Investigation Challenge 02βDetermine:
Which source IP generated failed loginsagainst the largest number of unique users?Use:
COUNT(DISTINCT ...)153 β Investigation Challenge 03
Section titled β153 β Investigation Challenge 03βDetermine:
Which critical production assetshave overdue open vulnerabilities?You need:
assets+vulnerabilities154 β Investigation Challenge 04
Section titled β154 β Investigation Challenge 04βDetermine:
Which assets have bothopen incidentsandcritical vulnerabilities?Use:
assets
vulnerabilities
incidents155 β Investigation Challenge 05
Section titled β155 β Investigation Challenge 05βDetermine:
Which active accounts have no MFAand at least one failed login?156 β Investigation Challenge 06
Section titled β156 β Investigation Challenge 06βDetermine:
Which assets have no documented ownerbut still have active incidents?157 β Investigation Challenge 07
Section titled β157 β Investigation Challenge 07βDetermine:
Which source IPs generatedonly failed authenticationand no successful authentication?Think about:
GROUP BY
HAVING158 β Investigation Challenge 08
Section titled β158 β Investigation Challenge 08βDetermine:
Which user had three or morefailed logins and at leastone successful login?Then manually inspect the timeline.
159 β Investigation Challenge 09
Section titled β159 β Investigation Challenge 09βDetermine:
Which open vulnerabilityhas the earliest overdue due date?160 β Investigation Challenge 10
Section titled β160 β Investigation Challenge 10βCreate one query that returns:
HOSTNAME
CRITICALITY
OWNER
OPEN FINDINGS
CRITICAL FINDINGS
ACTIVE INCIDENTSfor every asset.
161 β Build the Final SQL Portfolio
Section titled β161 β Build the Final SQL PortfolioβYour repository should contain:
sql-security-analytics/|+-- database/| +-- security-lab.db| +-- security-lab-backup.db|+-- queries/| +-- 01_identity_review.sql| +-- 02_authentication_analysis.sql| +-- 03_vulnerability_analysis.sql| +-- 04_asset_hygiene.sql| +-- 05_incident_analysis.sql| +-- 06_security_metrics.sql|+-- reports/| +-- privileged-users-without-mfa.csv| +-- failed-login-summary.csv| +-- vulnerability-priority.csv| +-- security-analysis-report.md|+-- README.md162 β README Structure
Section titled β162 β README StructureβInclude:
PROJECT OVERVIEW
SECURITY USE CASE
DATABASE SCHEMA
TABLE RELATIONSHIPS
DATASET DESCRIPTION
HOW TO CREATE DATABASE
HOW TO RUN QUERIES
SECURITY QUESTIONS
SAMPLE FINDINGS
SECURITY CONSIDERATIONS
LIMITATIONS163 β Document the Schema
Section titled β163 β Document the SchemaβAdd:
USERS | | username βLOGIN_EVENTS
ASSETS | +------β LOGIN_EVENTS | +------β VULNERABILITIES | +------β INCIDENTS164 β Entity Relationship Mental Model
Section titled β164 β Entity Relationship Mental Modelβ USERS | | β LOGIN EVENTS | β ASSETS / \ β βVULNERABILITIES INCIDENTS165 β Document Limitations
Section titled β165 β Document LimitationsβYour lab does not include:
REAL-TIME SIEM INGESTION
BILLIONS OF EVENTS
MULTI-TENANT SECURITY
FULL IDENTITY GOVERNANCE
CLOUD SECURITY TABLES
EDR TELEMETRY
THREAT INTELLIGENCE TABLES
ADVANCED TEMPORAL CORRELATION166 β Common SQL Security Analytics Mistakes
Section titled β166 β Common SQL Security Analytics MistakesβAvoid:
SELECT * EVERYWHERE
NO WHERE FILTER
WRONG JOIN TYPE
JOINING ON WRONG COLUMN
IGNORING NULL VALUES
COUNTING DUPLICATED JOIN ROWS
NO DATA NORMALIZATION
ASSUMING METRICS EQUAL RISK
NO TIME CONTEXT
NO ASSET CONTEXT
NO IDENTITY CONTEXT
RUNNING WRITE QUERIES ON PRODUCTION
HARDCODING DATABASE CREDENTIALS167 β Manual Validation
Section titled β167 β Manual ValidationβBefore trusting any query:
SELECT SMALL SAMPLE βMANUALLY COUNT βRUN ANALYTICS QUERY βCOMPARE RESULTS168 β Query Validation Example
Section titled β168 β Query Validation ExampleβManually review:
SELECT *FROM login_eventsWHERE source_ip = '10.10.88.20';Count the usernames.
Then compare against:
SELECT source_ip, COUNT(DISTINCT username)FROM login_eventsWHERE source_ip = '10.10.88.20'GROUP BY source_ip;169 β Security Analytics Principle
Section titled β169 β Security Analytics PrincipleβNever assume:
QUERY EXECUTED SUCCESSFULLY=QUERY ANSWER IS CORRECTA syntactically correct query can still contain:
BAD LOGIC
BAD JOIN
WRONG FILTER
WRONG ASSUMPTION170 β Mission Validation Checklist
Section titled β170 β Mission Validation ChecklistβConfirm:
- SQLite environment prepared
- Security database created
- Users table created
- Assets table created
- Login events table created
- Vulnerabilities table created
- Incidents table created
- Synthetic data inserted
- Basic SELECT queries completed
- WHERE filtering used
- ORDER BY used
- GROUP BY used
- HAVING used
- COUNT used
- DISTINCT used
- INNER JOIN used
- LEFT JOIN used
- CASE used
- NULL values handled
- Subquery used
- CTE used
- Privileged users reviewed
- MFA gaps reviewed
- Failed logins analyzed
- Failed sources analyzed
- Multi-user sources identified
- Disabled-account activity reviewed
- Vulnerabilities joined to assets
- Critical findings identified
- Overdue findings identified
- Unowned assets identified
- Unassigned incidents identified
- Security metrics generated
- Query results exported
- Analyst report created
- Queries manually validated
- Database backup created
- Destructive production queries avoided
- Limitations documented
Mission Review
Section titled βMission ReviewβYou started with:
SEPARATE SECURITY TABLEScontaining:
USERS
ASSETS
AUTHENTICATION
VULNERABILITIES
INCIDENTSYou transformed those tables into:
SECURITY QUESTIONS βSQL QUERIES βFILTERING βCORRELATION βAGGREGATION βSECURITY FINDINGSWhat You Built
Section titled βWhat You BuiltβYou now have a local enterprise-style security analytics database capable of answering questions about:
IDENTITY SECURITY
MFA COVERAGE
AUTHENTICATION FAILURES
PRIVILEGED ACCESS
ASSET CRITICALITY
VULNERABILITY EXPOSURE
ASSET OWNERSHIP
INCIDENT OWNERSHIPKey Security Lesson
Section titled βKey Security LessonβThe most important lesson from this lab is:
SECURITY ANALYTICSSTARTS WITHA QUESTIONDo not begin with:
WHAT QUERY CAN I WRITE?Begin with:
WHAT SECURITY QUESTIONDO I NEED TO ANSWER?Then identify:
WHICH DATA?
WHICH TABLES?
WHICH RELATIONSHIPS?
WHICH FILTERS?
WHICH TIMEFRAME?
WHICH CONTEXT?Final Mental Model
Section titled βFinal Mental ModelβSECURITY QUESTION βIDENTIFY DATA βSELECT βFILTER βJOIN βGROUP βAGGREGATE βVALIDATE βINTERPRET βSECURITY DECISIONSQL alone does not create security insight.
The real equation is:
STRUCTURED DATA +SQL +SECURITY CONTEXT +ANALYST JUDGMENT =ACTIONABLE SECURITY INSIGHTWhatβs Next?
Section titled βWhatβs Next?ββ‘οΈ Lab 06 β Vulnerability Data Analysis and Prioritization
The next lab takes the vulnerability concepts from this SQL lab and turns them into a complete Python-based security workflow.
You will build:
VULNERABILITY SCANNER EXPORT βPYTHON βVALIDATE βNORMALIZE βDEDUPLICATE βASSET CONTEXT βRISK PRIORITIZATION βOWNER MAPPING βREMEDIATION REPORTYou will work with:
SEVERITY
CVSS
ASSET CRITICALITY
EXPOSURE
OWNERSHIP
REMEDIATION DUE DATES
DUPLICATE FINDINGSand convert raw vulnerability data into an analyst-ready remediation queue.