Regex for Security Analysts: Patterns You'll Use Every Week
A practical regex cookbook for log analysis and IOC extraction: IPs, domains, URLs, hashes, base64 and defanged indicators, plus the mistakes that quietly break your matches.
Why regex is a core analyst skill
Regular expressions are the duct tape of security work. You use them to pull indicators out of a log dump, grep a packet capture, filter a SIEM query, and clean up a messy threat report before it goes into your pipeline. You do not need to be a regex wizard. You need a dozen patterns you trust and an understanding of where they break.
This is that cookbook. Every pattern below is written to be readable rather than maximally clever, because a regex you can debug at 2am beats one that is theoretically perfect.
You can build and test all of these in the browser with the free regex tester on mlab.sh, which highlights matches and capture groups as you type. When you just want the indicators out of a blob of text, the IOC extractor does the common patterns for you. Test against real data before you trust a pattern in production.
IPv4 addresses
The pattern everyone writes first, and the one everyone writes wrong first:
\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b
This matches 192.0.2.10, but it also happily matches 999.999.999.999, which is not a valid address. For quick log triage that is usually fine, because junk like that rarely appears. When you need correctness, constrain each octet to 0 to 255:
\b((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\b
That is uglier but only matches real octets. Pick based on the job: the loose version for a fast eyeball pass, the strict version for anything feeding a blocklist. Sample data throughout this article uses the RFC 5737 documentation range (192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24) so nothing here matches a live host.
IPv6 addresses
Full IPv6 matching is painful because of the :: compression rule. A pragmatic pattern that catches most real addresses without trying to be a full validator:
\b(?:[A-F0-9]{1,4}:){2,7}[A-F0-9]{1,4}\b
Add the i (case-insensitive) flag so it catches both 2001:DB8::1 and 2001:db8::1. For strict validation, reach for a purpose-built library rather than a monster regex. This is a good rule generally: when the format has complex rules, regex is for extraction, not validation.
Domains and hostnames
\b(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}\b
This handles labels of 1 to 63 characters, allows hyphens inside labels but not at the edges, and requires a TLD of at least two letters. It matches mail.example.com and sub.domain.example.org. Be aware it will also match things that look like domains but are filenames, such as report.final.pdf. If your text mixes both, filter the results against a known TLD list afterwards.
URLs
\bhttps?://[^\s"'<>)\]]+
Deliberately loose. It grabs everything from http:// or https:// up to the first whitespace or common delimiter. Trying to fully parse a URL with regex is a losing game because of query strings, fragments, encoded characters and unusual schemes. Extract greedily, then parse the result with a real URL parser.
The exclusion set at the end matters. Without it, a URL at the end of a sentence pulls in the trailing period or bracket, and a URL inside quotes swallows the closing quote.
File hashes
Hashes are fixed-length hex, which makes them the easiest and most reliable IOCs to match. Anchor on length with \b boundaries so you do not clip a longer string.
\b[a-fA-F0-9]{32}\b # MD5
\b[a-fA-F0-9]{40}\b # SHA-1
\b[a-fA-F0-9]{64}\b # SHA-256
One gotcha: a 64-character hex string could be a SHA-256 or could be something else entirely, like a hex-encoded key. Length alone is a strong hint, not proof. Also watch that a longer hex blob does not get partially matched; the word boundaries handle the common cases but not hex embedded in a larger hex string.
Email addresses
A full RFC 5321 compliant email regex is monstrous and nobody uses it in practice. This covers essentially all real addresses:
\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b
It matches [email protected] and [email protected]. It will reject some technically valid but exotic addresses. That trade is almost always worth it for log analysis.
Base64 blobs
Encoded payloads, embedded certificates and obfuscated commands often show up as base64. The challenge is that base64 uses A-Z a-z 0-9 + / with = padding, which overlaps heavily with normal text. A length threshold cuts the noise:
\b[A-Za-z0-9+/]{40,}={0,2}
The {40,} says "only flag blobs of at least 40 characters," which filters out ordinary words. Tune the threshold to your data. Too low and you match everything; too high and you miss short encoded commands. When hunting encoded PowerShell, remember it is often UTF-16 base64, so decode and check for the tell-tale null bytes.
Defanged indicators
Threat reports and shared intel often "defang" indicators so they are not accidentally clickable: hxxp://evil[.]example[.]com, 192[.]0[.]2[.]10, evil(dot)example(dot)com. Your extraction has to handle these, and sometimes you need to refang them back.
Match a defanged IP:
\b\d{1,3}\[\.\]\d{1,3}\[\.\]\d{1,3}\[\.\]\d{1,3}\b
Refang in one pass with a set of replacements:
| Find | Replace |
|---|---|
[.] or (.) or (dot) |
. |
hxxp / hXXp |
http |
[://] |
:// |
[@] or (at) |
@ |
A quick refang in Python:
import re
def refang(text):
text = re.sub(r"\[?\.\]?|\(dot\)", ".", text)
text = re.sub(r"hxxp", "http", text, flags=re.IGNORECASE)
text = re.sub(r"\[://\]", "://", text)
return text
Handle defanging at the extraction stage so the same domain does not enter your pipeline in three different spellings. Consistency here prevents duplicate indicators downstream, a problem we touch on in the IOC lifecycle.
The mistakes that quietly break your matches
Unescaped dots. A bare . matches any character. 192.0.2.10 written as 192.0.2.10 in a pattern also matches 192x0y2z10. Escape literal dots as \. when you mean a real period.
Forgetting anchors. ^ and $ anchor to the start and end of a line. Without them, a pattern meant to validate a whole field will match it as a substring. Validating a hash field? Use ^[a-f0-9]{64}$, not the unanchored version.
Greedy quantifiers. By default .* and .+ are greedy: they grab as much as possible, then backtrack. In href="a" href="b", the pattern href="(.*)" captures a" href="b, not a. Use the lazy versions .*? and .+? when you want the shortest match, or better, exclude the delimiter with a character class like [^"]*.
Case sensitivity. Hex hashes, IPv6 and domains appear in mixed case. Add the i flag or your [a-f0-9] misses A1B2.
Performance basics
Regex can be fast or it can hang your process. The failure mode is catastrophic backtracking: a pattern with nested quantifiers on ambiguous input explores an exponential number of paths.
The classic trap looks like (a+)+ or (.*)*. Against input that almost matches, the engine tries every combination before giving up. Feed a hostile string to a vulnerable pattern and you have a denial of service (this class of bug is called ReDoS).
Three habits keep you safe:
- Avoid nested quantifiers where one quantified group contains another.
- Prefer specific character classes (
[^"]*) over.*so the engine has less to backtrack over. - Test patterns against large and adversarial inputs, not just the happy path, before deploying them where an attacker controls the text.
For extraction over big log volumes, a simpler pattern that you run in two passes often beats one clever pattern that backtracks.
Build a small trusted toolkit
You do not need to memorise these. Keep a snippet file with your tested patterns, drop new data into the regex tester before trusting a pattern, and let the IOC extractor handle the routine pulls so you can focus on the odd cases. The goal is not regex mastery. It is a handful of patterns you know cold and can debug under pressure.
Match loosely, validate strictly, and test against real data. A regex you understand beats a regex that is merely correct.