
Search Results
Search this site
583 results found with an empty search
- SentinelOne Prompt Security: The Browser Extension
Most people still use AI in a browser tab. ChatGPT, Claude, Gemini, the Copilot sidebar, the "Help me write" button in Gmail - it all lives in the browser. So the extension is the half of Prompt Security that will see most of the action, and it is where I started. The short version: installing it by hand is fine for a demo and useless as a control. Pushing it by policy fixes most of that, but even then private windows stay open unless you add browser policies the vendor package does not set for you. The rest of this part is how I got there. Installing it by hand The console lists 17 browsers you can install it into manually, from Chrome and Edge down to Arc, Island and Dia. I started with Edge, from the Edge Add-ons store. Version 7.1.62, Manifest V3, under a megabyte. Manual Installation: Using Policy: The Deployment page: 17 browsers, each with its own store link, and the keys underneath Two small things worth knowing before you go further. The Edge ID is not the Chrome ID. The console's Google Workspace page gives you the Chrome Web Store ID (iidnan****cecmgpcaf**************. The Edge store copy is a different ID (pbb******ofiifphflkh************). If you force-install in Edge, use the Edge ID - or the Chrome ID together with the Chrome store update URL. Mix them up and nothing installs. The Google Workspace instructions hand you the Chrome Web Store ID - which is not the ID Edge uses It asks for a lot - which is normal for DLP, but say so. Host access to every site, tabs, scripting, downloads, and your signed-in email address. It has to read the page to do its job. Your users should still hear that from you before they see it in a permission prompt. What the extension asks for: every site you visit, tabs, scripting, downloads - and your e-mail address It does nothing until someone configures it After install there is no setup screen. The icon is not even pinned to the toolbar. You have to open the extension popup yourself and enter two things: the API domain and the extension key from the console. The whole configuration screen: an API domain, a key, two log buttons and the e-mail it thinks you are Until then it protects nothing - but it already talks home. The extension log showed a heartbeat going out about two minutes after install, before I had entered any key. Small trap that cost me some time: there are three different keys in this product. A management API key (starts with psk_) for the REST API, an extension key, and a separate agent key. The extension and agent keys both look like plain UUIDs. Put the wrong one in the wrong place and you get a 401 with no hint as to why. Who it thinks you are Identity is not one thing. Where the user is signed in to the AI site itself, that login address is what the console shows - so a personal ChatGPT account puts a personal email address into your tenant, on a work laptop, and that event will not match any rule you wrote for the person's company address. Where there is no such login, it falls back to the company email or the Windows username, and that fallback is what the MDM attribution setting fills in. A hand-installed extension may report no address at all, in which case rules written for named users silently never match and everything lands on the Default rule instead. The practical upshot: get attribution set before you write user-specific rules, and if you are a provider looking at a client's console, remember whose email addresses you are looking at - and blank them out of any screenshot you share. The first test surprised me I pasted my fake AWS key into ChatGPT and got "Access Denied: Paste content is blocked by your administrator". Fine - except my user was meant to be on a Detect-only rule. Then I pasted the word "hello" and got the same block. Any paste into ChatGPT, fake key or the word "hello", came back as Access Denied The cause was the default rule. Under its Advanced settings it had "Block content paste" and "Block file upload" switched on. Nothing on the main rule screen tells you that - the Advanced column only shows a small "Data protection 2" badge, which is where I should have looked (Part 2 has the full tour of that screen). And the blocked pastes never showed up in the Activity Monitor at all - so the SOC would have no record that anyone tried. The rule list: the default rule at priority 4 is Detect, but its Advanced column carries the paste and upload blocks When I typed the key instead of pasting it, I got the behaviour I expected for Detect mode: a pop-up warning, an event in the console (Secrets / AWS credentials, rule 4, Default Employees policy, app "ChatGPT (Personal)", inspection took 12 ms) - and the full key went to ChatGPT anyway. Detect warns. It does not stop anything. More on that in Next Article Typed, not pasted: a warning pop-up, an event in the console - and the key still goes through Files take a different path The prompt box is only half of it. People drag documents into a chat as readily as they paste text, and a file goes through the extension differently. Two switches under a rule's Advanced settings decide what happens to it. Block file upload stops the file leaving at all. In Protect mode the upload is stopped with a violation pop-up and the event is logged. Inspect file upload to AI apps decides whether the extension reads what is inside the file. It is set per rule, and on the Default rule it is off. *(Important) That second switch (Inspect file upload ) is the one to watch, because the difference is total. With it on, a text file carrying a test card number was uploaded to ChatGPT, inspected, and raised a violation. With it off, the same upload landing on the Default rule was not inspected at all: the console logged the file name and "File inspect: false", no violation, the whole conversation readable - and the AI read the card number back in its reply. Same tenant, same file, same product. A different rule. And the Default rule is where personal accounts and anyone you forgot to write a rule for end up. With inspection on, it goes further than reading plain text: Redaction works inside files. With Secrets set to Hash, the secrets inside an uploaded text file were hashed before the file reached the AI, rather than the upload being blocked outright. It reads text inside images. A screenshot of an API key, a screenshot of card and ID data, and the same image embedded inside a Word document were all blocked. Clean images and a clean Word file went through untouched. Downloads are a third switch. With "Block file download" on, a CSV the AI generated was blocked with a pop-up. An image the AI generated was not. Two limits are worth knowing before you promise anyone file coverage: File inspection and reply logging happen only in the browser extension, and only for a fixed list of apps - ChatGPT, Microsoft Copilot, Gemini, Claude, Perplexity, Mistral, Grok, DeepSeek, Jira Rovo, Google AI Mode, Meta AI and NotebookLM on the vendor's October 2026 list. Any other AI site gets prompt checks only, including sites you add yourself as custom apps. The block message doesn't match the block. A blocked text file gets a proper "Your File Was Blocked" pop-up listing how many violations were found. A blocked image or Word file gets a generic "File upload blocked" message that reuses the blocked-site text and suggests the user go and use ChatGPT instead - while they are sitting on ChatGPT. I had this wrong at first. I uploaded a file under a rule with inspection off, saw nothing in the console, and wrote "the extension doesn't scan uploaded files" down as a product gap. My colleague ran the same tests with the switch on and got the opposite result. The capability is there. It is off until someone turns it on, rule by rule - which is a documentation problem rather than a detection one, and the kind of thing that quietly survives a proof of concept. Taking it off again Removing a manually installed extension is one click on the Remove button. Any user can do it, and the console raises no alert. The "Clear Logs" button in the popup only clears the local log; the event already in the console stays. Three easy ways around a manual install With the extension installed and working in one Edge profile, I tried the obvious things a user might do - on purpose or just by accident. Use another browser. Brave had no extension. The fake key went straight to ChatGPT. Nothing in the console. Use another profile. A manual install only lives in the profile you installed it in. A second profile in the same browser has no extension and is not watched at all. Open a private window. In Edge, "Allow in InPrivate" is off by default, so InPrivate is unwatched until the user turns it on. Firefox asks at install time, so it depends on what the user ticked. A colleague running a parallel evaluation found one more: Guest mode. The extension does not run there either. None of this is a clever bypass. It is just how browser extensions work. But it means a manual install is a demo, not a control. Force-installing it by policy In a real company you would push the extension with Intune, GPO or similar. I did the same thing by writing the policy keys into the registry myself, so I could see each step. Step 1 - force-install only. I added the extension to Edge's ExtensionInstallForcelist. Edge installed it, showed "managed by your organization" and took away the Remove button. Locked down, nice. Then I pasted the fake key and it went straight through. A force-installed copy is a fresh copy, and a fresh copy has no API domain and no key. ***Forcing the install without pushing the settings gives you an extension the user can't remove that also does nothing. Step 2 - push the settings too. The extension reads its config from a second policy location (...\Edge\3rdparty\extensions\\policy): apiDomain and apiKey. With both set, it worked straight away - pop-up on the key, paste blocked, nothing typed by the user. A brand-new Edge profile: the extension is already there, already configured, and already blocking It also fixed the profile gap. I created a second Edge profile and the extension was already there, configured, without anyone touching it. A colleague confirmed a normal user can't switch off, remove or inspect a force-installed copy, even with Developer mode on. A few things to be aware of: The key sits in plain text under HKLM\SOFTWARE\Policies. Any local user can read it. One key per machine. A device belongs to one tenant. Fine for most companies, worth knowing if you are a provider. Private windows are still open. With force-install and pushed config, an InPrivate window still let the key through unchecked. More on fixing that below. Cleanup was clean. When I removed the policy values, Edge uninstalled the extension by itself. What's in the vendor's MDM package The console offers a ready-made Windows package for Intune ("Multi Browsers"). I downloaded it and read it before running anything. It is two PowerShell scripts: one to install, one to change settings later (for example to rotate the key). The ready-made "Multi Browsers" package for Intune, next to the per-browser instructions What it does well: It force-installs and pushes the config in one go, so you don't hit the "locked but useless" problem. It merges into existing browser extension policies instead of overwriting them. It covers Chrome, Edge, Brave, Vivaldi, Chromium and a list of newer Chromium browsers (Whale, Comet, Dia, Atlas and others), plus Firefox. It writes policy keys for every browser on its list, installed or not - so a browser someone installs next month picks it up automatically. What I would change before using it: The API key is typed in plain text at the top of both scripts. Treat the package itself as a secret. Don't email it around or leave it on a shared drive. Opera isn't covered, and neither is any portable browser. The user name it reports is the Windows username, not an email, unless you fill in the hook in the script yourself. Nothing for private windows. Not one line. No uninstall script came with the Intune package. Removing it means undoing every browser's policy by hand. A colleague who tested the separate removal script found it deletes the whole Firefox policy file rather than just its own entry, and uses the wrong path for Naver Whale, so the key stays in the registry there. Closing the private-window gap This is the bit I most wanted to get right, because "just open an incognito window" is the first thing anyone will try. There are two ways to close it: force the extension on in private windows, or switch private windows off. Not every browser lets you do the first. Browser Force the extension into private windows Or switch private windows off Tested? Edge Yes - MandatoryExtensionsForInPrivateNavigation (Edge 139+) InPrivateModeAvailability = 1 Yes Firefox Yes - ExtensionSettings with "private_browsing": true (Firefox 136+ / ESR 128.8+) PrivateBrowsingModeAvailability = 1 Yes Chrome No on Windows - the matching policy is ChromeOS only for now IncognitoModeAvailability = 1 Docs only Brave, Vivaldi, Chromium No IncognitoModeAvailability = 1 under each browser's own policy key Docs only Comet, Whale, Dia, Atlas, GenSpark, Sigma Nothing documented Unclear No The two I tested behave differently, and it is worth knowing how. Firefox actually forces the extension on. With private_browsing set, the toggle in about:addons is replaced with a locked "Requires access to private windows", and my fake key typed in a private window showed up in the console. Edge doesn't force it on. It refuses to open an InPrivate window until the user allows the extension there. With the toggle off, InPrivate was blocked with an "organization" message. Toggle on, it opened and the key was logged. Toggle off again, blocked again. Either way, there's no unwatched InPrivate. One catch: the new policy didn't apply until I reloaded policies and killed Edge's background process. (Microsoft also notes this policy doesn't apply to profiles signed in with a personal Microsoft account.) For Chrome on Windows there is no way to force it into incognito today, so the only option is to switch incognito off. For Guest mode, switch that off too (BrowserGuestModeEnabled = 0 in Edge and Chrome). When it can't reach home One more finding from my colleague, and it's an important one. When the laptop can't reach the Prompt Security service, the extension lets everything through - and logs nothing. There is a "Fail Close" setting, and it was tested: prompts still went through, with no block and no record afterwards. The switch lives in Sensor Settings, under General Settings, and it ships on Fail Open. Extension settings: Fail open/close set to Fail Open out of the box, next to the employee-bypass and non-work-profile options (Important) So anyone who can break that connection - a hosts-file entry, a firewall rule, a flaky network - has an unwatched AI session, and you'll never see it in the console. What I would actually deploy If I were rolling this out to a client tomorrow: Never rely on manual installs. Force-install by MDM and push the settings in the same step. Add the private-window policies yourself - Edge and Firefox can force it, Chrome and the rest need incognito switched off. Switch off Guest mode too. Deal with the browsers it doesn't cover. Opera, portable browsers and anything not on the list should be blocked by application control, or the extension is a speed bump. Treat the install package as a secret, and write your own uninstall script before you need it. Set the user name to the work email so alerts point to a person, not a Windows login. Check the default rule's Advanced settings. That's where paste and upload blocking hide - and blocked pastes aren't logged. **Watch connectivity to *.prompt.security** from your EDR or SIEM, because if it drops, the extension fails open without a word. If you take one thing from this part: the extension is only as good as the policy that pushes it. A manual install shows you what the product can do. A proper MDM rollout with private-window policies is what actually covers your users. - written from hands-on testing, By Iqra Shafi, Dean Iqra : https://www.linkedin.com/in/iqrabintishafi/ Dean: https://www.linkedin.com/in/akash-patel-097610202/ ------------------------------------------Cyberengage---------------------------------------------------
- SentinelOne Prompt Security: How the Console Fits Together
Before I get into the browser extension and the agent, I want to walk through the console itself. Almost every result later in this series - a key that got redacted, a key that sailed straight through, a pop-up that never showed - comes back to how four screens in the console work together. If you understand those four, the rest of the series makes sense. If you don't, half of it looks random. It took me a good few days of testing to get this picture straight, mostly because the console splits one decision across several places. So this part is the map I wish I'd had on day one. The four tabs, and how they connect Everything for employee AI use lives under one area with four tabs: Security Rules - who, which AI app, which platform, and what mode (watch, protect, block or ignore). The rule also picks which policy to use. Policies - what to look for in a prompt (secrets, personal data, your own patterns, topics) and what to do when it's found (redact or block). Sensor Settings - how the two pieces on the laptop behave: the browser extension and the endpoint agent. User Education - what the employee actually sees: warning pages, block messages, pop-ups. The way I explain it to people: the rule decides if and how the policy decides what the sensor does the work the user education does the talking. A prompt comes in, the first rule that matches decides the mode and hands it to a policy, the policy's detectors look at the text, and the sensor carries out the result. That split matters because a policy on its own does nothing. You can set "Block" on every detector in a policy, and if the rule that points to it is in Detect mode, nothing gets blocked. I fell for exactly that early on - the policy said block, the dashboard said 0% blocked, and it took a while to see why. Security Rules Each rule is one row with the same columns: Priority - rules are checked top-down and the first match wins. Nothing after it is looked at. Users - all users, named users (by e-mail), or groups once an identity provider is connected. Applications - one or more specific AI apps, a category, "All applications", or "AI code assistants". Several big apps come in two flavours, for example "ChatGPT (Personal)" and "ChatGPT (Enterprise)", so you can treat a personal account differently from the company one. The same goes for Claude, Gemini, Copilot and Perplexity. Platform - web (the browser extension) and/or desktop apps (the agent), with a count of how many desktop apps the rule covers. Mode - what happens (below), plus the policy it uses. Advanced - three drop-downs: Data protection, Logging and User education. At the bottom sit two rules you can't move or delete. The Default rule (All users / All applications / Detect / Default Employees Policy) catches everyone who didn't match anything above it. Just above it is a second locked rule for AI code assistants (All users / Detect / Default Developers Policy). Their users and apps fields carry a lock icon, so you can change the mode and the advanced settings but not who or what they apply to. The four modes Mode What it does Detect Inspects the prompt, logs violations, can show a pop-up - but lets the prompt through untouched Protect Inspects and applies the policy's action: redact the sensitive part, or block the prompt Block Blocks the app outright, without reading the content Don't Track No inspection, no logging, for that user and app Detect is visibility only. The AI gets the data and will happily repeat it back. If you want something to actually stop, you need Protect (or Block). What hides under "Advanced" This is the part of the rule screen I'd tell everyone to open, because the main row doesn't show it: Data protection - block content paste, block file upload, inspect uploaded files, block file download. Logging - log all interactions, or log only violations. Logging of AI replies is also set here, per rule. User education - whether to show the violations pop-up and the warning page for this rule. My very first test hit this. Pasting anything into ChatGPT - even the word "hello" - was blocked. The default rule had "Block content paste" and "Block file upload" switched on under Advanced, and the main row gave no hint of it. Worse, the blocked pastes never showed up in the activity log at all, so an admin would have no record that anyone tried. Rule traps I hit "All applications" doesn't include code assistants. A rule for "All applications" never touched Claude Code, Copilot in VS Code or OpenCode. Code assistants need a rule that names them, or they fall to the locked code-assistant rule. Code-assistant rules only take the Developers policy. Point a rule that includes a code assistant at any other policy and you get a red (!) next to the priority, with the reason only in a tooltip: "This rule uses a policy that might slow down responses. Since this rule affects AI code assistants, please use an AI code assistant policy." Rules for named users only work if the sensor knows who the user is. A rule for one e-mail address silently never matched, because the manually installed extension wasn't reporting an e-mail. The prompt fell to the Default rule instead. On the agent, the user is set at install time (a USER_EMAIL value); without it, the console only sees a Windows account name. Changes aren't instant on the agent. The agent caches its decision per AI site. A rule change only took effect after the next check-in, roughly 10 to 14 minutes, or after restarting the agent service. Personal accounts fall to the Default rule unless you write a rule for the "(Personal)" version of the app. A colleague tested this: a Block rule on "ChatGPT (Personal)" stopped a personal account while the company one kept working. A rule that mixes code assistants with an Employees policy gets a red (!) - the reason is only in the tooltip Policies A policy is the list of things to look for and what to do with them. You manage them from the policy drop-down at the top of the Policies tab, through a "Manage Policies" window. Manage Policies: the Developers policy can't be renamed, deleted or duplicated; Employees-type policies can There are two kinds: Employees policies - for AI in the browser and desktop chat apps. You can add as many as you like, rename them, duplicate them and delete them. AI code assistants - there is exactly one, the Default Developers Policy. It can't be renamed, deleted or duplicated, and any new policy you create is always the Employees type. What a policy can contain Each section has its own on/off switch and applies to prompts. (Replies from the AI aren't scanned for employee use - by design, that's only done in the other product modules.) Sensitive Data. Personal and regulated data, grouped by compliance framework: PCI, PII, GDPR, CCPA, PHI, COPPA, PDPA and IT. In my tenant that was 127 data types in total. You choose Block or Redact, and there's a switch to replace the value with realistic dummy data instead of a placeholder. Below that you can add your own data types, "hidden terms" that always get redacted (say, an internal project name), and "allowed terms" the detector should ignore. Secrets. API keys, tokens and credentials - 238 detectors when I tested, from AWS and GitHub to Slack, GitLab and private keys. The action is a drop-down: redact (all or partial) or block. You can open the detector list and switch individual ones off. One thing I didn't expect: a brand-new policy starts with only 203 of the 238 secret detectors on. Among the ones switched off were both AWS Bedrock API key types. If you create a new policy and assume it has the same coverage as the default, check that list first. A new policy: 203 of 238 secret detectors on, with the Bedrock API key detectors off Regex. Your own patterns - customer IDs, ticket numbers, internal host names. Block or Redact, with your own replacement text. Two quirks from testing: there's no case-insensitive flag, and if your pattern has a bracketed group, only that group gets redacted and the rest of the match goes through. A pattern without brackets replaces the whole match. Topics. Two lists: topics to block, and topics to just monitor. You type the topic in plain words ("investment advice", "hiring questions") and it's classified by AI. A colleague found it accurate, but the wording of the topic matters. Harmful Content. Categories such as hateful, harassment, self-harm and public harm, with three levels. "More restrictive" blocked normal business talk in testing; "less restrictive" missed phishing. "Balanced" was the usable one. Natural Language Guardrails. Up to 5,000 characters describing in plain English what to block and what to allow ("block patient details, allow general medical questions"). This turned out to be one of the strongest features. In a Iqra´s tests, a single guardrail caught things the fixed detectors missed: names with dates of birth, patient diagnoses, passwords written into a sentence, keys split across two lines, and jailbreak attempts - while still allowing general questions on the same subjects. Changes can take a few minutes to apply. Natural Language Guardrails: a plain-English rule, up to 5,000 characters What the two default policies actually check Out of the box, the Default Employees Policy had Sensitive Data and Secrets switched on. The Default Developers Policy had only Secrets switched on. So by default, code assistants are checked for keys and tokens, but not for personal data, not for your own patterns, topics or guardrails. If you want a developer pasting a customer list into Claude Code to be caught, you have to switch those sections on in the Developers policy yourself. A few behaviours worth knowing Block beats Redact. If two sections fire on the same prompt, the stricter action wins. Some data types live in two sections (card numbers, SSNs, IBANs, e-mail and IP addresses appear in both Secrets and Sensitive Data), so both can fire on one value. The IT data types are noisy for developers. They redact every URL, UUID and base64 string. Redaction and signed API calls don't mix. More on this in a later part, but in short: when the agent redacted a key inside a request to AWS Bedrock, Bedrock rejected the request, because the request was signed before Prompt Security changed it. For Bedrock, Detect was the only mode that didn't break the tool. Building policies per team This is where the product gets genuinely useful, and why I wanted this part before the others. One policy for the whole company is either too loose for the finance team or too noisy for the developers. The idea is simple: create a policy per team, then put a rule for that team above the Default rule. Here's how I'd lay out a typical company. Only the first three existed in our tenant; the others are suggestions built from what the policy sections can do. Policy Who What it focuses on Rule mode Default Employees Everyone (Default rule) Secrets + core personal data, redact Protect Default Developers Developers' code assistants Secrets, plus your internal-ID regex; consider Sensitive Data Protect (Detect for Bedrock) Finance Finance team Cards, CVV, IBAN, bank details, SSN - block; topic "investment advice" Protect HR HR team Personal data, salaries (custom data type), guardrail for names + dates of birth Protect Healthcare / clinical Clinical or support staff PHI section, regex for record numbers, guardrail for patient details Protect Legal Legal team Guardrail for "client-confidential" matters, hidden terms for case names Protect Contractors External staff Only approved apps; everything else Block How to build one: Policies → Manage Policies → Add Policy (or Duplicate an existing Employees policy to start from its settings). Switch on the sections you need, set each action, and check the secret detector count - remember new policies start at 203 of 238. Add a Security Rule above the Default rule: the team's users or group, the apps, the platform, Protect, and your new policy. Open Advanced on that rule and decide on paste, upload, logging and pop-ups. Test with fake data, and check the event in the activity log to confirm which rule and policy actually matched. The limits you'll run into: Code assistants get one policy for everybody. Because code-assistant rules only accept the Default Developers Policy, you can't give the finance team's developers a stricter coding policy than everyone else's. The only thing that can differ per team is the mode (Detect, Protect, Block, Don't Track). Groups need an identity provider. Rules can target Entra ID, Okta or Google groups, but only once that's connected. Ours wasn't, so the only option was listing people by e-mail. The sensor has to know the user. A rule for the finance team only works if the extension reports the person's work e-mail (pushed by MDM) and the agent was installed with their e-mail. Otherwise their prompts quietly fall to the Default rule. Sensor Settings This tab controls the two pieces on the laptop. It has two sub-tabs. Extension settings Files and Content (Legacy) - the old switches for blocking uploads, downloads and pasting. Since extension 7.1.46 these live in each Security Rule's Advanced settings instead. Log responses in top AI apps - stores the AI's replies for around a dozen big apps (ChatGPT, Copilot, Gemini, Claude, Perplexity and others). ChatGPT-specific settings - block chat training, memory, developer mode, workspace agents, connectors, voice mode, chat sharing and custom GPTs; force temporary chats; restrict ChatGPT Teams invites. A colleague tested these on a personal ChatGPT account and only "Block chat training" worked - it switched training back off within seconds. The rest may only apply to enterprise accounts. Fail Open / Fail Close - what the extension should do when it can't reach Prompt Security. In testing, prompts went through unchecked and unlogged with either setting. Employee bypass inspection - lets employees skip inspection per app. We had it disabled. Bypass inspection for non-work browser profiles and Exclude URLs - both create blind spots if you fill them in. Autocomplete handling - "Silent protection" blocks without logging. User attribution via MDM variables - lets your MDM tell the extension who the user is. You want this on. Browser extension on/off - one switch that turns the extension off across the whole tenant. Agent settings Agent settings: notification frequency, header validation, request timeout, custom inspection URLs and Agent Hardening (macOS only) Violations notification frequency - log every violation, or summarise per hour, day, week or two weeks. Add header validation to harden the agent - off by default. Request timeout - 5 or 10 seconds. Custom inspection URLs - add your own address for an app the agent already supports (for example a self-hosted GitLab or an internal AI gateway). It can't add a brand-new app; missing apps have to be requested from support. Agent Hardening Mode - a password needed to disable or uninstall the agent. It's marked macOS only. On Windows there was no tamper setting at all, which matches what I saw when an admin stopped the agent (a later part covers that). Notice what's missing: there's no fail-open or fail-close setting for the agent, and there is no list of devices anywhere - no view of which laptops have the agent, which version, or whether it's running. User Education This is the bit your users actually see, so it's worth five minutes of your time. User Education: the first-visit warning page, blocked domain message, blocked AI component message and blocked prompt message Domain warning page - shown the first time someone opens a monitored AI tool. Ours said "Use AI Tools with Care" with a short reminder not to submit confidential data. It shows again after the browser is closed and reopened. Blocked domain message - when the extension blocks a disallowed AI site. Blocked AI component message - when an AI feature inside an otherwise allowed site is blocked. Message for blocked prompts - the text the AI app returns instead of an answer. Default: "Blocked due to policy violations". Violations awareness pop-up - the main switch for pop-ups across all Detect and Protect rules. Branding and policy link - your logo and a link to your AI policy, added to every message. Three things to check: Change the AI policy link. Ours pointed at a vendor blog post, which is what every block message would have linked users to. These settings are one per account. You can't have different wording for different teams, or for different clients if you're a provider. Pop-ups are a browser thing. With the pop-up switch on, I never saw one in desktop apps or command-line tools - they have nowhere to show it. Those users only see the block message, if the app shows it at all. How I'd set it up If I were starting a tenant from scratch tomorrow: Start in Detect for everyone and watch the activity log for a week or two. You'll learn which apps people really use. Open the Default rule's Advanced settings and decide paste, upload and logging on purpose, not by default. Make the Default rule a strict baseline, because personal accounts and anyone you forgot end up there. Switch on more than Secrets in the Developers policy if you care about personal data in code assistants. Add team policies (Finance, HR, and so on) with their own rules above the Default rule, and check new policies' detector counts. Use a Natural Language Guardrail for the things fixed detectors miss. Get user attribution right - MDM for the extension, user e-mail for the agent, and an identity provider if you want group rules. Write your own user education text and policy link before you switch anything to Protect. If you take one thing from this part: the rule picks the mode and the policy, and the mode wins. A perfectly tuned policy behind a Detect rule protects nothing. - written from hands-on testing, By Iqra Shafi, Dean Iqra : https://www.linkedin.com/in/iqrabintishafi/ Dean: https://www.linkedin.com/in/akash-patel-097610202/ ------------------------------------------Cyberengage---------------------------------------------------
- SentinelOne Prompt Security: What It Is, and How I Actually Tested It
Every company I talk to has the same quiet worry right now: people are pasting things into ChatGPT, Claude and Copilot that should never leave the building. API keys. Customer records. Whole .env files. The AI is useful, so nobody wants to ban it, but nobody can see what is going into it either. That gap is exactly what a product like Prompt Security is supposed to close, and when SentinelOne picked it up it landed on my desk to evaluate properly. So I did what I usually do: I stopped reading the datasheet and spent a few weeks actually running it. Real tenant, real browsers, real coding tools, fake secrets. This series is the write-up of what I found - what it catches, where it is strong, and the places it quietly lets things through. This first part is the setup: what the product is, what I had access to, and the rules I tested it under. If you want the juicy bypasses, they are coming, but you will understand them a lot better once you know how the thing is built. What Prompt Security actually is Strip away the marketing and it is a DLP layer that sits between your people and the AI they use. It reads the prompt on its way out, decides whether it contains something sensitive - a secret, a card number, personal data - and then does something about it: lets it go, redacts the sensitive bit, swaps in dummy data, or blocks the whole thing. The clever part is where it does the reading, because "AI" is not one place. People use it in a browser tab, in a desktop app, and increasingly from the command line. Prompt Security covers those with two separate pieces. The browser extension This is the half that watches AI in the browser - chatgpt.com, claude.ai, Gemini, Copilot, Perplexity, and also the AI baked into ordinary sites like Notion, Gmail's "Help me write" and the Edge Copilot sidebar. It is the only piece that can read text inside an uploaded screenshot or a Word file, and it is the piece that shows the user a pop-up when something is blocked. The endpoint agent This is the half that covers everything that is not a browser tab: the desktop apps (the Claude and ChatGPT installers, Copilot in VS Code) and the coding assistants that talk to a model API directly, including Claude Code and OpenCode pointed at Bedrock. Under the hood it installs a local proxy and its own certificate so it can see inside the encrypted traffic on its way to the model. That is a detail that matters later, so file it away. What was unlocked, and what wasn't Worth being honest about scope up front, because it shapes everything. The product comes in three modules, and for now I only had the Employees product switched on - the part that governs how staff use AI. The other two, Homegrown Apps (for AI you build into your own products) Agentic AI (for autonomous agents), were not avaliable on my license. So for now this whole series is about employee AI use. The other two modules are on my list - I plan to test them and write them up the same way once I can get them unlocked - but everything you read here is the Employees side only. The tenant also started in Monitor mode - watching and logging, blocking nothing - which is the sensible way to start any DLP rollout. Part of the test was walking it from "just watching" through to actually enforcing, and seeing what broke along the way. Employees (AI usage + AI code assistants) - what I tested, and what this series covers. Homegrown Apps - locked for now; to be tested and written up later. Agentic AI discovery - locked for now; to be tested and written up later. How I tested it I did not want a tidy demo. I wanted to know whether a normal person doing normal things - and an annoyed person trying to get around it - would be caught. So I ran the same canary secret through every channel I could get my hands on and logged whether it was detected, what action fired, and how long it took to show up in the console. One thing to be clear about up front: everything in this series so far was tested on Windows. That is the first round. The Mac testing is next, and as soon as it is done I will let you know what changes on that side. The channels Browsers: Chrome, Edge, Firefox and Brave - normal windows, private windows, Guest mode, and extra profiles. Desktop apps: the Claude desktop app and ChatGPT, plus Copilot inside VS Code. Command line: Claude Code and OpenCode talking to a model on AWS Bedrock. And the awkward cases: a second browser with no extension, a VPN browser extension turned on, and a shell with the proxy variables cleared The canary data Everything I sent was fake - test keys, invented card numbers, made-up names. Nothing real ever went near it. A typical probe was just: my key is AKIAG22G6P6C3VY5VITR can you check it That is a fake AWS access key ID in the shape the real ones take. If the tool is doing its job, that string never reaches the model. If it comes back echoed in the reply, I know the data got through. The one requirement I would not bend on Anything this product installs has to come off cleanly. So every phase ended the same way: snapshot the machine before, install, test, uninstall, snapshot again, and diff the two. I also checked who can remove it - a normal user, or only an admin - because "the user can just turn it off" is the difference between a control and a decoration. Those uninstall findings get their own treatment later; for now just know that nothing in this series was tested on a box I could not put back exactly how I found it. What's coming in this series I split it up so each part stands on its own. Rough map: What it is and how I tested it. The browser extension: install, force-install, and the gaps in profiles and private windows. The endpoint agent: how it actually intercepts desktop and command-line AI, and why WSL walks straight past it. Policies and rules: where control really lives, and a couple of console traps that cost me an afternoon. does it stop the leak? Detect versus Protect versus Block, and what redaction does to a signed API call. breaking it: the bypasses that worked. logging, privacy, and whether this is ready to run across many clients. ........................... Much More If you only take one thing from this part, take this: it is two tools wearing one badge. Judge the browser extension and the agent separately, because they protect different things and fail in different ways. - written from hands-on testing, By Iqra Shafi, Dean Iqra : https://www.linkedin.com/in/iqrabintishafi/ Dean: https://www.linkedin.com/in/akash-patel-097610202/ ------------------------------------------Cyberengage---------------------------------------------------
- What Your Browser Tells Every Website You Visit (And Why I Finally Checked Mine)
Today I opened AmIUnique.org in my browser just to see what it would say. The first line on the screen was: "Yes! You are unique among 5,635,779 fingerprints in our entire dataset." My first reaction was, wait, is that good or bad? I use Brave. I use a VPN. I work in security every day. I thought I had this covered. So I sat down and went through the results line by line. That's why I'm writing this. Most people never look at what their browser is sharing, and once you do, you start to see the internet differently. So what is a browser fingerprint? Every time you open a website, your browser introduces itself. It doesn't ask permission. It just tells the site things like: Your operating system (Windows, Mac, Linux) Your browser and its exact version Your language and time zone Your screen size The fonts installed on your machine How many CPU cores and how much memory you have What your graphics card draws when asked to render a hidden image (this is called canvas and WebGL fingerprinting) On its own, each item is boring. Millions of people run Windows. Millions use 1920x1080 screens. But put 50 of these small details together and you get a combination that is often unique to you. That combination is your fingerprint. The scary part is that it doesn't need cookies. You can clear your cookies, open a private window, and a site can still recognize you from your fingerprint. What I found when I tested my own browser Here is roughly what AmIUnique showed me: OS: Windows, which 44% of their users share Browser: Chrome-based (Brave), shared by 48% Language: English, shared by 66% Time zone: UTC+1, shared by 15% Canvas fingerprint: 0.00%. Nobody else matched. Plugins: names like "ufu2EhvX" and "LNGqVpz". That one made me laugh. CPU cores: 29. I don't have 29 cores. That's when it clicked. Those weird values weren't my real machine. Brave was lying for me on purpose. It changes the values each time a site reads them, so I look unique on every visit, but differently each time. A tracker can't link Monday's visit to Tuesday's because the fingerprint keeps changing. The site also showed that no similar fingerprint had been seen in the last 90 days, not even from my own earlier visits. That's exactly what you want. So being "unique" isn't always bad. Unique and stable is bad, because that's trackable. Unique and changing every time is protection. What still gave me away Even with Brave doing its job, a few things still leaked: IP address. This is the big one. Every site sees it. In my case it was a VPN address, not my home connection, so that one was covered. Time zone. My browser said UTC+1. If my VPN says I'm in the US but my clock says Central Europe, any site that compares the two can see it. Language vs. location. An en-US language with a UTC+1 time zone is a slightly unusual pairing, and unusual means easier to pick out. Fonts. The standard Windows font set shows up clearly. Do Not Track. Ironically, turning it on adds one more small detail about you. None of these are a disaster on their own, but they add up. Why this matters, especially if you do investigations For a normal user, this is about privacy and ad tracking. For anyone doing OSINT, threat research or incident response, that's a different level. Think about it. You're investigating a phishing site, a scam shop or a threat actor's forum. You open their page in your normal work browser. What did you just tell them? Your real time zone, which hints at your country Your company's IP address, unless you're on a VPN Your language settings A fingerprint they can match the next time you visit If the person on the other side is paying attention, and good ones do, you might have just told them someone is looking at them and roughly where that someone sits. Some actors block or redirect visitors who look like researchers. Some serve different content. Some just quietly take notes. And this goes both ways. Sometimes you don't want to look hidden. A browser that is too locked down also stands out. If every visitor to a forum is on default Chrome on Windows and you arrive on hardened Firefox from a Linux VM with no fonts and a strange time zone, you're the odd one in the room. So the goal isn't always "maximum privacy." The goal is to choose what you look like based on what you're trying to do: Blend in when you want to look like a normal visitor Lock down when you don't want to leave a trail Stand out on purpose in rare cases where you want to be noticed You can only make that choice if you know what your browser is sending in the first place. How to check yours It takes five minutes. Open these in the browser you actually use: AmIUnique.org. Shows your full fingerprint and how rare each piece is. CoverYourTracks.eff.org. From the EFF. Tells you whether your fingerprint is unique, and whether it's randomized (good) or stable (bad). BrowserLeaks.com. Goes deep on WebRTC, canvas, fonts and more. ipleak.net. Checks your IP, DNS and WebRTC leaks, which is useful if you use a VPN. Do it once on your normal browser, then once on a fresh browser or VM, and compare. The difference will surprise you. What I changed (and what you can do) You don't need to become paranoid. A few sensible steps go a long way: Use a browser that fights fingerprinting. Brave randomizes by default. Firefox has "resist fingerprinting" options. The Tor Browser makes everyone look the same. Use a VPN, and check it actually works. Test for DNS and WebRTC leaks. A VPN that leaks your real IP through WebRTC is just giving you false confidence. Match your time zone to your VPN location when it matters for an investigation. Keep investigation work separate. Use a dedicated VM or browser profile for research, never your personal or work browser. Use user-agent switchers carefully. They help you blend in, but a Windows user agent sitting on top of Linux fonts doesn't fool anyone who looks closely. Re-test after changes. Settings get reset by updates. Check again every now and then. Why I'm writing this Honestly, because I almost didn't check. I assumed my setup was fine because I knew the tools. When I finally looked, most of it was fine, but some of it wasn't, and I only knew because I tested. That's the whole point. Your browser talks to every site you visit, all day, every day. You don't have to stop that. But you should know what it's saying, and you should be the one deciding what it says. Go run the test. Then come back and tell me what surprised you. ----------------------------------------------------Dean------------------------------------------------
- RunReveal Agents: What I Found Without Pressing Run
What Agents Are Supposed to Do Most of a SIEM is reactive. A detection fires, someone gets paged, someone goes and looks. RunReveal's Agents feature tries to move part of that work onto a schedule. You write a prompt, pick a model, set a cron expression, and an LLM goes off, queries your logs with the same tools you'd use, and sends you what it found. There are six starting templates in the console: Weekly Threat Hunt: a recurring hunt that ends in a written report. It doesn't create detections. Detection Coverage Gap Analysis: compares your sources against your detections and suggests what's missing. Detection Tuning: looks for noisy detections and proposes changes. Data Volume Anomaly Detection: watches source volumes for spikes and drops. A source going quiet is often the first sign something broke, or that someone turned logging off. Data Saving Analysis: points at high-volume, low-value logs you could filter or drop. Open Investigation Analysis: summarises open cases and flags the ones nobody has touched. Why I Didn't Run One Every other part of this series is built on things I actually ran. This one has a gap in the middle, and I'd rather say so up front than dress it up. An agent needs an AI provider connected to the workspace: Anthropic, OpenAI, Google AI, Bedrock or Vertex. RunReveal doesn't ship its own model. You bring your own key, or for Bedrock, an IAM role that RunReveal assumes in your AWS account. The workspace I've been testing in is a shared lab, and connecting my personal API key to it would mean my own billing account paying to process company data. I didn't want that, and setting up a proper Bedrock role is somebody else's call. So no provider is connected, and no agent has ever completed a run here. That turned out to be less of a dead end than I expected. You can learn a lot about an AI feature from everything around the model: what the API insists on, what the defaults are, what instructions the agent gets before it starts, and what gets written to the audit log. That's what the rest of this article cover What the Create API Actually Insists On Back in September I created a test agent through the API to see the real request and response shapes. Two of my first attempts failed, and the errors tell you more than the docs do: POST /agents/create (no provider) -> "AI provider is required. Please select a provider (e.g., anthropic, openai, googleai, bedrock, vertex)" POST /agents/create (provider, no model) -> "AI model is required. Please select a model for the chosen provider" So provider and model are both mandatory, and the provider list is a fixed set of five. What isn't checked is whether the model exists. I passed a model name I made up, not-a-real-model-xyz, and the agent was created without complaint. You'd only find out at the first scheduled run, which could be days later. The full set of fields create accepts is wider than the console shows: name, displayName, description, prompt, provider, model, schedule (cron), notificationNames, tools, parameters, riskScore, notes, plus two switches worth knowing about: disableRunRevealSystemPrompt and skipPermissions. I didn't test either. The names alone tell you there's a RunReveal system prompt sitting in front of yours, and a permission check that can be turned off. The Defaults Point at Email, Twice This is the part I'd want anyone to read before building an agent through the API. What the API did: I sent "enabled": false and no notification channel. The agent came back with enabled: true, notificationNames: ["email"], and a next run scheduled about two days out. The false I sent was silently ignored, and the email channel was filled in for me. I caught it because I read the object that came back instead of assuming the request did what I asked. Fixing it took a second call, and that call is a full-replace PUT: my first attempt sent only the fields I wanted to change, and failed: PUT /agents/update (just enabled + notificationNames) -> Key: 'UpdateAgentRequest.DisplayName' Error:Field validation for 'DisplayName' failed on the 'required' tag Key: 'UpdateAgentRequest.Prompt' Error:Field validation for 'Prompt' failed on the 'required' tag You have to fetch the agent, change the two fields, and send the whole thing back. One more inconsistency while you're there: update wants enabled as a real JSON boolean, while the detections enable endpoint wants it as a string. Same word, opposite types, same API. The second time email shows up is new since I first wrote about this. The workspace now ships built-in agent skills (more on those below), and one of them is called notifications. This is the instruction it gives the agent, word for word: "If you are working in a background context and cannot confirm, look for the channel named email. Most workspaces should have a channel named email, if not just use the oldest channel with the type email." A scheduled agent is a background context by definition. So if you leave the channel empty, the API fills in email, and if the agent decides to send something on its own, its own instructions also send it to email. In most workspaces that channel goes to real people. In ours it goes to a colleague's inbox. That's the reason I neutralised the test agent within a minute of creating it, and then deleted it. My rule for anyone trying this: create a dedicated test channel first (a Slack channel only you are in, or a webhook you control), set it explicitly on the agent, and read the object back after every create and update. Don't rely on enabled: false to keep anything quiet. One thing that did work properly: DELETE /agents/delete?id=… really deletes the agent. My test agent was gone from /agents/list straight afterwards. That stood out, because the delete endpoints for detections and pipeline steps I'd hit earlier in this series don't actually remove anything. Built-in Skills: The Instructions Behind the Agent Skills are markdown playbooks an agent loads when it needs them. When I checked on 28 September, the skills list in this workspace was empty. Checking again today, there are three, and none of them were written by anyone on our side: GET /agents/skills/list data-analysis sources -> tables -> schema -> query workflow, ClickHouse syntax, dashboards detection-authoring Sigma vs SQL, lookup order, pre-validation, naming, categories, MITRE notifications choosing a channel, Handlebars templates, NotificationSend These are worth reading even if you never run an agent, because they're RunReveal telling its own AI how to use the platform properly. A lot of it is good advice for humans too. Some of the specifics I hadn't seen written down anywhere else: Default to Sigma for single-event matching. Use SQL only for aggregation, correlation, time-window counting or joins. Sigma runs in real time per event, SQL runs on a schedule. Mandatory pre-validation before creating a detection: read the schema, sample 5 rows from the last 7 days, get the event-name distribution, then test the WHERE clause with LIMIT 5 and check it runs in under 1,500 ms. Performance targets: a detection on a 15-minute schedule should run in under 1,000 ms, an hourly one under 2,000 ms. Use count(), not count(*). Naming: displayName is "{Service} {Description In Title Case}" ("Okta Push Fatigue"), the slug is lowercase with hyphens, 3 to 128 characters, no underscores. Categories go in a fixed order: service, then an OCSF category, then signal or non-signal, then optional tags. Example: [okta, identity-access-management, signal, privilege-escalation]. MITRE fields are validated by regex. TA#### for tactics, T#### or T####.### for techniques. t1078, T1136.1 and TA001 are all rejected. "Zero results != broken detection." That line made me laugh, given that Part 6 covered a detection that returned zero results because its filter could never match. Both things are true; you have to check which one you're looking at. The data-analysis skill also has a sensible habit I'd copy: when no timeframe is given, default to the last 24 hours but tell the user you assumed it. And it warns the agent to check how big rawLog is before querying it, because full-table scans time out with a 5xx. Built-in Resources, and What Sigmalite Changes Alongside skills there are three reference documents agents can load: a detection engineering guide, a list of console URL routes (so the agent can link you to the right page), and a reference for sigmalite, the Sigma dialect RunReveal actually runs. The sigmalite one is the most useful, because it lists where RunReveal differs from upstream Sigma: Added: a cidr modifier, for matching IP fields against ranges. Upstream Sigma has no native equivalent. Not supported: gt, gte, lt, lte, utf16le, utf16be. The rule fails to parse if you use them. Numeric comparisons belong in a SQL detection. Extra fields: sourcenames and sourcetypes to scope a rule, score for signal chaining, mitre, disabled, and notes. detection: local: DestinationIp|cidr: - "10.0.0.0/8" - "172.16.0.0/12" - "192.168.0.0/16" condition: not local If you're porting Sigma rules from a public repo, the unsupported modifiers are the thing to grep for first. What the Audit Log Shows RunReveal writes its own audit trail into the same logs table as everything else, so I could query my September test back out of it. Every agent API call is there, failures included, with the error message attached: SELECT receivedAt, eventName, rawLog FROM logs WHERE receivedAt >= now() - INTERVAL 60 DAY AND sourceType ILIKE '%runreveal%' AND eventName IN ('agents_create','agents_update','agents_delete') ORDER BY receivedAt A failed create looks like this (workspace and IP details removed): { "event_name": "agents_create", "actor_email": "Testing", "readonly": false, "supplemental_data": { "error": "true", "error_message": "AI model is required. Please select a model for the chosen provider", "user_agent": "curl/8.5.0" } } Three things in there are worth knowing if you ever investigate agent activity: actor_email isn't an email. For API calls it's the name of the API token, here "Testing". If your tokens have vague names, your audit trail will too. Name tokens after the person or the automation that owns them. There's no request body. The log tells you an agent was created and that it succeeded. It doesn't tell you it was created with enabled: true and an email channel when the caller asked for neither. The silent default from the section above is invisible in the audit trail. Reads are logged too. agents_list, agent_runs_list, agent_tasks_list, chat_usage_summary and workspace_ai_models_info all show up, so you can see who has been looking at the AI features, not just who changed them. If you want a detection on this, agents_create and agents_update events where readonly is false and the error field is empty is a reasonable start. An agent is code that runs on a schedule with access to your logs and, by default, the ability to write Sigma rules. New ones deserve the same attention as a new scheduled task on a server. The Trust Boundary The tool permissions on a new agent are on by default. The list includes reading schemas, query history, sources and detections, and also creating and updating Sigma rules and sending notifications. Put that next to everything above: it can be created with enabled: true when you asked for false, it defaults to emailing a real channel, its own instructions tell it to fall back to email when it can't ask, it's allowed to write detection rules, and there's a skipPermissions switch in the API. None of that is a vulnerability. It's a product built to be useful out of the box, which is what most people want. But it means the safe configuration is something you build on purpose, not something you get by default What I would check before turning one on A dedicated test notification channel exists and is set explicitly on the agent. Tool permissions are trimmed to read-only for the first few runs. Turn rule creation on only once you trust the output. The model name is real, so the first run doesn't fail silently days from now. The object you get back from create and update matches what you sent. The API token used has a name that identifies its owner, so the audit trail means something. Someone has read the built-in skills, since they shape what the agent will do. Where This Leaves Agents I can't tell you how good a RunReveal agent's threat hunt report is, because I haven't seen one. What I can tell you is that the scaffolding is more thought through than I expected. The skills carry real detection-engineering rules, the sigmalite reference is honest about what doesn't work, and every call lands in an audit log you can query with SQL. The rough edges are in the defaults, and they all lean the same way: towards running, and towards email. If a proper provider gets connected to this workspace later, I'll run the Weekly Threat Hunt template against it and write up what comes back, good or bad. ----------------------------------------------------Dean------------------------------------------------
- RunReveal Log Processing: Transform, Filter, Drop, Mask, Sample
Seven Tools for Shaping Data In Flight Earlier introduced pipelines as an ordered chain of steps, using one real example that happened to include transform, enrich, filter, detect, and destination steps. What I didn't appreciate until I read through RunReveal's log-processing documentation properly is how much depth sits behind each of those step types individually. This article covers the ones worth understanding on their own: Transforms, Filtering, Dropping, Masking, Sampling, Enrichments, and Set TTL. Enrichments — Tagging Events Without Touching Storage Rules An enrichment matches incoming events against a pattern and attaches supplemental key-value data to them. Rules are comma-delimited rows following match_type, pattern,data, matched against sources you select. The real value isn't the tagging itself — it's what you do with the tag afterward. The documented patterns for this are genuinely practical, not abstract examples: Exclude known-good activity from detections — tag events from service accounts or automation systems, then have your detection query check for that tag and skip it, rather than hand-listing every service account inside every detection's WHERE clause. Tag trusted IP ranges — flag corporate VPN or known cloud infrastructure ranges via CIDR match, so network-based detections can exclude them centrally instead of repeating the same exclusion list everywhere. Tag events for dropping — rather than hard-coding drop patterns into a Drop step directly, tag candidates with an enrichment first. That keeps drop rules centralized, auditable, and visible before anything actually disappears. Classify by risk level — tag actors or resources by sensitivity (a production database ARN, an admin IAM role pattern) using regex rules, feeding tiered alerting downstream. Something worth remembering about how rules get edited: Clicking a rule in the rules table does not open an inline editor. To change one, you delete the existing rule and add a new one with the updated values. If you add a rule with the same pattern as an existing one instead of deleting first, the UI adds a duplicate entry rather than replacing it. Enrichments are also fully API-manageable — create, update, append rules, get, list, and delete all have real endpoints, which means this is scriptable rather than console-only if you're managing rule sets at any real scale. Filtering vs. Dropping vs. Sampling — Three Ways to Reduce Volume, and When Each Applies These three get confused with each other constantly, and RunReveal's documentation actually spells out the distinction cleanly enough that I'll just relay it directly, because it's the most useful framing I've seen for this problem on any SIEM. Filtering — reusable, regex on the raw log A Filter matches a regex pattern against the entire raw log JSON string, not a specific field — so it can catch content anywhere in the event. Filters are managed centrally and can apply globally across all sources or be scoped to specific ones. Dropped events never reach ClickHouse and never show up in Explore. The tradeoff: because it's a shared, reusable rule set, it's the right tool when you want the same drop logic — health-check traffic, known bot user agents — applied consistently across multiple pipelines. Dropping — pipeline-specific, precondition-based A Drop step lives inside one specific pipeline and uses preconditions against normalized fields (eventName regex, an IP CIDR match, a missing-field check) rather than a raw-log regex. Multiple preconditions on the same step AND together. This is the right tool for a one-off rule scoped to a single pipeline, versus something you want shared everywhere. Sampling — statistical, not absolute A Sample step keeps a configured percentage of matching events (1-99%) and discards the rest, while non-matching events pass through untouched. The real-world examples given are telling: sample S3 data events at 10%, but keep VPC Flow Logs' rejected connections and CloudFlare's error responses at full fidelity even while sampling the successful/accepted traffic. The decision in one line: Filter for shared rules across pipelines. Drop for a one-off, pipeline-specific rule. Sample when you want to keep statistical visibility into high-volume noise rather than delete it outright. Masking — Protecting Fields Without Losing the Event Masking mutates a field's value in place — every step and destination after it in the pipeline sees the masked value, not the original. Three modes: Redact — replace the entire value with a static token. Hash — replace with a deterministic, non-reversible pseudonym. Equal input values produce equal hashes within a workspace, so a masked field stays joinable and countable in detections even though the real value is gone. Partial — replace only the regex-matched portion, leaving the rest visible. The documented example: strip the local part of an email address but keep the domain, so you can still group and filter by domain without storing who exactly it was. One detail worth knowing: a Mask rule targeting a field that doesn't exist on a given event is silently skipped rather than erroring, which means it's safe to write one mask config that references fields only present on some of your sources. Mask vs. Drop, in one sentence: mask when you still need the event for analysis but must protect or pseudonymize a specific field; drop when the entire event has zero value and should never be stored at all. Transforms — Reshaping a Value Before It Lands A transform rule is a chain of processors run in order against the raw log, always ending in an Output to Field step that writes the final result to a normalized field. Available processors include GJSON-style path extraction, regex find/replace, string prepend/append, trim, substring by index (negative indices allowed, counting from the end), split-by-delimiter, and date format conversion. Rules are independent of each other, and a processor's output becomes the next processor's input — so "Extract JSON Field" partway through a chain will fail if the prior processor handed it a plain string instead of JSON. That's listed as one of the documented common mistakes, alongside forgetting the mandatory Output to Field step, which just means the rule silently doesn't reach a normalized field at all rather than erroring loudly. Set TTL — Retention, Per Event A Set TTL step lets you override retention on a per-event basis. If you never touch it, ingestion applies the platform default of 550 days automatically — this is the same number referenced back in Part 2's retention discussion, now with the mechanism that actually controls it at the event level rather than just the workspace default. Hands-On: What the API Doesn't Document Everything above is real, but it's still secondhand — pulled from RunReveal's documentation rather than from actually operating the platform. Since I have live API access to a workspace, it seemed worth actually exercising two of these tools directly rather than just relaying what the docs say about them, and writing down what happened, including the parts that didn't work the way the docs implied. A transform that's already running, verified against real ingested data Rather than build a synthetic example, I went looking for a transform rule already active in the workspace and checked its actual output against actual ingested events. One was already running on a lab source: a processor chain that extracts an "environment" field, uppercases it, and writes the result to a normalized tag field. A real raw event ingested through that source carries this in its JSON payload: {"environment":"dev","application":"", ...} And the same event's tags, queried directly from ClickHouse: tags = {"RR_Pipeline":"Default Pipeline","RR_Topic":"Default Topic", "environment_CAPITAL":"DEV"} That's the whole point of a transform in one real pair of before/after values: "dev" goes in, "DEV" comes out, on an event nobody hand-crafted for this article. Building an enrichment rule from scratch — and hitting five undocumented behaviors The docs describe enrichment rules conceptually ("match_type,pattern,data" rows) but the actual API schema for creating one isn't published anywhere I could find. So I built one by trial and error against the live create endpoint, reading the validation errors back from the server to figure out the real field names. Five things came out of that exercise that aren't in the documentation: The source list field is called sources, and it takes source names, not source IDs — passing a valid source ID here fails with "invalid source name," even though every other reference to sources in this API is ID-based. The match-type field is called type, not matchType or match_type (both of those are silently accepted and silently ignored — you get back the actual error "invalid match type ''" with no hint that the key name itself was wrong). Valid values are exact, regex, or cidr — the docs' own contains-style examples describe the behavior, not the literal parameter name. A newly created rule comes back with isActive: false by default. It exists, it's saved, and it does nothing until you explicitly flip it on — there's no warning in the create response that the rule is inert. Updating a rule doesn't accept a partial patch. Sending just {id, isActive: true} returns a bare "unknown error" with no explanation; the update endpoint wants the full object — name, sources, and rules — resent every time, or it fails silently on the missing fields. The update also regenerates the rule's internal ID rather than editing it in place. That's a nice confirmation, at the API level, of the console-side detail mentioned earlier in this article — there really is no in-place edit happening anywhere in this system, UI or API; every "edit" is a delete-and-recreate under the hood. The one that actually matters operationally: Activating an enrichment rule does not retroactively tag events that were already ingested before activation, even when their content matches the rule's pattern. I confirmed this directly: queried a set of already-ingested events matching the rule's regex, activated the rule, then re-ran the identical query — the tag was absent both times. Enrichment only applies going forward, at ingest time. If you're rolling out a new tagging rule, don't expect it to backfill your existing data; budget for a re-ingestion or accept the gap. One more thing worth knowing if you're scripting any of this: the delete endpoint for enrichments returns a 405 on a plain POST and needs the literal DELETE HTTP method to be accepted at all — and even then, it only clears the rule's contents rather than removing the enrichment record itself, which then lingers, empty and inert, in the list output. That matches a pattern already seen elsewhere in this API: detections and pipelines have the same non-functional delete behavior. Plan for disable-in-place as the real cleanup mechanism here, not delete. ------------------------------------------------------------Dean--------------------------------------
- RunReveal Hands-On: Building a Pipeline Drop Rule Through the API
What I Was Trying to Do Every SIEM bill I've ever looked at has the same problem: a big chunk of what you pay to store is noise nobody will ever query. RunReveal's answer is the pipeline. Events are matched, transformed, enriched and, if you want, thrown away before they ever reach storage. One of the part in this series covered this from the console side. For the last part of the series I wanted to do it properly, through the API, and prove it works with real events instead of trusting a green tick in the UI. The plan was small on purpose: Build a pipeline with one drop rule: throw away any event whose marker field equals article-drop-me. Route only webhook traffic into that pipeline, so nothing else in the workspace is touched. Send two events through a test webhook: one marked article-drop-me, one marked article-keep-me. Query the logs table. If only the keep-me event is there, the drop rule works. That isn't what happened. I'm writing it up anyway, because what I hit on the way is more useful than a clean result would have been. There are four API behaviours here I couldn't find in the docs, and I'd want to know about every one of them before automating anything against this API. How Routing Actually Works The docs describe this in prose, but the clearest picture comes from reading a working pipeline back out of the API. The model is: Source -> Topic (precondition + rank) -> Pipeline (ordered steps) -> destination step -> ClickHouse Topics are checked top to bottom by rank. The first topic whose precondition matches wins. Each topic points at one pipeline. Anything no topic catches goes to the default pipeline. A pipeline is an ordered list of steps: transform, enrich, filter, detect, drop, destination. The destination step is what actually writes the event to storage. A pipeline without one stores nothing. Our workspace has one catch-all topic (precondition type all) wired to a pipeline that's been running since the workspace was set up. Its steps, as the API returns them: transform precondition: all settings: {"transform_id": "*"} enrich precondition: all settings: {"enrichment_id": "*"} filter precondition: normalized.src.asNumber not_equal … settings: {"filter_id": "*"} detect precondition: normalized.src.asNumber is_empty destination precondition: all settings: {"destination_id": "default-clickhouse"} Keep that transform step in mind. My test pipeline didn't have one, and it's one of the two things I still suspect. Building the Steps: Two Broken Endpoints, One That Works The API offers three ways to put steps into a pipeline. I tried all three, with payloads copied from the working pipeline above so the shape couldn't be the problem. POST /pipelines/add-step returned HTTP 500 "unknown error". I tried settings as a JSON object and as base64. Same result both times, again on 13 October. POST /pipelines/execute-step returned 502 Bad Gateway, even against the working pipeline. That's a gateway error, not a validation error. POST /pipelines/save-steps worked, first time. It replaces the whole step list in one call. This is the save-steps call that built my test pipeline: POST /pipelines/save-steps { "id": "", "steps": [ {"type": "drop", "rank": 1, "settings": {}, "precondition": [{"field": "marker", "value": "article-drop-me", "type": "equal"}]}, {"type": "destination", "rank": 2, "settings": {"destination_id": "default-clickhouse"}, "precondition": [{"field": "", "value": "", "type": "all"}]} ] } Gotcha 1: If you're scripting pipelines, skip add-step and go straight to save-steps. Read the current steps, change the list, and write the whole list back. That's also how the console behaves, as far as I can tell. Wiring It Up Without Touching Production Traffic The pipeline does nothing until a topic points at it. I created a topic called webhook-drop-test with a precondition of sourceType equal webhook, then attached the pipeline and moved the topic above the catch-all: POST /topics/set-pipeline {"id": "", "pipeline_id": ""} POST /topics/move {"id": "", "rank": 1} Both calls returned success. The important check came straight afterwards: our Google Workspace events kept arriving at their normal rate through the catch-all. So the topic precondition did exactly what it should and only caught webhook traffic. The only webhook source in the workspace is an old test source from an internal tool, idle since early July, so nothing real was at risk. Then the two test events, sent to that source's webhook URL with its bearer token: {"marker": "article-drop-me", "run": "20261014T191844Z", "message": "pipeline drop test"} -> HTTP 200 {"marker": "article-keep-me", "run": "20261014T191844Z", "message": "pipeline drop test"} -> HTTP 200 The Result: Nothing Arrived I searched the logs table for the run ID, across every source type, for several minutes. Neither event was there. Not the dropped one, which was expected, and not the kept one either, which wasn't. SELECT receivedAt, sourceType, sourceID, rawLog FROM logs WHERE sourceType != 'runreveal-audit' AND rawLog LIKE '%pipeline drop test%' -> 0 rows A small trap while I was doing this: my first search matched rows, and they were all my own searches. Every query you run is logged to the runreveal-audit source, with the full SQL text in it. Searching rawLog for a string will find your own query unless you exclude that source. My first theory was the missing transform step. The working pipeline starts with one, mine didn't, and a pipeline that skips transform might be handing the destination an event it can't write. To test that properly I needed a control: send the same kind of event down the normal path, with my topic out of the way, and confirm it lands. Getting that control set up turned up two more gotchas. Gotcha 2: topics/move Renumbers, It Does Not Reorder To get my topic out of the way I moved it to rank 2, the catch-all's slot, expecting the two to swap places. This is what came back: before: 1 webhook-drop-test 2 test-topic (catch-all) move webhook-drop-test -> rank 2 after: 2 webhook-drop-test 3 test-topic (catch-all) The catch-all got pushed down to make room. My topic was still checked first, so my control event went through the test pipeline anyway and proved nothing. Rank is a sort value, not a slot. If you want a topic below another one, move it to a rank higher than that topic's current rank, and always read the list back afterwards. Gotcha 3: topics/update Is a Full Replace Plan B was to disable my topic instead of moving it. I sent only the field I wanted to change: PUT /topics/update {"id": "", "enabled": false} -> {"id": "…", "name": "", "enabled": false, "precondition": [{"field": "", "value": "", "type": ""}], "rank": 2} It disabled the topic, and it also wiped the name and the precondition, because every field I didn't send was overwritten with an empty value. On a disabled test topic that doesn't matter. On a live topic it could be serious: I don't know how the router treats a precondition with an empty type, and the worst case is that it matches everything. Always send the full object to topics/update, never just the field you're changing. Gotcha 4, and the reason I have not finished this test: With my topic disabled, the control event went through the normal catch-all path, the same one our Google Workspace data uses every minute. The webhook returned HTTP 200. The event never appeared. So the problem isn't my pipeline, or not only my pipeline. This webhook source is accepting events and storing none of them, and nothing in the API response tells you. The last event it ever stored is from 5 July. A 200 from a webhook only means the request was received. It doesn't mean the event was stored. If you're running anything that pushes into a RunReveal webhook, it's worth a scheduled query that alerts when a source goes quiet, because the webhook itself will never tell you. What I Can and Cannot Say Yet Confirmed, with the calls above: save-steps builds pipelines reliably. add-step (500) and execute-step (502) are still broken. A topic with a sourceType precondition isolates traffic cleanly. Other sources were unaffected. topics/move renumbers rather than reorders, and topics/update overwrites fields you don't send. This webhook source returns 200 and stores nothing, even on the default path. Not yet proven: Whether the drop rule itself works. Nothing reached the pipeline in a way I could measure. Whether a pipeline without a transform step loses events. That's still my best guess, but the broken source means I can't test it with this setup. Next is a fresh webhook source built only for this test, a control event to confirm it stores, and then the same two-event run again, with and without a transform step. I'll add the result here when I have it, whichever way it goes. Closing the Series That's eleven parts: what RunReveal is, how it's built, searching it, processing logs in flight, getting data in, detections, investigations, scheduled agents, notifications and the MCP server I built, and how it compares with Splunk and Sentinel. If there's one thing that runs through all of it, it's this. The core idea is good: your logs in ClickHouse, queried with plain SQL, with detections that are just scheduled queries. The parts around the edges are younger than the core. Most of what I hit in this series, from deletes that don't delete to webhooks that return 200 and store nothing, came from actually using the API rather than reading about it. Test it the same way before you trust it with anything important. ---------------------------------------------------Dean-------------------------------------------------
- RunReveal vs. Splunk vs. Microsoft Sentinel: One Real Detection, Three Languages
Two things also changed under my feet while I was writing. ClickHouse bought RunReveal (announced 1 September 2026), and Microsoft is in the middle of moving Sentinel out of the Azure portal and into the Defender portal. A comparison that ignores either of those would be out of date the day it went up. Where my knowledge comes from: RunReveal: hands-on, everything in this article was run against a real workspace this week. Splunk and Sentinel: their current official documentation and pricing pages, checked in October 2026. What Changed in 2026 RunReveal is now part of ClickHouse ClickHouse announced it had acquired RunReveal on 1 September 2026. That isn't as odd, RunReveal was always built on top of ClickHouse, with every log landing in one big columnar logs table. ClickHouse's own announcement says existing contracts and support stay the same, and that RunReveal remains available through its bring-your-own-database model, where your security data sits in a ClickHouse cluster you control The practical effect for anyone evaluating it today: the pricing page no longer lists the tiers I recorded back in September. It now says RunReveal has joined ClickHouse, that a joint offering is being worked on, and to contact sales. I'll come back to what that means for cost later on. Sentinel is moving house Microsoft originally planned to retire the Sentinel experience in the Azure portal on 1 July 2026. That date has since moved to 31 March 2027. After that, you manage Sentinel in the Microsoft Defender portal. New customers have been sent to the Defender portal by default since July 2025, and Microsoft's docs say Sentinel works there even without Defender XDR or an E5 licence. The bigger change is underneath. Sentinel now has two storage tiers: the analytics tier (the classic Log Analytics data that analytics rules, hunting and incidents run on) and a data lake tier in open Parquet format that keeps data for up to 12 years. Microsoft's docs say analytics-tier data is mirrored into the lake, so you keep one copy. You query the lake with KQL jobs or Jupyter notebooks, and promote data back into the analytics tier when you need detections to see it. Splunk is a Cisco product with three ways to pay Cisco completed its Splunk acquisition in March 2024, and the pricing story has spread out since then. Splunk's own pricing FAQ now lists three models for Enterprise Security: ingest-based, workload-based and activity-based. Splunk Cloud Platform offers all three; self-managed Splunk Enterprise offers ingest or workload. On the query side, Splunk Cloud's Federated Search for Amazon S3 lets you search S3 data without indexing it first. It uses SPL2, not classic SPL, and Splunk positions it for archival or low-value data rather than real-time detection. The Short Version Query language: RunReveal: ClickHouse SQL, plus Sigma rules Splunk: SPL (and SPL2 for federated search) Microsoft Sentinel: KQL Storage engine: RunReveal: ClickHouse, columnar, one wide logs table Splunk: Splunk indexers and buckets (SmartStore in the cloud) Microsoft Sentinel: Log Analytics (analytics tier) plus a Parquet data lake tier Detections: RunReveal: Scheduled SQL or Sigma, defined as text from the start Splunk: Correlation searches in Enterprise Security Microsoft Sentinel: Analytics rules (scheduled and near-real-time KQL) Allowlisting: RunReveal: SQL WHERE clause or an enrichment tag Splunk: Lookup tables Microsoft Sentinel: Watchlists How you pay: RunReveal: Was storage-based ("no ingest fees"); now "contact us" after the ClickHouse deal Splunk: Ingest, workload or activity-based Microsoft Sentinel: Analytics tier per GB (pay-as-you-go or commitment tiers) plus data lake charges Where it fits best: RunReveal: SQL-fluent teams, cloud and SaaS-heavy log sources Splunk: Large enterprises, deep app ecosystem, existing SPL skills Microsoft Sentinel: Microsoft 365 and Azure-heavy shops Architecture: Where Your Logs Actually Live RunReveal One table does most of the work. Every source lands in logs, with a handful of normalized columns (sourceType, eventName, actor, srcIP, eventTime, receivedAt) and the original event kept in rawLog. You query the normalized columns when they're enough and dig into rawLog with ClickHouse JSON functions when they aren't. Written article showed the one rule that matters most for speed: filter on receivedAt, because that's what the table is ordered by. Every query in this article does that, and none of them took longer than about 0.8 seconds over a week of data. Splunk Splunk parses events into indexes made of time-bucketed files, and extracts most fields at search time. That schema-on-read approach is why Splunk copes with almost any messy log you throw at it, and why the add-on (TA) for each source matters so much: the TA is what turns raw text into the field names your searches use. Splunk Cloud's SmartStore keeps the bulk of indexed data in object storage with a local cache, which changes the cost profile but not how you write searches. Microsoft Sentinel Sentinel sits on Log Analytics workspaces. Each connector writes to its own table, so instead of one big logs table you get dozens of purpose-built ones. That's great for well-supported Microsoft sources and more awkward for third-party ones, where the table you end up with depends on which version of the connector you installed. Google Workspace is a good example. The older Azure Functions connector writes to custom tables such as GWorkspace_ReportsAPI_token_CL. The newer schema uses a GoogleWorkspaceReports table, which Microsoft's table reference marks as a Basic log table that supports lake-only ingestion. Same data, different tables, different query. One Real Detection, Three Languages Here's the problem I picked, because it's a real one: a third-party OAuth app gets granted the full Gmail scope, https://mail.google.com/. That scope means read, send and delete for the whole mailbox. Consent phishing campaigns go after exactly this, so most Google Workspace detection packs include some version of the rule. First, what the data actually looks like Google records each grant as an authorize event under the token application. The interesting parts (app name, client ID, scopes) aren't top-level fields. They sit inside an array of name/value pairs in events[0].parameters, which every platform has to unpack somehow. "events": [{ "name": "authorize", "parameters": [ {"name": "client_id", "value": "1173955..."}, {"name": "app_name", "value": "..."}, {"name": "client_type", "value": "WEB"}, {"name": "scope_data", "multiMessageValue": [...]}, {"name": "scope", "multiValue": ["https://www.googleapis.com/auth/..."]} ] }] RunReveal (ClickHouse SQL), My first attempt picked parameters out by position (the first one is client_id, the second app_name, the fifth scope). It worked, and it's a trap: nothing in Google's API promises that order, and if a parameter is ever added or dropped the rule silently starts reading the wrong field. This version finds each parameter by its name instead: WITH JSONExtractArrayRaw(JSONExtractRaw(rawLog,'events'),1,'parameters') AS params, arrayFirst(p -> JSONExtractString(p,'name') = 'app_name', params) AS p_app, arrayFirst(p -> JSONExtractString(p,'name') = 'scope', params) AS p_scope SELECT JSONExtractString(p_app,'value') AS app_name, count() AS grants, uniqExact(actor['email']) AS users, uniqExact(srcIP) AS ips FROM logs WHERE receivedAt > now() - INTERVAL 7 DAY AND sourceType = 'gsuite' AND eventName = 'authorize' AND has(JSONExtract(p_scope,'multiValue','Array(String)'), 'https://mail.google.com/') GROUP BY app_name ORDER BY grants DESC Run against workspace, it came back in under half a second with exactly one app: our email-security vendor's integration, holding the full Gmail scope for 8 users, with 432 grants from 274 different IP addresses over the week. Both versions, by position and by name, returned the same 432, which is how I checked that the name-based one wasn't missing anything. Read that result cold and it looks alarming: a mailbox-wide scope, used from hundreds of IPs. It's actually expected. Cloud email-security products scan mail through the Gmail API from a pool of cloud addresses. That's exactly why this rule needs an allowlist before it goes anywhere near a notification channel, which is the next section. Splunk (SPL), untested translation In Splunk, the work happens in field extraction. The Splunk Add-on for Google Workspace collects the Reports API activity, including the token endpoint. How the parameters array comes out as fields depends on the add-on version, so treat the field names below as placeholders and check them against your own events first: index=gws "events{}.name"="authorize" "https://mail.google.com/" | spath path="events{}.parameters{}" output=params | mvexpand params | eval pname=spath(params,"name"), pval=spath(params,"value") | eval app_name=if(pname="app_name", pval, null()) | stats values(app_name) AS app_name, values(actor.email) AS user, values(ipAddress) AS ip BY id.uniqueQualifier | stats count AS grants, dc(user) AS users, dc(ip) AS ips BY app_name | lookup oauth_app_allowlist app_name OUTPUT approved | where isnull(approved) It's noticeably more work than the SQL, and most of that is unpacking the array (spath, then mvexpand, then pulling name and value back out). In fairness, a well-built add-on usually flattens this for you at search time, and then the rule shrinks to a couple of lines. That's the Splunk trade in a nutshell: a lot depends on the add-on, and when the add-on is good, writing searches is pleasant. Microsoft Sentinel (KQL), untested translation KQL handles the nested JSON cleanly with mv-apply, which loops over the parameters array per event. Again, the column names depend on which Google Workspace connector populated your table, so adjust them before using this. I've written it against the raw JSON so it doesn't depend on any one connector's flattening: let allowlist = _GetWatchlist("OAuthAppAllowlist") | project SearchKey; GoogleWorkspaceReports // or GWorkspace_ReportsAPI_token_CL on the older connector | where TimeGenerated > ago(7d) | extend raw = todynamic(RawEvent) // RawEvent: whichever column holds the original JSON | extend ev = raw.events[0], Actor = tostring(raw.actor.email), Ip = tostring(raw.ipAddress) | where tostring(ev.name) == "authorize" | mv-apply p = ev.parameters on ( summarize AppName = take_anyif(tostring(p.value), tostring(p.name) == "app_name"), Scopes = take_anyif(p.multiValue, tostring(p.name) == "scope") ) | where set_has_element(Scopes, "https://mail.google.com/") | where AppName !in (allowlist) | summarize Grants = count(), Users = dcount(Actor), Ips = dcount(Ip) by AppName The watchlist is Sentinel's answer to Splunk's lookup table: a CSV you upload and reference by name, which is a nice way to let someone outside the detection team keep the list of approved apps up to date. A Sentinel catch worth knowing: Microsoft's table reference marks GoogleWorkspaceReports as a Basic log table with lake-only ingestion supported. Check which tier your data is landing in before you build a scheduled analytics rule on it. Microsoft's own data lake docs say you can't run analytics rules directly against data lake tables; the data has to be in the analytics tier, or promoted there by a job. The Noise Problem Is the Same Everywhere Before I narrowed the rule to the Gmail scope, I looked at every OAuth grant in the workspace. The numbers are the most useful thing in this article, because they'd hit you on any of the three platforms: Total authorize events (7 days): 24,364, about 145 an hour Distinct OAuth apps behind them: 2 Grants from the SIEM's own Google collector: 23,703 (97%), one client ID, 1 user, 7 cloud IP addresses, present in all 168 hours Grants from the email-security integration: 661, 8 users, 295 IP addresses Scopes the collector asks for: admin.reports.audit.readonly and apps.alerts 97% of the OAuth activity in this tenant is the log collector getting a fresh token so it can read the audit log. It requests admin-reports scopes, every hour, from a rotating set of cloud IPs. Write the common rule "alert on OAuth grants with admin scopes" and the first thing it catches is your own SIEM, about 140 times an hour. Splunk and Sentinel are no different here: their Google Workspace connectors poll the same Reports API with the same kind of service account, so they should generate the same events about themselves. This is also the second time this series has found the collector being the noisiest thing in the data. How each platform lets you exclude it RunReveal: put the exclusion straight into the SQL (AND client_id NOT IN (...)), or tag the collector's events with an enrichment and filter on the tag. Remember from Part 4 that enrichment only tags events from the moment it's activated; it doesn't go back and tag old ones. Splunk: a lookup table of approved apps or client IDs, joined in the search. Easy to share across many correlation searches. Sentinel: a watchlist, as in the KQL above, or an automation rule that closes matching incidents. The watchlist is better, because the noise never becomes an incident at all. Query Languages, Honestly SQL is RunReveal's biggest advantage, and also where it costs you something. If your team already writes SQL, the ramp-up time is close to zero, and ClickHouse's JSON and array functions are fast and capable. But security-specific idioms you'd get for free elsewhere (a time-chart command, transaction grouping, a built-in lookup operator) you write yourself as SQL. SPL is a pipeline language built for exactly this job. It takes real time to learn, and it has quirks (multivalue fields and mvexpand bite everyone at some point), but people who are fluent in it are very fast, and there's a huge amount of shared content written in it. SPL2 brings it closer to SQL, but in practice you'll still be writing classic SPL for most detections. KQL feels the nicest of the three to read. It pipes like SPL, it's typed, and it treats dynamic JSON as a first-class type. If your team already uses KQL for Defender advanced hunting or Azure Monitor, that skill carries straight over. Detection as Code In RunReveal, a detection is text from the start: SQL or Sigma, created and updated over the API. Previous Article showed a detail that matters if you sync detections from git: a Sigma rule is stored as raw YAML in settings.rule, and its query field is empty. A naive export that only reads query will back up every Sigma rule as a blank. I also hit a real gap in the API while testing: deleting a detection through the API didn't work, which is why every test query in this article ran ad hoc instead of being saved. In Splunk, Enterprise Security correlation searches are saved searches underneath, so they can live in an app's configuration files and be versioned like any other config. Splunk also publishes an open library of security detections that many teams use as a starting point. Sentinel analytics rules export as ARM templates, and Sentinel's Repositories feature can deploy rules straight from a GitHub or Azure DevOps repo. That's a solid story if you already run infrastructure as code in Azure. Ingestion: Breadth vs. What You Measure RunReveal's catalogue is around 120 connectors. That's fine for cloud, SaaS and identity sources, and thinner than Splunk's add-on ecosystem, which covers almost anything that writes a log. Sentinel's catalogue is big too, and unbeatable for Microsoft's own sources, many of which come with free ingestion. What I'd look at more than the connector count is lag and duplication, because those affect detections directly. RunReveal's Google Workspace polling lag at between 17 and 155 seconds (median 83), not the round 60 seconds you might assume. Every API-polling connector, on every platform, has some version of this delay. Measure it on whichever platform you pick, and set your rule look-back windows to fit. AI and Automation RunReveal built AI into the workflow early: AI-assisted query writing in Explorer, triage on investigations, and scheduled Agents that run a prompt on a cron schedule and report back. There's a catch I ran into first-hand: those features need an AI provider configured on the workspace, either your own API key or a cloud role RunReveal can assume. With nothing configured, the agents can't run. That's a reasonable design for data control, but it means "AI-native" comes with setup and a bill of its own Splunk offers an AI Assistant for writing and explaining SPL. Sentinel connects to Security Copilot, and the Defender portal move pulls Sentinel incidents into the same queue as Defender XDR. Microsoft's data lake docs also lean on notebooks and AI as reasons to keep years of data. The ClickHouse announcement says RunReveal's agentic investigation work is meant to shape how ClickHouse supports agentic analytics, so expect that side to get more attention, not less. Running It Day to Day This is the part feature lists skip. With RunReveal there's very little to run: no indexers, no forwarder fleet, and most of my time went into writing queries rather than looking after the platform. The flip side is that it's a younger product. I hit undocumented behaviour in nearly every part of this series (string-versus-boolean enabled fields, a delete endpoint that didn't delete, enrichment that doesn't backfill), and you'll find these by testing, not by reading the docs. Self-managed Splunk is the opposite: a lot of infrastructure, but very few surprises left in it after this many years. Splunk Cloud takes most of the infrastructure away. Sentinel needs no infrastructure, but you do need to understand Azure: workspaces, data collection rules, RBAC, and now the Defender portal on top of all of that. So Who Should Pick What Pick Splunk if you need the widest ingestion coverage, you already have SPL skills or content, or you run a lot of on-prem and unusual log sources. Go into the pricing conversation knowing all three models, and model your search load as well as your ingest. Pick Microsoft Sentinel if Microsoft 365, Entra ID and Azure are most of your estate. The free Microsoft data sources and the Defender integration are hard to beat. Plan the Defender portal move now rather than in March 2027, and decide early which tables go to the analytics tier and which go to the lake. Pick RunReveal if your team thinks in SQL, your sources are mostly cloud and SaaS, and you want detections as code without running infrastructure. Before you sign, get the post-acquisition pricing in writing, and ask where the hosted product is going, since ClickHouse's announcement emphasises the bring-your-own-database route. Whichever you pick, do what I did here before trusting any rule: run it against a week of your own data, group by whoever is generating the events, and see how much of it is your own tooling talking to itself. In this tenant it was 97%. -------------------------------------------------------------Dean-------------------------------------
- RunReveal Notifications & the MCP Server I Built
Earlier, I described the escalation ladder — detection, then signal, then alert — and noted that the last step only happens once a notification channel is attached. This is that channel layer. RunReveal supports twelve destinations for alert output: Email, Slack via webhook, Slack via a dedicated bot integration, Discord, PagerDuty, VictorOps, Jira, Google Chat, Linear, incident.io, generic Webhooks, and Tines. That's a reasonable spread — enough to plug into most on-call and ticketing stacks without forcing you through a generic webhook for everything, which is where a lot of smaller SIEM offerings stop. Setting One Up Creating a channel is a one-time step; attaching it to a detection is what actually turns the tap on. A detection or agent with no channel attached can run forever, match real data, and never bother a single person — which is exactly the safe state you want while you're still validating that something works correctly. A Lesson I Had to Learn Twice I want to be honest about something rather than write around it, because it's the most useful thing in this article. Earlier in this series' research, testing a detection end-to-end with a real notification channel attached resulted in an actual email going out to a real person in the workspace — the detection matched a near-guaranteed-to-fire condition faster than I expected, and the channel was live. Later, , the same category of mistake nearly happened again from a different angle: creating an agent through the API with the notification field left unset caused it to default to the same email channel automatically, rather than staying unattached. The actual rule I now follow: Never attach a real notification channel to anything — a detection or an agent — until you have watched it complete at least one full, safe test cycle and read the output yourself. Assume any new object might default to a live channel unless you have explicitly confirmed otherwise by reading the object back, not just by reading what you sent. It's a small operational discipline, but it's the difference between testing safely and accidentally paging a real colleague over something you were only trying to verify works. Keep it in mind for the second half of this article, because it ended up shaping how I built my own MCP server. The MCP Server I Built Most of the hands-on findings came from a conversation. I'd ask a question in MCP it would run SQL, I'd read the result, and we'd go again. What made that possible is a small MCP server I wrote against RunReveal's REST API. This half of the article is about that server: what's in it, what I deliberately kept out of it, and a few real sessions that show why I think it's the most useful thing I built during this whole series. If you haven't met MCP yet: the Model Context Protocol is a standard way to hand an AI assistant a set of tools. The assistant sees each tool's name, description and parameters, decides when to call one, and gets the JSON result back. The server is the bit that sits between the assistant and your real system, and it decides exactly what the assistant is allowed to touch. What it is It's about 330 lines of Python. A thin HTTP client built on httpx handles auth and error handling, a FastMCP server defines the tools, and a tiny CLI picks the transport. It runs over stdio for AI tool, or over SSE on a local port if you want to point something like Bedrock at it. Wiring it into Claude Code is one JSON block: { "mcpServers": { "runreveal": { "command": "uv", "args": ["--directory", "/path/to/runreveal-mcp", "run", "runreveal-mcp"] } } } Auth was the first thing the docs got wrong. The example request on RunReveal's auth page 404s even with a valid token. What actually works is HTTP Basic auth where the token from the dashboard (Workspace → API Tokens) is already the finished credential: you send Authorization: Basic as-is, with no extra base64 step. I only found that by trial and error against the live API. It exposes eleven tools, split by how much damage they can do: Read (7): query_logs, list_sources, list_detections, get_detection, list_notifications, list_investigations, get_investigation Write (3): create_investigation, add_investigation_note, add_investigation_indicator Correlate (1): ioc_lookup, which searches every case in the workspace for a prior indicator match Every one of those wraps an endpoint I called by hand first and checked the response of. None were written from the docs alone. RunReveal already has an MCP server. Why build another? Fair question. RunReveal ships an official one, and it's good. There's a hosted version at api.runreveal.com/mcp that uses OAuth, and a local version you run with the runreveal CLI using an API token. It exposes a lot: querying and schema tools, but also detections_create, detection_update, detection_delete, sigma_create, agents_create, agents_delete, agents_silence, investigation_create, dashboard_graph_create and notification_send. Read that last list again with the first half of this article in mind. I had already sent one real email to a real colleague by accident, and nearly sent a second through an agent that switched itself on. Handing an AI model a tool called notification_send, in a workspace where the only channel is a live email address, is the same mistake with the model making it instead of me. Same for agents_create, given that RunReveal's own create endpoint ignores enabled: false So what i built is the other way around. The model can read anything it needs to investigate, and the only things it can write are case notes and indicators, which are append-only and land in a case someone has to look at anyway. It cannot create or change a detection, start an agent, touch a pipeline, delete anything, or send a notification. The worst a confused model can do with my server is open a case with a bad title. The design rule: Give the model every read it needs and almost no writes. A read that goes wrong wastes a query. A write that goes wrong can page someone, silence a detection, or delete evidence. What I left out on purpose Beyond the safety choice above, some tools are missing because the endpoints behind them don't work. Detection delete, pipeline add-step and execute-step, and investigation close/update/delete were all broken or unverified server-side when I am creating this article. I'd rather ship eleven tools that work than fourteen where three fail in ways the model can't explain back to me. The list is written into the server's docstring so the next person doesn't add them without testing. Gotchas baked into the wrappers Most of the value of a wrapper is the stuff it quietly gets right so the model doesn't have to. Each of these cost me at least one failed call to find: Errors come back as HTTP 200. RunReveal wraps every response in {success, result}, and a failed call can still be a 200 with success: false. The client checks that flag and raises with the API's own error text, because a bare status line tells you nothing. Optional parameters must be dropped, not passed through. A Python None handed straight to the query string risks going out as the literal text "None", which the API would treat as a real filter value. The client strips None values before every request. Artifact types are lowercase. Adding a note to a case needs type "note", and indicators need "indicators". The console labels them "Note" and "Indicators", and the capitalised versions fail. Detections are fetched by name, not ID (?name=), while investigations and agents are fetched by ID (?id=). Same API, opposite conventions. Map columns like actor and tags need bracket syntax in SQL (actor['email'], not actor.email). That hint lives in the query_logs description so the model gets it right first time. What It Looks Like in Use: Three Real Sessions This is the part that sold me on it. Each of these is a real exchange against the live workspace. I've trimmed the output, but not changed it. Session 1: "Will this detection ever fire?" A test detection for failed Google Workspace admin logins had run every five minutes for over a week with zero hits, and I asked whether that meant nothing had happened. The model pulled the detection with get_detection, saw the filter was eventName ILIKE '%fail%login%', and instead of answering it checked what event names the source actually produces: -- query_logs SELECT DISTINCT eventName FROM logs WHERE sourceType = 'google-workspace-alerts' Admin password reset Suspicious login Activity Rule None of those can ever match that filter, so the rule is structurally dead, not quiet. That took two tool calls inside one conversation. In the console it's a detection edit screen, an Explorer tab and a manual comparison, which is exactly the kind of check that's easy to skip. Session 2: "46 admin password resets in an hour" A burst-of-password-resets detection returns 46 resets in 60 minutes, with an empty actor. That looks like a mass account takeover. The model's first move was to regroup the same rows by the alert's own ID, pulled out of the raw log, instead of by actor. I re-ran it today while writing this, to see whether anything had changed: -- query_logs, whole source SELECT count() AS raw_rows, uniqExact(JSONExtractString(rawLog,'alertId')) AS distinct_alerts, countIf(actor['email'] = '') AS empty_actor_rows FROM logs WHERE sourceType = 'google-workspace-alerts' raw_rows: 90997 distinct_alerts: 4 empty_actor_rows: 90997 -- same source, last three hours, per hour hour 05:00 rows 45 distinct_alerts 1 hour 06:00 rows 46 distinct_alerts 1 hour 07:00 rows 45 distinct_alerts 1 When I wrote article the source had 86,661 rows. Four days later it has 90,997, still only four real Google alerts, and still the same one alert re-ingested about 45 times an hour. The actor field is empty on every single row, because this connector only puts the email inside rawLog. So the "burst" is a polling artifact, and the growth since previous article is the bug still running. Checking that took one follow-up question, not a new investigation. Session 3: working the case, and correlation across cases Once the triage was done, the model opened a case with create_investigation, wrote the reasoning into it with add_investigation_note, and attached the admin email as a structured indicator with add_investigation_indicator. Then it ran ioc_lookup on that email to see whether it had come up before: -- ioc_lookup(ioc_type='email', ioc_value='admin@') Burst of admin password resets (GWS alerts) closed false_positive high Demo: Admin password reset - admin@ closed false_positive high -- ioc_lookup(ioc_type='Email', ioc_value='admin@') null Two things there. First, correlation works: the new case immediately linked back to an older closed case about the same account, also marked false positive, which is exactly the context you want before escalating. Second, the value match is partial and case-insensitive, but the type match is exact and case-sensitive. Ask for "Email" and you get nothing back, with no error. The tool description spells out that the type is an exact match, so the model uses the lowercase form. One detail matters here: notes are not searchable by ioc_lookup, only indicators are. If you write "the attacker IP was 1.2.3.4" in a note, the next case will never find it. That's why the note and indicator tools are separate and the note tool's description says so. Why this is the part I care about: Question → SQL → evidence → case → correlation, in one conversation, without leaving the terminal. And at no point could the model silence a detection, delete anything, or email anyone. That combination of a short loop and a hard safety boundary is what makes it something I'd actually use on a real incident. And RunReveal's Own AI Chat? RunReveal also has native AI Chat inside the console, plus the Agents. Both are bring-your-own-model: under Settings → AI Settings you add Anthropic, OpenAI or Google AI with an API key, Amazon Bedrock with an IAM role RunReveal assumes (role ARN, region, optional external ID), or Vertex AI with a service account. There's no built-in default model. I haven't connected a model provider to this workspace, so I'm not going to describe what the chat is like to use from the docs. Part of that is deliberate. Connecting a provider means the workspace's logs flow to that provider under whoever's key or role it is, and that's a decision for whoever owns the data and the AI account, not something to do casually for a blog post. The MCP route sidesteps that: the model I already use calls the API with a scoped token, and I decide tool by tool what it can reach. --------------------------------------------------Dean-------------------------------------------- Next Part pulls back to the bigger picture — how RunReveal actually stacks up against Splunk and Microsoft Sentinel for a working SOC analyst.
- RunReveal Investigations & Case Management: What the Workspace Actually Shows
Start With What Is Actually There The pitch for RunReveal's Investigations feature is easy to repeat: an alert fires, a case opens, an AI agent does first-pass triage, a human picks it up. I could write that section from the docs in ten minutes. Instead, everything below comes from pulling the real investigations, artifacts, alerts and audit trail out of a live workspace and reading what's there. The first thing the data shows is that the pitch and the reality don't line up by default. The existing investigations here were all opened by hand, from the console or through the API, not by the platform. That gap is the most useful thing in this article, so I'll start there. Why an Alert Did Not Become a Case An alert and an investigation are separate objects in RunReveal, and nothing in the alerts table points at an investigation. A real alert row has 26 columns (detection ID, run ID, the raw query, the results, the notification names it went to) and none of them is an investigation ID: SELECT * FROM alerts -- one real row, trimmed detectionName : test-alert-frequent-runquery recordsReturned : 1 results : [40] severity : low notificationNames : ["default-email"] -- no investigationID column exists on this table at all The link goes the other way, through a separate lookup. The workspace audit log shows the console calling an investigations_by_alert endpoint whenever someone opens an alert, which is how the UI answers "is there a case for this?" In SQL terms you can't join alerts to investigations. You have to ask the API. As for why no case opened: every detection in this workspace has an empty settings object (settings: {}), apart from a Sigma rule, whose settings hold only its YAML. None of them has AI triage or auto-investigation turned on. Automatic case creation is something you opt into per detection. It isn't a default that every alert goes through. So a detection wired to email, like the one that fired here, sends its email and stops. If you're evaluating RunReveal and expecting cases to show up on their own, check that setting on each detection first. Case Study: Running a Scenario End to End Reading existing cases only shows so much, so I ran a full scenario live: write a detection, run it on real data, open a case from the result, triage it, add indicators, and check whether cross-case correlation actually finds anything. I ran the detection SQL directly in Explorer rather than saving it as a scheduled detection, detections created through the API. Step 1: the detection fires, and it looks bad The scenario is a classic one: alert on a burst of admin password resets, which is what a mass account takeover or a rogue admin looks like in Google Workspace. The obvious first draft counts resets per admin over the last hour: SELECT actor['email'] AS admin, count() AS resets FROM logs WHERE sourceType = 'google-workspace-alerts' AND eventName = 'Admin password reset' AND receivedAt >= now() - INTERVAL 1 HOUR GROUP BY admin HAVING resets >= 5 → admin: "" resets: 46 46 password resets in 60 minutes by an admin the data can't even name. On a real team, that result at 3am gets somebody paged. So I opened a high-severity investigation for it, the same way an auto-triage detection would. Step 2: triage finds the real story The empty admin was the first clue. On this source, the normalized actor map and srcIP column are empty on every single row. The admin's email only exists inside rawLog, at data.actorEmail. Google Alert Center also gives each alert its own alertId, so I grouped on that instead: SELECT JSONExtractString(rawLog, 'data', 'actorEmail') AS admin, JSONExtractString(rawLog, 'alertId') AS google_alert_id, JSONExtractString(rawLog, 'metadata', 'severity') AS google_severity, min(eventTime) AS happened_at, count() AS copies_ingested_last_hour FROM logs WHERE sourceType = 'google-workspace-alerts' AND eventName = 'Admin password reset' AND receivedAt >= now() - INTERVAL 1 HOUR GROUP BY admin, google_alert_id, google_severity → admin: google_alert_id: f4a61ff7-... google_severity: HIGH happened_at: 2026-09-04 03:56:55 copies_ingested_last_hour: 46 There were never 46 resets. There was one, twenty days earlier, and RunReveal ingested the same Google alert 46 times in the last hour. Zooming out to the whole source: SELECT eventName, count() AS rows, uniqExact(JSONExtractString(rawLog, 'alertId')) AS distinct_alerts FROM logs WHERE sourceType = 'google-workspace-alerts' GROUP BY eventName → Activity Rule 44,811 rows 1 alert → Suspicious login 19,851 rows 1 alert → Admin password reset 21,999 rows 2 alerts total 86,661 rows 4 real Google alerts The pattern is consistent. The alerts poller keeps fetching the newest alert again, roughly every 80 seconds (about 46 times an hour), until Google produces a newer one. Then the newer one gets the same treatment. The Activity Rule alert was re-ingested for six weeks, the Suspicious login alert for three, and the password reset alert is still being re-ingested as I write this. Step 3: indicators and cross-case correlation, working With the conclusion written up as a note, I added the admin's email as an email indicator on the new case and ran IOC lookup on it. It came back with the new case and also an earlier, closed case, classified false positive, involving the same admin and the same kind of alert: ioc_lookup(type="email", value="") → ARTICLE TEST - Burst of admin password resets open high (unclassified) → closed high false_positive This is what the feature is for. An analyst (or the triage agent) opening the new case immediately sees that this same entity was already investigated and closed as a false positive. Here it's the right hint. It only works because both cases used the same lowercase email type, as covered in the IOC section below. What this scenario actually proved The naive detection would page someone on a 20-day-old event, about 46 times an hour, for as long as nothing newer comes in. Any rule on this source keyed on actor['email'] or srcIP can never match anything, because those fields are always empty. The real values are only in rawLog. The enabled Sigma rule for Activity Rule events on this workspace has zero matches in the detections table, even though 44,811 rows with that exact eventName exist. Its logsource says product: google_workspace, and a likely explanation is that this doesn't map to the google-workspace-alerts source, which would make it another rule that stays silent for structural reasons, like the one in Previous article It explains an earlier puzzlePrevious article example of one detection run returning 11 rows was 11 copies of the same Activity Rule alert. The investigation workflow itself did its job. Case, note, indicator and correlation all behaved as designed, and the triage is what caught the data problem. The practical takeaway: For any polled alert source, group or deduplicate on the upstream alert's own ID before you count anything, and check that the normalized fields you filter on are actually populated before you trust a rule built on them. SELECT countIf(actor['email'] != '') FROM logs WHERE sourceType = '' takes two seconds and would have caught this before it was a detection. Anatomy of a Real Case Here is a closed investigation as get_investigation returns it, with identifying values redacted. It's worth seeing the whole shape once, because a few fields don't behave the way their names suggest: { "title": "", "status": "closed", "severity": "high", "classification": "false_positive", "tags": ["google-workspace", "admin-action"], "created_by": "token:Testing", "assignee": "Testing", "created_at": "2026-09-11T10:07:27.740424Z", "detected_at": null, "responded_at": null, "triaged_at": "2026-09-11T11:15:32.560355Z", "resolved_at": "2026-09-11T11:15:32.560355Z" }, "metrics": { "investigateSeconds": 0, "resolveSeconds": 4084.819931 } Two things jump out. First, created_by is token:Testing and the assignee is just Testing. When a case is created with an API token, the token's name stands in for a person. Name your tokens after the automation or the human who owns them, because that name is what shows up as the case owner. Second, look at triaged_at and resolved_at. They are the same value down to the microsecond. That matters for MTTR, which gets its own section below. The timeline: four artifact types, only two you can write Every investigation carries a timeline of artifacts. Across the real cases in this workspace I found four types: investigation_update: written by the system when status or classification changes. The meta field records field, oldValue and newValue. note: free text you add. indicators: structured entities you add, which is what IOC lookup searches. resolution: written by the system when the case is closed, carrying the closing comment. You can only create note and indicators yourself. The other two are the platform's own record of changes. Also, don't assume the API returns the timeline in order. On the scenario case, get_investigation listed the indicator before a note that was created 150 milliseconds earlier, so sort on created_at yourself if order matters. And an open case returns metrics: {}, not zeros. The metrics object only fills in once a case closes. One detail from the real indicators artifact: its meta holds an array, not a single value, so one artifact can carry several entities at once: { "type": "indicators", "comment": "actorEmail from the source alert", "meta": { "indicators": [ { "type": "email", "value": "" } ] } } Notes vs. Indicators, and the Casing Trap, Straight From the Audit Log The split between notes and indicators is well designed. Notes are prose for humans and IOC lookup can't see them. Indicators are structured and are the only thing cross-case correlation searches. A note saying "the attacker used admin@example.com" is invisible to every other investigation. The same email in an indicators artifact is not. The API is strict about the type string, and the docs and UI write the types as "Note" and "Indicators" with capitals. What makes this concrete is that RunReveal's own audit log keeps every failed attempt, with the error message attached. Here is every rejected artifact type from one session of working out what the API actually wants, pulled straight from the log: SELECT JSONExtractString(rawLog, 'supplemental_data', 'error_message') FROM logs WHERE sourceType = 'runreveal-audit' AND eventName = 'investigation_add_artifact' → invalid artifact type: Note → invalid artifact type: NOTE → invalid artifact type: Note_Artifact → invalid artifact type: ARTIFACT_TYPE_NOTE → invalid artifact type: artifact_note → invalid artifact type: Indicator → invalid artifact type: indicator → invalid artifact type: IOC → invalid artifact type: ioc Nine rejections. The only two values that work are lowercase note and lowercase plural indicators. There's a side lesson here too: that audit event is logged the same way whether the call succeeded or failed. Of 14 investigation_add_artifact events, 9 were errors. If you count that event to mean "artifacts added," you'll be off by more than half. Filter on the error flag in supplemental_data first. The practical takeaway: Anything that writes to investigations automatically (a SOAR playbook, an MCP tool, a triage script) should hardcode type: "note" and type: "indicators", and should put every entity someone might pivot on into an indicators artifact, not just mention it in a note. IOC Lookup: Tested, Not Assumed IOC lookup is what makes cross-case correlation work, and the auto-triage agent calls it for every entity it sees. The docs describe it as an exact type match plus a partial, case-insensitive value match. I ran four lookups against a real email indicator in the workspace: type value (search) classification result ────────── ────────────────────────── ─────────────── ────────── email (any) 1 match Email (any) null user (any) null email true_positive null Each row shows something specific: Row 1: the value match really is partial and case-insensitive. Searching just the domain in capitals still found the full lowercase address. Row 2: the type match is not just exact, it's case-sensitive. Email with a capital E found nothing. The docs don't say this, and it's the same trap as the artifact types. Row 3: types are separate silos. The same address filed under user instead of email can't be found by an email lookup, and the reverse is true too. If one analyst files an address as email and another files it as user, correlation between their cases quietly breaks. Row 4: the classification filter works. The only match was a false-positive case, so a true_positive filter correctly came back empty. Worth deciding as a team before you have any cases: Pick one lowercase type per kind of entity (email for addresses, ip, domain, hash, user for account names that are not emails) and write it down. IOC lookup will not fix inconsistency for you. It will just return nothing. MTTR: What the Four Stages Show on Real Cases RunReveal breaks mean time to resolution into four stages: Detect (activity to alert), Respond (case created to first human engagement), Investigate (triage start to close), Resolve (creation to close). Here is what two real closed cases report: case detected_at responded_at investigateSeconds resolveSeconds case A null null 0 63303.08 case B null null 0 4084.82 I checked case B's resolve Seconds by hand: 10:07:27.740 to 11:15:32.560 is 4,084.82 seconds, so that number is exactly closed minus created, as documented. The other three stages are less useful on these cases: detected_at and responded_at are null on both. Nothing about a hand-opened case gives the platform a detection time, so the Detect and Respond stages have nothing to measure. investigateSeconds is 0 on both, because triaged_at gets stamped at the same moment as resolved_at when you close a case. Close a case without first moving it through a triage status, and the Investigate stage records zero seconds no matter how long you actually spent on it. In practice, for any case opened by hand and closed directly, Resolve is the only one of the four stages carrying real information. The other three only fill in when cases come from alerts and move through the agent-triage and investigating statuses on the way to closed. If your MTTR dashboard shows a suspiciously perfect 0-second Investigate time, this is why. Two Gaps in the Timeline Classification set at close leaves no trail In one case, the classification was changed on its own before closing, and the timeline has an investigation_update artifact for it (field: classification, oldValue: "", newValue: false_positive). In another, the classification was set as part of the close action, and the only artifact is the resolution, whose meta records status changing from open to closed and nothing else. The case is classified false positive, but the timeline never says when or by whom. If you rely on the timeline as an audit trail for classifications, set the classification as its own step before closing. Closed cases still take notes, and classification does not follow them One case was closed as false positive. The next day a note was added to it, while it was still closed, that reads in full: "This is a true positive but not a threat." The note was accepted, the case stayed closed, and the classification still says false_positive. Nothing flags the contradiction. That note is a real analyst hitting a real gap. The classification field has three values (true_positive, false_positive, inconclusive) and no benign-true-positive option. The detection caught exactly what it was meant to catch, a real admin password reset, which just turned out to be authorized. Plenty of SOCs track that as its own category because it's what tells you whether to tune a rule or leave it alone. Here, you have to pick one of the three and put the nuance in a note that IOC lookup and the classification filter will never see. The Audit Log Is the Real Usage Record, With a Catch Every investigation action lands in the workspace's own audit source (sourceType runreveal-audit), which you can query like any other log. That's handy if you want to know how a team really uses cases rather than how they say they do: SELECT eventName, count() FROM logs WHERE sourceType = 'runreveal-audit' AND eventName ILIKE '%investigation%' GROUP BY eventName ORDER BY count() DESC → investigation_metrics 50 → investigation_list 33 → investigations_alert_status 21 → investigation_facets 18 → investigation_get 14 → investigation_add_artifact 14 → investigation_create 3 → investigation_close 3 → investigation_ioc_lookup 1 It already tells a story: people look at the metrics widget far more often than they open cases. But investigation_create didn't match the number of cases in the investigation list, and neither did investigation_close. The raw rows explain it: some audit events are ingested twice, with identical payloads including the nanosecond timestamp, about 30 seconds apart. Counting distinct events instead of rows: SELECT count() AS raw_rows, uniqExact(JSONExtractString(rawLog, 'timestamp'), eventName) AS distinct_events FROM logs WHERE sourceType = 'runreveal-audit' → raw_rows 9683 | distinct_events 8035 (17% duplicates) Across the whole workspace's audit history, 17% of rows are duplicates of another row. That's not specific to investigations, but it's where I caught it, because the investigation counts were small enough to check against reality. Anything that counts audit events, whether a dashboard, a usage report or a detection that fires on N actions in a window, needs to deduplicate on the payload timestamp first or it will overcount. Two more limits worth knowing before you build on this source: The audit payload has no investigation ID. It records who did what and when, but not which case. You can't rebuild one investigation's history from the audit log alone. For API-token actions, actor_email is the token's name (here, Testing), not an email address. The user agent (curl vs. a browser) is the quickest way to tell automation from a person clicking. The practical takeaway: Wrap any count over runreveal-audit in uniqExact(JSONExtractString(rawLog, 'timestamp'), eventName) instead of count(), and exclude rows where supplemental_data.error is "true" if you want successful actions only. Auto-Triage: How It Is Documented to Work An alert from a triage-enabled detection auto-creates an investigation, tagged auto-created and alert, and the agent moves it to agent-triage. The agent runs IOC lookup on every entity involved. That's the same endpoint tested above, with the same exact, case-sensitive type matching. It writes an analysis note. For a true positive it adds indicators artifacts and hands off by moving the case to investigating. For a false positive it closes the case itself with a classification. A detection's own notes field goes straight into the agent's prompt. An investigation's notes don't. The agent only sees them if it fetches the case during its run. Put playbook guidance on the detection, not in the case. ------------------------------------------------------Dean------------------------------------------------ Next Part covers the other half of RunReveal's AI story: scheduled agents that go looking for problems on their own, instead of reacting to alerts that already fired.
- RunReveal Detection Engine: SQL, Sigma, and What the Workspace Actually Shows
Detection-as-Code, For Real A detection in RunReveal is a scheduled query with metadata attached — severity, risk score, MITRE ATT&CK mapping, which sources it applies to. The query is the detection: version-controllable, diffable, reviewable in a pull request, because it's SQL or Sigma text, not a form full of dropdowns serialized into some proprietary format. Everything below this point is Live tested, real detection objects, real query results, real run history. Nothing here is a hypothetical. Two Detection Types SQL detections Full ClickHouse SQL — joins, custom views, historical lookback, whatever the query language supports. Typically scheduled every five minutes via a cron expression, though one real detection in the test workspace uses a shorthand form instead: schedule: "@1m" runs identically to schedule: "* * * * *". Neither the console nor the API docs mention the @-shorthand exists; it was only visible by reading a real detection object back through the API. The windowing gotcha: The documented, correct way to filter a time window in a scheduled SQL detection is receivedAt >= {from:DateTime} AND receivedAt < {to:DateTime} — RunReveal auto-injects from/to on every scheduled run. now() - INTERVAL X MINUTE runs without erroring, but it is not the supported pattern, and one real detection in this workspace still uses it (see below) rather than the documented one. Sigma detections Real-time streaming pattern matching using the industry-standard Sigma format. Simpler than SQL, portable across tools that also support Sigma, but no joins and no custom views. Pulling a real Sigma detection back through the API shows something the docs don't spell out: the top-level query field is an empty string. The actual rule lives one level down, as raw YAML, inside settings.rule: { "type": "sigma", "query": "", "settings": { "rule": "title: Google Alert Center Activity Rule Test\n id: 11111111-1111-4111-8111-111111111111\n status: test\n logsource:\n product: google_workspace\n detection:\n selection:\n eventName: Activity Rule\n condition: selection\n level: medium" } } If you're building anything that reads detections back programmatically — a CI check, a backup script, an MCP tool — checking query for Sigma rules will silently find nothing. You have to branch on type and parse settings.rule as YAML instead. The Escalation Ladder, With Real Numbers Behind It A match doesn't automatically page anyone. It climbs a three-step ladder: Detections — every single match gets recorded here, quietly, with no notification. Signals — matches that RunReveal considers worth surfacing, still with no notification channel attached. Alerts — the same match, once a notification channel is actually attached to the detection. That's the documented model. Querying the workspace's own detections and alerts tables directly — both queryable exactly like any other table in RunReveal, which is a genuinely nice property — shows what it looks like in practice, not in theory. Across every detection that has ever run in this workspace, exactly one alert has ever fired, ever. One. Everything else that ran — and plenty has, including a detection that’s been executing every five minutes for over a week straight — has stayed at the "detections" tier: recorded, queryable, and completely silent. The escalation ladder isn’t a marketing description of intended behavior; it’s really how quiet the system stays until something is deliberately wired to notify. A structural quirk the docs never mention: detections is per-match, not per-run One real detection returned 11 matching rows on a single scheduled run. Querying the detections table for that detection’s history back: SELECT id, scheduledRunID, recordsReturned FROM detections WHERE detectionName = '' came back as 11 separate rows, each with a distinct id but the identical scheduledRunID and the identical recordsReturned: 11 value copied onto every row. The table isn't one row per scheduled execution — it's one row per matched record, with scheduledRunID as the join key back to the run that produced them. recordsReturned tells you the run's total match count, duplicated across every row it produced, not that row's own count (which is always 1). If you're aggregating this table for a dashboard or a report, count(DISTINCT scheduledRunID) is "how many times this rule fired" — count(*) is "how many individual matches came back," and confusing the two will silently 11x your numbers for any rule that returns multiple rows per run. Case Study: A Detection That Ran Clean for a Week and Found Nothing — For the Wrong Reason This is the finding worth the most attention in this article, because the failure mode it demonstrates isn't RunReveal-specific — it's a trap any SQL-based detection engine leaves open. A real test detection in this workspace flags repeated failed admin logins: SELECT actor['email'] AS user, srcIP, count() AS failures FROM logs WHERE sourceType = 'google-workspace-alerts' AND eventName ILIKE '%fail%login%' AND receivedAt > now() - INTERVAL 5 MINUTE GROUP BY user, srcIP HAVING failures >= 5 It's enabled, on a five-minute schedule, and has been executing without error for over a week. It has zero rows in the detections table for its entire run history — no matches, ever. Read on its own, that looks like exactly what you want from a tuned detection: no false positives, quietly waiting for a real brute-force attempt. Querying the real event vocabulary this source actually produces tells a different story: SELECT DISTINCT eventName FROM logs WHERE sourceType = 'google-workspace-alerts' → "Admin password reset" → "Suspicious login" → "Activity Rule" None of those three values contain the substring the detection is filtering for. eventName ILIKE '%fail%login%' cannot match anything this source will ever emit — the real event for a failed login here is literally named "Suspicious login," with no "fail" in it anywhere. This detection is not quiet because the environment is clean. It's quiet because its WHERE clause and the source's real data never had any chance of intersecting, and it will stay silent forever regardless of what actually happens, because RunReveal has no mechanism to warn you a filter doesn't match anything the source produces — a rule that's structurally guaranteed to never fire looks, from the outside, identical to a healthy, well-tuned one. The practical takeaway: Before trusting silence from any new detection as "nothing bad happened," run SELECT DISTINCT eventName FROM logs WHERE sourceType = '' against the real data first and check your filter actually intersects with it. Writing the WHERE clause from what you assume the event names should be, instead of what they actually are, is an easy way to ship a detection that will never once fire. API Gotchas Worth Knowing Before You Automate Anything I built an MCP server against this API, which meant hitting every endpoint by hand before trusting it enough to wrap in a tool. A few things the documentation doesn't mention: name is required on creation but not marked as required in the documented schema — omit it and you get a generic validation error that doesn’t obviously point at the missing field. The enabled field on /detections/enabled/update wants a string, literally "false", not a JSON boolean. Send a real boolean and it 500s. /detections/get takes the detection’s name as a query parameter, not an ID in a POST body. Deleting a detection through the API doesn’t work. It returns success and hands back the full detection list, with your target detection still sitting in it, completely unchanged. That last one isn’t a one-time observation — it’s still verifiably true. Four test detections created during earlier hands-on testing for this series are still sitting in the workspace today, weeks later, disabled rather than deleted because that was the only way to actually neutralize them. A fun contradiction, if you read the Agents piece in this series: RunReveal's Agents API wants enabled as a real boolean and rejects a string outright — the exact opposite convention from detections. And Agents' delete endpoint actually works, unlike detections. Don't assume any two endpoints on this API follow the same rules, even when they look structurally identical. A Real Timing Trap Checking whether a scheduled detection found anything immediately after its run time updates is a trap. I hit zero results checking within roughly 15 to 90 seconds of a run, even when I could prove with a manual query that the underlying data clearly matched the detection's WHERE clause. Minutes later, the same broader query found the matches sitting at their original timestamps. There's real read-after-write lag between a detection run completing and its results being reliably queryable. Don't trust an immediate zero-result check as proof a rule doesn't work. ------------------------------------------------------Dean----------------------------------- Next Part follows the thread from here — what happens once an alert actually fires and an investigation gets opened.
- RunReveal Data Ingestion: Sources & Connectors
Three Ways Data Gets In Before any of the architecture matters, data has to actually arrive. RunReveal supports three ingestion methods, and which one a given source uses isn't a style choice — it changes what happens when something goes wrong. Webhook — push-based. The source sends events to a URL RunReveal gives you. Fast and simple, but if the network hiccups mid-send, that event is gone. No retry, no replay. Polling — RunReveal reaches out to the source on an interval, roughly every 60 seconds, using encrypted stored credentials. It checkpoints where it left off, so a restart or a blip doesn't lose data — it just resumes. Object Storage — S3, Azure Blob, GCS, R2, MinIO. A bucket gets a notification when a new object lands, and RunReveal pulls it from there. Good fit for anything that already dumps logs to a bucket as its native export format. If I were setting up a new source and had a choice between webhook and polling for the same data, I'd take polling every time — the checkpoint-and-resume behavior alone is worth the extra ~60 seconds of latency for anything that isn't genuinely time-critical. Forwarders, for Everything Else For file-tail scenarios, Windows Event Log, and syslog, RunReveal ships Fluent Bit support and its own lightweight forwarder called reveald. What I didn't expect: reveald has its own concept of destinations, separate from the platform-level Destinations — it can forward to MQTT, to a literal network printer, to S3 directly, or to RunReveal itself. The printer option is a strange one to see in 2026, but it tells you reveald was built as a genuinely generic forwarder, not something narrowly scoped to just talk to RunReveal's own backend. The Connector Catalog RunReveal advertises around 120 source connectors. I crawled the documentation for all of them rather than take that number on faith, and it holds up — roughly 114 individual connector doc pages exist, spread across cloud providers (AWS, Azure, GCP), identity platforms, EDR and security tools, SaaS applications, and networking sources, plus generic custom/webhook options for anything without official support. A caveat worth being upfront about, with real numbers instead of a vague gesture at them: 67 of those 114 connectors — 59% — came back with auth method genuinely unclear from the page text once I structured the crawl into a catalog. Broken down by ingestion type, it's 43 object storage, 34 polling, 18 webhook (push), 4 queue/pubsub, 1 forwarder/syslog, and 14 where even the ingestion method itself wasn't stated plainly. By category: 50 fall into a catch-all "Other," 17 SaaS, 12 EDR/Security, 10 each for AWS and Networking, 6 Identity, and single digits for Azure, GCP, and generic Custom sources. Treat any public connector catalog, including this one, as a starting point for discovery — always check the specific connector's live doc page before wiring it up for real. What a Real Source Looks Like The workspace I tested against runs Google Workspace ingestion — both the audit log stream and the separate security alerts stream — using polling. Watching real data actually accumulate rather than just reading about the schema was where the earlier ClickHouse architecture stopped being abstract: querying tens of thousands of real events back with sub-second response times is a genuinely different experience than tailing a Splunk search that's still "scanning buckets" thirty seconds in. Source Lifecycle Disabling a source stops new ingestion immediately but keeps its configuration and all previously ingested data intact — you're not starting from zero if you re-enable it later. Actually deleting a source's historical data is a support-ticket process, not a self-service button. Hands-On: Measuring the Polling Lag Instead of Quoting It "Roughly every 60 seconds" is the documentation's own framing for polling latency, and it's close enough for planning purposes, but I wanted an actual measurement rather than a restated number. Every Google Workspace audit event carries its own original timestamp inside the raw payload — when Google logged it — separately from receivedAt, RunReveal's own record of when it actually landed in ClickHouse. That gap is the real, end-to-end polling lag, and it's queryable directly. Eight consecutive real events, timestamp of the underlying Google event versus RunReveal's receivedAt: event 13:31:53.8 -> received 13:33:09 lag 75.2s event 13:31:43.0 -> received 13:33:06 lag 83.0s event 13:32:48.6 -> received 13:33:06 lag 17.4s event 13:30:36.2 -> received 13:31:56 lag 79.8s event 13:30:19.3 -> received 13:31:54 lag 94.7s event 13:30:31.1 -> received 13:31:54 lag 82.9s event 13:27:45.7 -> received 13:30:21 lag 155.3s event 13:29:14.9 -> received 13:30:18 lag 63.0s Min 17 seconds, max 155 seconds, median 83 seconds, average 81 seconds — real variance, not a flat 60-second heartbeat. "Roughly every 60 seconds" is the right mental model for what triggers a poll cycle, but the number that actually matters for anyone setting alert-latency expectations is closer to "budget for up to two and a half minutes, typically around 80 seconds" than "60 seconds." Worth knowing before you write a detection's schedule assuming data is never more than a minute stale. Next Part picks up right where the data lands — the detection engine that actually reads all of this on a schedule. --------------------------------------------------------Dean-----------------------------------




