I'm a Cyber Security analyst working in a self-managed live CSOC environment. What I can show you is the part most "I connected logs to a SIEM" projects skip: writing detections that catch real attacker behaviour while staying quiet on legitimate activity, and being honest in writing about the false positives, telemetry blind spots, and licensing constraints I ran into along the way.
My coherent thread across this lab is AI threat detection across identity and endpoint.
I also write blogs, turning the papers and ideas I'm reading into plain-English pieces on detection, incident response and human-centric cybersecurity.
This site showcases practical detection writeups, blue team strategies, and case studies drawn from both lab and research findings.
Windows and Entra ID telemetry into a Log Analytics workspace. Most fire on a simulated attack, confirmed quiet on benign activity where complete; the three AI-misuse rules go as far as the evidence allows, and each card says exactly where it stands.
Every card expands. Detection 05 is the one with automated response and a coverage audit behind it.
An attacker repeatedly attempts to log in with guessed credentials. Each failure writes a Windows Event ID 4625. A burst of these against one host in a short window is a strong brute-force signal.
// filter to failed logons → extract the attacked account from the
// event text → count failures per host in 5-minute windows
Event
| where EventLog == "Security"
| where EventID == 4625
| extend TargetAccount = extract(@"Account For Which Logon Failed:[\s\S]*?Account Name:\s+(\S+)", 1, RenderedDescription)
| where isnotempty(TargetAccount) and TargetAccount != "-"
| summarize FailedAttempts = count(), TargetedAccounts = make_set(TargetAccount) by Computer, bin(TimeGenerated, 5m)
| where FailedAttempts >= 5
Simulated a brute-force burst on the lab VM via repeated runas failures. The detection returned a single row: 25 failed attempts in one 5-minute window, attacker usernames captured. The scheduled rule raised an incident automatically (Credential Access category).


RenderedDescription text, not a dedicated field — extracted via regex.>= 5 threshold reflects real attack tempo, not test tempo.Attackers frequently launch PowerShell with flags legitimate users rarely combine: encoded commands, hidden windows, execution-policy bypass, download cradles. Sysmon logs every process launch as Event ID 1 with the full command line.
Event
| where Source == "Microsoft-Windows-Sysmon"
| where EventID == 1
| extend Image = extract(@"Image:\s+(\S+)", 1, RenderedDescription)
| extend CommandLine = extract(@"CommandLine:\s+(.+?)\s+CurrentDirectory:", 1, RenderedDescription)
| where Image has "powershell" or Image has "pwsh"
| where CommandLine has_any ("-enc", "-EncodedCommand", "-nop", "-noprofile",
"-w hidden", "-windowstyle hidden", "-exec bypass", "-executionpolicy bypass",
"downloadstring", "iex", "invoke-expression", "frombase64string")
| project TimeGenerated, Computer, Image, CommandLine
Ran benign-but-suspicious-looking PowerShell (harmless — carries the flags attackers use, does nothing damaging). The detection returned all four launches with command lines correctly matched.


-nop and -ExecutionPolicy Bypass are occasionally used by legitimate admin scripts. Production tuning: require 2+ suspicious flags together, or exclude known-good signed scripts by hash/path.To survive reboots, malware commonly writes itself into a registry autostart location — CurrentVersion\Run, RunOnce, or Winlogon shell keys. Sysmon logs registry modifications as Event ID 13.
The naive version fired on real, legitimate activity: userinit.exe writing ctfmon.exe (Windows' own text/input service) to a Run key. The rule wasn't wrong — it correctly matched "a write to an autostart location" — it was too broad. A rule that fires on benign activity is worse than useless: it trains analysts to dismiss the alert, so a real attack tripping the same rule gets ignored too.
Event
| where Source == "Microsoft-Windows-Sysmon"
| where EventID == 13
| extend TargetObject = extract(@"TargetObject:\s+(\S+)", 1, RenderedDescription)
| extend Details = extract(@"Details:\s+(.+?)\s+(?:User:|Image:)", 1, RenderedDescription)
| extend Image = extract(@"Image:\s+(\S+)", 1, RenderedDescription)
| where TargetObject has_any (@"CurrentVersion\Run", @"CurrentVersion\RunOnce",
@"Winlogon\Shell", @"Winlogon\Userinit")
// narrow allowlist — excludes only the exact benign combination,
// not userinit.exe wholesale (a broad exclusion would let an
// attacker abuse userinit.exe slip straight through)
| where not(Image endswith "userinit.exe" and Details has "ctfmon.exe")
| project TimeGenerated, Computer, TargetObject, Details, Image
True positive — planted a fake persistence entry via New-ItemProperty: caught. False positive — the legitimate ctfmon.exe write: correctly did not fire after tuning. The scheduled rule raised a High-severity incident with an attack-story graph.


When an attacker first lands on a host, they orient themselves — whoami, net user, systeminfo, ipconfig, tasklist, arp, and similar. No single command is malicious; every one runs during legitimate administration. The signal is variety — many different recon tools from one host in a short window — not any individual command.
Event
| where Source == "Microsoft-Windows-Sysmon"
| where EventID == 1
| extend Image = extract(@"Image:\s+(\S+)", 1, RenderedDescription)
| extend ProcessName = tolower(tostring(split(Image, "\\")[-1]))
| where ProcessName in ("whoami.exe","net.exe","net1.exe","nltest.exe",
"systeminfo.exe","ipconfig.exe","tasklist.exe","hostname.exe",
"nslookup.exe","arp.exe","route.exe")
| summarize ReconCommands = make_set(ProcessName),
DistinctReconCount = dcount(ProcessName) by Computer, bin(TimeGenerated, 10m)
| where DistinctReconCount >= 5
The key line is dcount(ProcessName) — distinct commands, not total. Ten runs of whoami alone isn't recon; whoami + net + nltest + systeminfo + ipconfig is. The detection measures variety, not volume.
Ran a 10-command recon burst. The detection returned a single row: distinct count of 9, full command set captured. Incident raised under Discovery category.


ARP.EXE), others lowercase. Without normalising case, dcount silently under-counted and the detection appeared broken when it wasn't — fixed with tolower(...).Once an attacker has a foothold on a credential, registering their own MFA method is one of the cleanest ways to keep access — the victim can reset their password and the attacker is still in. "User registered a new MFA method" looks completely normal in isolation; the signal isn't the registration itself, it's the registration happening shortly after a sign-in that doesn't look like the account owner.
SignInLogs.IPAddress. Coarser than a real risk score (no device/location-velocity/threat-intel scoring behind it) — documented as a tradeoff, not pretended away.
Correlates two Entra ID signals within a 30-minute window: a sign-in from an untrusted IP, and a same-user AuditLogs entry for "User started security info registration."
let TrustedIPs = dynamic(["<office/home IP 1>", "<office/home IP 2>"]); let UntrustedSignIns = SigninLogs | where IPAddress !in (TrustedIPs) | where ResultType == 0 | project SignInTime = TimeGenerated, UserPrincipalName, IPAddress, Location; let MFARegistrations = AuditLogs | where ActivityDisplayName == "User started security info registration" | extend UserPrincipalName = tostring(InitiatedBy.user.userPrincipalName) | project RegTime = TimeGenerated, UserPrincipalName; UntrustedSignIns | join kind=inner MFARegistrations on UserPrincipalName | where RegTime between (SignInTime .. (SignInTime + 30m)) | summarize arg_min(SignInTime, *) by UserPrincipalName, IPAddress | project SignInTime, RegTime, UserPrincipalName, IPAddress, Location
Baseline sign-in from my real AU IP → connected via Proton VPN (Seattle, US exit node), signed in again from the untrusted IP → registered a new MFA method from that same VPN session, the actual T1556.006 action.



Every other detection in this lab stops at "incident created." This one doesn't. A Sentinel automation rule fires on incident creation, runs an Azure Logic App, and posts a triage comment recommending the first containment step — revoking active sessions — referencing the IR playbook.
The original plan had the playbook write to a Sentinel watchlist (auto-blocking the flagged IP). The watchlist UI in Defender XDR was broken at build time, making watchlist writes from Logic Apps unreliable. I scoped the automation down to "add comment to incident" — a smaller, reliable action — rather than ship something flaky for a better demo.


summarize dedup only protects within a single run's lookback window.Employees running local or desktop AI tooling — LLM runners, AI coding assistants, desktop chatbot clients — outside any sanctioned IT process. Not inherently malicious; it's a visibility gap, since unsanctioned AI tools can move data through model context without enterprise DLP or logging.
Event
| where Source == "Microsoft-Windows-Sysmon"
| where EventID == 1
| extend Image = extract(@"Image:\s+(\S+)", 1, RenderedDescription)
| extend ProcessName = tolower(tostring(split(Image, "\\")[-1]))
| where ProcessName has_any (
"ollama.exe", "llamafile.exe", "lm-studio.exe", "lmstudio.exe",
"chatgpt.exe", "claude.exe", "gemini.exe", "copilot.exe",
"cursor.exe", "aider.exe", "openai.exe"
)
| project TimeGenerated, Computer, Image, ProcessName
True positive: installed real Ollama on the lab VM, confirmed the process running, queried Sentinel — 5 matching rows with a clean full path.
False-positive side: not yet tested. Only one validation pass has been run. Stated plainly rather than implying a clean bill of health.


Not a tuning exercise in the usual sense — two real telemetry limitations forced a redesign. CDN-edge hostnames break domain matching: even confirmed real connections showed DestinationHostname as a CDN edge name or blank, never the literal domain. Browser traffic is invisible to this telemetry entirely: a systematic check showed zero msedge.exe rows despite multiple tabs open, because the SwiftOnSecurity Sysmon baseline config excludes browser processes from network-connection logging by design.
This means the rule, as built, cannot detect browser-based AI usage — e.g. pasting data into a web chat UI — only non-browser, direct-process egress. Documented as an open, known gap, not silently worked around.
Event
| where Source == "Microsoft-Windows-Sysmon"
| where EventID == 3
| extend Image = extract(@"Image:\s+(\S+)", 1, RenderedDescription)
| extend ProcessName = tolower(tostring(split(Image, "\\")[-1]))
| extend DestinationPort = extract(@"DestinationPort:\s+(\S+)", 1, RenderedDescription)
| where ProcessName has_any ("ollama.exe", "llamafile.exe", "lmstudio.exe", "python.exe", "node.exe")
and DestinationPort in ("443", "80")
| project TimeGenerated, Computer, Image, DestinationPort
True positive: Ollama on the VM was making real outbound calls to Google-hosted infrastructure on port 443 — observed organically, confirming the rule catches real egress from known AI tooling. 8 matching rows.
The browser-exclusion limitation is written directly into the rule description in Sentinel. No incident screenshot for this one by design — the writeup documents a telemetry-scope finding and rule configuration, not an incident walkthrough.
AI agent frameworks that autonomously execute commands by spawning a shell as a child process — LangChain-style agents, AI coding assistants, local LLM tools. Not inherently malicious — this is how agentic tools legitimately operate — but a meaningfully different risk profile from a human typing commands.
Event
| where Source == "Microsoft-Windows-Sysmon"
| where EventID == 1
| extend Image = extract(@"Image:\s+(\S+)", 1, RenderedDescription)
| extend ParentImage = extract(@"ParentImage:\s+(\S+)", 1, RenderedDescription)
| extend ProcessName = tolower(tostring(split(Image, "\\")[-1]))
| extend ParentProcessName = tolower(tostring(split(ParentImage, "\\")[-1]))
| where ParentProcessName has_any (
"ollama.exe", "llamafile.exe", "lmstudio.exe",
"claude.exe", "chatgpt.exe", "cursor.exe", "aider.exe",
"python.exe", "node.exe"
)
| where ProcessName has_any ("cmd.exe","powershell.exe","pwsh.exe","wscript.exe","cscript.exe","bash.exe")
| project TimeGenerated, Computer, ParentImage, Image
python.exe and node.exe as parent processes is broad — almost any script that legitimately shells out for build tools, package managers, or CI would trigger this. Only one validation data point exists (a synthetic test), so this hasn't been exercised against real-world usage patterns yet. Stated as an open tuning item, not pretended away.
True positive: a 2-line Python script calling subprocess.run("cmd.exe /c echo agentic_test", shell=True) created a real python.exe → cmd.exe event, caught on the first attempt.


Detection 5 is paired with a complete incident response runbook — triage steps, containment sequencing, eradication, post-incident hunting query, and an escalation model framed around a solo-analyst / small-team reality rather than a hypothetical large SOC.
A Flask-based identity and access management system for an aged-care setting, where the access model has to map cleanly onto real care roles: administrators, nurses, carers, and residents. The platform layers TOTP-based MFA, JWT session management, bcrypt password hashing with Have I Been Pwned breach-checking, and a honeypot subsystem that emails admins on intrusion attempts.
My piece was the role-based access control layer. Rather than a flat role check, access is granted per staff-to-resident assignment with explicit read vs. write levels — a nurse can write care plans only for residents they're assigned to, a carer gets read-only on theirs, an admin sees everyone, and a resident sees a limited view of their own vitals. The access checks are enforced server-side against the assignment table on every request, so the permission boundary doesn't depend on the UI hiding things it shouldn't.
A multi-site Active Directory environment built to practice the structural side of identity administration: promoted a second domain controller, stood up a Paris AD site on its own subnet, transferred the RID operations master (FSMO) role, and built a delegated-permissions OU structure — a Paris Admins security group with password-reset rights scoped only to the Paris OU, plus a contractor account with a hard expiry date. The point was least-privilege delegation done properly: giving a group exactly the rights it needs over exactly the objects it should touch, and nothing wider.
A login-only intrusion, step by step, and where the detection actually lives
Nine common sources and the gaps they carry by design
A coverage audit of my own Sentinel detections
The line that excuses broken process — and what it costs dwell time
More technical writing at kajalbuilds.hashnode.dev