One alert in, one verdict out: building IntelPulse 2.0 for a whole SOC
How I turned a single-user threat intelligence workbench into a self-hosted web app for a team, and what shipping it taught me that the tests could not.
Twenty minutes, every alert
Here is a firewall alert a SOC analyst might see on a night shift:
Nov 03 02:14:11 fw-edge-01 kernel: [UFW BLOCK] SRC=203.0.113.66 DST=10.20.4.15 DPT=445One source address. The question is simple: is this noise, or is it someone we should be worried about? The work is not. You open AbuseIPDB, paste the address. Then AlienVault OTX. Then GreyNoise, to see if it is just an internet-wide scanner. Then ThreatFox and URLhaus, in case it is known malware infrastructure. Then you weigh what five sites said, in five different formats, and write it up.
A real alert names more than one indicator. Doing this by hand can take twenty minutes, and at forty alerts a shift, that is where triage time goes. It is also where indicators get missed, because the fifth tab is the one you skip at 3 a.m.
I built IntelPulse to do that pass in one request, and to show its working.
What it does
You paste an alert, a log, a list of indicators or a file. IntelPulse:
- Extracts every indicator. IP addresses, domains, URLs, file hashes, email addresses and CVEs, from plain lists, syslog, Windows events as JSON or a SIEM export. Defanged notation (
hxxp://,1.2.3[.]4) is refanged. Private, loopback, CGNAT and documentation address space is dropped before anything is looked up, so internal addresses never leave the building. - Asks every source at once. AbuseIPDB, AlienVault OTX, GreyNoise, ThreatFox and URLhaus, plus offline datasets the server keeps: Feodo Tracker, FireHOL, the ThreatFox dump, CISA’s known exploited vulnerabilities, NVD and GeoLite2. One task per indicator and source, in parallel, each with its own timeout, so a vendor that is down degrades the result instead of failing it.
- Scores each indicator, with the arithmetic shown.
- Writes the ticket. Severity and priority, a summary, the evidence from each source in its own words, the MITRE ATT&CK techniques, and a containment checklist. Markdown or JSON, or filed straight into Jira or ServiceNow.
A score you can argue with
The part I care most about is the scoring, because a verdict nobody can explain is a verdict nobody should act on.
Each source’s answer is normalised to a signal between 0 and 1. The score is the larger of two numbers:
- a weighted mean of the sources that answered. A source that timed out or has no key is left out, not counted as “clean”, so a dead API cannot quietly lower a verdict;
- an authority floor: the strongest single hit, times how much that source is trusted. One confirmed C2 listing on abuse.ch still reads critical when four quieter sources shrug.
Then analyst judgement applies. A GreyNoise “benign” (Shodan, Censys, research scanners) damps the score. The workspace’s allow list forces 0 and its block list forces at least 90. Confidence is reported separately from the score, because “85 from one source” and “85 from five” are different instructions to an analyst.
Every indicator in the console opens to the bar per source, what each vendor said, and a How was this scored? dialog that prints the sum. The weights are configuration, and the API publishes them.
From a workbench to a team’s tool
Version 1 was a single-user tool: one shared API token, and a console that showed sample data. A SOC is a team, though, and a team’s alerts are the most sensitive data it has. Version 2.0 is the work of making IntelPulse a self-hosted web app a whole SOC can sign in to, without making it a liability. It took four milestones and about two dozen pull requests.
Accounts and workspaces. You sign up as yourself or as your organization, and every case, indicator, setting, token and audit record belongs to one workspace. Another workspace’s ID is a plain 404. One test calls every route with no caller and expects a refusal; others call the routes that take an ID from a second workspace and expect nothing back.
A team’s workflow. Admins invite members and set roles. Every case has a status, an assignee and a closing reason. The queue orders open cases by priority, and a SIEM or an EDR can push alerts in with an API token that can do nothing else.
Alert text, sealed and then gone. What a case was made from (the paste, the uploaded file, a pushed alert’s description and evidence) is sealed with AES-256-GCM. The case’s own ID is part of the associated data, so a value copied onto another case’s row does not open. After the workspace’s retention period, 30 days unless an admin sets 1 to 365, the text is deleted and the verdict and indicators stay. An admin can hold a case to keep it. Rotating the key re-seals everything in one transaction, or nothing.
An audit log that can be checked. Every change goes into a hash chain, and audit verify shows a record that was edited, removed or reordered. It has no secret in it, so someone with write access to the database could rewrite it end to end. That caveat is written down in the project rather than left for someone to find.
Get Vinit Rami’s stories in your inbox
Join Medium for free to get updates from this writer.
Nothing from anyone else. The console’s fonts are bundled. There are no analytics, no third-party scripts and no CDN, and the content security policy allows none. What leaves the server is the indicator itself, sent only to the sources a workspace turned on.
One more thing: a server in one command
A self-hosted tool is only as good as the evening it takes to host it. IntelPulse 2.0 ships a deploy kit. ./deploy/setup.sh asks for your address and a Cloudflare Tunnel token, writes the secrets, and starts Postgres, Redis, the app, its worker and beat, and the tunnel. Then it prints a one-time code that makes the owner’s account. The machine needs no open inbound port.
The containers run read-only, with every Linux capability dropped. There are scripts for backups, restores and updates, and a smoke test that signs an account up, triages a sample and checks the verdict against any address.
What shipping taught me that the tests could not
The test suite was green the whole way: 647 backend tests, plus Node tests and five Chromium suites, 1,144 checks on every pull request. I still found the most important bugs by doing what a user would do.
Running it on Postgres. The tests use SQLite. The first time I ran the deploy kit on a real Postgres, the first sign-up returned a 500. The migration that created the default organization inserted it as workspace 1 with the ID given. Postgres does not move a SERIAL column’s sequence for an ID you give it, so the next workspace asked the sequence, got 1 again, and hit the primary key. SQLite has no sequences and never saw the problem. One migration fixed it. The lesson was bigger: CI now runs the deploy kit itself, on Docker and Postgres, on every pull request.
A fresh install. The same run showed the worker and beat crashing at start. A minor release of kombu, Celery’s messaging library, had moved a dependency of its Redis transport under an optional extra. My pins had not changed, but a fresh install no longer brought it in. It is a one-word fix (celery[redis]) and an outage you only meet on a clean machine.
Writing the privacy page. To say honestly what the browser keeps, I read what the browser keeps. On a server, the workbench was still saving its recent cases (titles, verdicts, indicators) to local storage, the way the demo does. That outlived signing out, and the next person on the same browser could see it. Writing the documentation found a privacy bug.
Reading the code as an attacker. The review round before release found ten more issues. Two stand out:
- The forwarded address. Behind a proxy, the rate limiter trusted the first
X-Forwarded-Forentry. Proxies append to that header rather than replace it, so the first entry is whatever the client sent. Every request could claim a fresh address. - The email change. Changing your password asked for the current one, but changing your email did not. A stolen session could point the reset link at its own mailbox and keep the account long after the session ended.
All ten are fixed, most of them pinned by a test that fails on the old code, and written up with what is still open in docs/SECURITY-REVIEW.md.
None of these was exotic. Each was found by running the thing the way it will really be run, or by writing down what it really does and checking that the sentence was true.
By the numbers
- 100% precision and recall extracting 9,325 indicators, on the project’s own 3,400-line, 15-format test corpus. The corpus being the project’s own is the main caveat, and the benchmark document says so.
- 8 intelligence sources, checked in one pass.
- 1,144 automated checks on every pull request.
- 23 security findings found and fixed across two review rounds: 13 in September’s audit, 10 in this one.
Try it
The live demo runs entirely in your browser, on synthetic data, with nothing to install: vinitrami-soc.github.io/intelpulse. Pick a sample alert and open How was this scored?.
To run it for your team, the code and the deploy guide are on GitHub: github.com/vinitrami-Soc/intelpulse. It is MIT-licensed.
Next on the list: more sources (MalwareBazaar, VirusTotal, Shodan InternetDB, EPSS), notifications to Slack and Teams, STIX 2.1 and MISP export, and a handoff from PhishHawk, my phishing email triage tool, so a reported email’s indicators land in IntelPulse as a case.
If you work in a SOC and IntelPulse gives you a verdict you think is wrong, please tell me. That is the most useful bug report there is.
Vinit Rami builds defensive security tools with an offensive-security background. IntelPulse and PhishHawk are open source on GitHub.










