Leçon 3.6 · Triage statique· 45 min
Writing a Triage Report
Turn static triage into a report people can act on: verdict and confidence, ATT&CK-mapped capabilities, defanged IOCs, detections and next steps.
Cette leçon n’est disponible qu’en anglais pour le moment.
Objectifs
- Structure a triage report around the decisions its readers must make, leading with the bottom line
- State verdicts with explicit confidence levels and keep observed facts separate from inference
- Map capabilities to MITRE ATT&CK technique IDs and present host and network IOCs in defanged form
- Label reports for sharing with TLP and know when to export IOCs as STIX or MISP
- Run a time-boxed triage and review your own report against a rubric
Triage is only finished when someone else can act on it. You may have identified the file, read its strings and imports, measured its entropy and written a YARA rule, but if that knowledge stays in your terminal history, the SOC still does not know whether to isolate the host, the detection team has no rule to deploy, and the next analyst will redo your work from scratch.
A triage report is the product of the first stage of the analysis funnel from What Is Malware Analysis?. It is short, it is written in hours rather than days, and it exists to support decisions. This lesson covers what goes into one, how to phrase what you know and what you only suspect, and how to share it. It ends with a complete example report and a timed lab.
A report exists to support decisions
Every triage report answers four questions, in this order of urgency:
- Is it malicious? Or benign, or potentially unwanted, or unknown.
- What does it do? Its capabilities, at the level of detail triage allows.
- What do we block and hunt for? IOCs and detection content.
- Does it need deeper analysis? And if so, which questions remain.
If a reader cannot find the answer to question 1 in the first ten seconds, the report has failed, however good its technical content.
Write for your readers
Different people read different parts of the same report:
| Reader | Wants to know | Reads |
|---|---|---|
| SOC / incident response lead | Contain? Which hosts? How urgent? | Summary, verdict, recommended actions |
| Detection engineer | What to deploy, and how noisy it will be | Detection content, IOCs, capabilities |
| Threat hunter | What to search for in historical data | IOCs, ATT&CK techniques |
| CTI analyst | Known family? Links to other activity? | Identification, similarity hashes, notes |
| Reverse engineer (next stage) | Where to start, what is already done | Open questions, analyst notes, tooling |
| Management | Risk and impact in plain language | Summary only |
So the non-technical answer comes first, then progressively more technical sections, each usable on its own.
Anatomy of a triage report
Executive summary (BLUF)
Bottom line up front. Two to five sentences: what the file is, the verdict with its confidence, the one or two capabilities that matter, and the most important action. No tool names, no hex, no jargon a manager would have to look up. Write it last, put it first.
Verdict and confidence
One line each: the verdict (malicious, suspicious, potentially unwanted, benign, unknown), the family if identified, and the confidence. Then a short justification that points at the evidence sections below.
File identification
A table with everything needed to find this exact file again: file names seen, size, type, MD5, SHA-1, SHA-256, and the similarity hashes from Identifying and Hashing Files (imphash, ssdeep or TLSH). Add where the file came from (host, path, email, ticket), when it was collected, and the compile timestamp with a note that it can be forged.
Capabilities mapped to ATT&CK
List what the sample can do, each capability with its evidence and a
MITRE ATT&CK technique ID such as T1140 or
T1071.001. ATT&CK IDs give hunters and detection engineers a shared
vocabulary, and let a CTI team compare this sample with others. Capa output
from Reading Capabilities from Imports is a good
starting point, but every mapping you publish is your claim: check it.
Map only what the evidence supports, and say what kind of evidence it is. An
import of CreateRemoteThread is evidence of a capability, not proof the
sample uses it.
Indicators of compromise
Split IOCs by where a defender will look for them:
- Host indicators: hashes, file paths and names, mutexes, named pipes, registry keys, service names, scheduled task names, PDB paths.
- Network indicators: domains, IPs, URLs, user-agents, URI patterns, TLS certificate or JA3/JA4 fingerprints.
Give each IOC a type, a value, and a short context note: "mutex created at startup, likely single-instance check" is far more useful than a bare string. Mark IOCs that were only seen statically, since they may be unused or decoys.
Defang network indicators so nobody clicks or auto-resolves them, and so mail filters and chat clients do not turn them into live links:
| Live form | Defanged form |
|---|---|
http://update.example.com/gate.php | hxxp://update[.]example[.]com/gate[.]php |
https://cdn.example.net | hxxps://cdn[.]example[.]net |
203.0.113.7 | 203.0.113[.]7 |
ops@example.org | ops[@]example[.]org |
Defang in prose and tables meant for humans; provide a machine-readable export (see below) with the original values for tools.
Detection content
The YARA rules from Writing Your First YARA Rules, plus anything else you produced: Sigma, EDR queries, Suricata signatures. For each rule, state what it was tested against (variants, goodware corpus) and its expected noise. An untested rule should say so.
Recommended actions
Concrete, owned and ordered: block these hashes, deploy this rule in alert-only mode for a week, search for this mutex across the fleet, reimage this host. Match the actions to the verdict and confidence; a low-confidence verdict should not trigger fleet-wide blocking.
Open questions and next steps
What triage could not answer, and the analysis that would answer it: "The
second-stage payload is encrypted in RT_RCDATA; dynamic analysis or reversing
the decoder at 0x401A20 is needed to recover it." This section is the hand-off
to the next stage of the funnel, and it keeps you honest about the limits of
triage.
Analyst notes and tooling
Who analysed it, when, how long it took, which tools and versions were used, and anything odd you noticed but could not place. Tool versions matter: capa rule sets and YARA modules change, and a reader must be able to reproduce your results.
Saying how sure you are
"This is malware" and "this might be malware" lead to very different actions, so confidence must be explicit and use consistent words. A common convention, borrowed from intelligence analysis:
| Phrase | Meaning | Typical basis |
|---|---|---|
| We assess with high confidence | Judgement is well supported; it could still be wrong, but that would be surprising | Multiple independent observations that agree; family confirmed by code, not just strings |
| We assess with moderate confidence | Credible and plausible, but not corroborated enough for high confidence | One strong indicator, or several weaker ones |
| We assess with low confidence | Plausible, but evidence is fragmentary or could have other explanations | A single string, a reputation hit, similarity only |
Attach confidence to judgements, not facts. "The file imports
WriteProcessMemory" needs no confidence level; it is observed. "The sample
injects into other processes" is a judgement and does.
Observed versus inferred
The most common triage-report error is presenting an inference as an observation. Keep them visibly separate:
| Observed (fact) | Inferred (assessment) |
|---|---|
The string LabAgent/1.0 (...) is present in .rdata | The sample probably uses this user-agent for HTTP traffic |
| No networking DLLs are imported | Network functions, if any, are resolved dynamically or absent |
Sections are named UPX0, UPX1, UPX2, and UPX1 has entropy 7.7 | The sample is packed with UPX |
| Its imphash matches three samples in our repository | It may share a build environment with them |
Words like observed, present, imports, contains mark facts. Assess, likely, suggests, consistent with mark inference.
Tip: When static evidence contradicts an assumption, say so. "A user-agent string is present, but no networking imports were observed; the string may be unused or network APIs may be resolved at runtime" is a perfectly good triage finding, and it tells the next analyst exactly what to check.
Sharing: TLP, STIX and MISP
Label every report with the Traffic Light Protocol (TLP 2.0, maintained by FIRST) so recipients know how far they may pass it on:
| Label | May be shared with |
|---|---|
TLP:RED | Named recipients only |
TLP:AMBER+STRICT | The recipient's organisation only |
TLP:AMBER | The recipient's organisation and its clients, on a need-to-know basis |
TLP:GREEN | The recipient's wider community, not publicly |
TLP:CLEAR | Anyone, subject to copyright |
Put the label at the top of the report and on any exported data. Internal
reports about an ongoing incident are usually TLP:AMBER or stricter; a
sanitised write-up of a commodity family may be TLP:CLEAR.
For machine consumption, IOCs travel as structured data rather than prose. STIX 2.1 (an OASIS standard) models indicators, malware, relationships and ATT&CK patterns as JSON objects, usually exchanged over TAXII. MISP is a widely deployed open-source sharing platform with its own event and attribute format and STIX import/export. Your report links to the event or bundle; the prose report stays the human-readable authority.
A report template
Copy this skeleton and fill it in. Every heading stays, even if the content is "none observed", so readers know you checked.
TLP:<LABEL>
# Triage report: <file name or family> (<report ID>)
Analyst: <name> | Date: <YYYY-MM-DD> | Time spent: <minutes>
## 1. Executive summary
<2-5 sentences, plain language, verdict + confidence + key action>
## 2. Verdict
Verdict: <malicious | suspicious | PUA | benign | unknown>
Family: <name or "unidentified">
Confidence: <low | moderate | high>
Rationale: <1-3 bullets pointing at sections 4-5>
## 3. File identification
| Field | Value | (names, size, type, MD5, SHA-1, SHA-256, imphash,
ssdeep/TLSH, compile time, source, collection time)
## 4. Capabilities
| Capability | Evidence (observed / inferred) | ATT&CK |
## 5. Indicators of compromise
### Host | Type | Value | Context |
### Network | Type | Value (defanged) | Context |
## 6. Detection content
<rules, what they were tested against, expected noise>
## 7. Recommended actions
<ordered, concrete, with owners>
## 8. Open questions and next steps
## 9. Analyst notes and tooling
<tools and versions, environment, anything unexplained>Example report: LabImplant v1 (fictional)
Warning: Everything in this section is a fictional example written about the benign training program built in Writing Your First YARA Rules. There is no real incident, host, or threat actor. The hashes are from one build of that program; yours will differ.
TLP:CLEAR · Report TR-2026-0929-01 · Analyst: A. Analyst · 2026-09-29 · Time spent: 30 minutes
1. Executive summary
A file named lab_implant.exe, found in the malware-analysis training lab, was
triaged statically. We assess with high confidence that it is not
malicious: it is a training program that contains strings resembling implant
artefacts (a custom user-agent, a mutex name, a named-pipe name and a
configuration string) but has no networking, persistence or injection
capability. No containment is required. A YARA rule has been produced so that
lab copies are recognised and not escalated as real incidents.
2. Verdict
- Verdict: Benign (training artefact)
- Family: None; internal lab program "LabImplant"
- Confidence: High
- Rationale: The import table contains only C runtime and
KERNEL32.dllfunctions linked by the mingw-w64 runtime (section 4); the implant-like strings are not referenced by any API that would use them; the code decodes a 5-byte blob to the textdemo!and prints its strings.
3. File identification
| Field | Value |
|---|---|
| File name | lab_implant.exe |
| Size | 17,408 bytes |
| Type | PE32+ executable (console), x86-64, stripped to external PDB |
| MD5 | de4d02e98a521e93dcfdb01c6a5025e9 |
| SHA-1 | 625d683cad6ccd138c7cdce4069ffa1bf9690988 |
| SHA-256 | a603a2f93afc7ecdf9f88d5f6c05055b9cfcfa21074df4088ad527980b240495 |
| Imphash | 3659d62105a06dba325698c81e1cd172 (shared by trivial mingw-w64 console programs; not distinctive) |
| Compile timestamp | 2026-09-29 13:21:24 UTC (header value; forgeable) |
| PDB name | lab_implant.pdb |
| Sections | 10, standard mingw-w64 layout; .text entropy 5.88 |
| Packer | None detected; whole-file entropy 4.71 |
| Source | Training lab build directory, collected 2026-09-29 |
4. Capabilities
| Capability | Evidence | Type | ATT&CK |
|---|---|---|---|
Decodes an embedded blob with a position-dependent XOR key (0x5A + i) | Decoder loop in .text; 5-byte blob in .data decodes to demo! | Observed (static) | T1140 Deobfuscate/Decode Files or Information |
| Stores data in encoded form | Same 5-byte blob | Observed (static), trivial scale | T1027 Obfuscated Files or Information |
| HTTP communication | User-agent string present; no WinINet, WinHTTP or Winsock imports; no GetProcAddress import | Not supported by evidence | (T1071.001 would apply if confirmed) |
| Single-instance check via mutex | Mutex name string present; no CreateMutex import | Not supported by evidence | None |
VirtualProtect, VirtualQuery and Sleep appear in the import table. They
are imported by the mingw-w64 runtime startup code, not by the program's own
functions, and are not treated as evidence of capability.
5. Indicators of compromise
Host indicators
| Type | Value | Context |
|---|---|---|
| SHA-256 | a603a2f93afc7ecdf9f88d5f6c05055b9cfcfa21074df4088ad527980b240495 | This build only |
| Mutex name (string) | Global\LabDemoMutex | Present as ASCII string; never created |
| Named pipe (string) | \\.\pipe\lab-demo-pipe | UTF-16LE string; never opened |
| Config marker | LABCFG{interval=3600;jitter=20;mode=demo} | Printed at runtime |
| PDB name | lab_implant.pdb | CodeView debug record |
Network indicators
| Type | Value | Context |
|---|---|---|
| HTTP user-agent | LabAgent/1.0 (Windows NT 10.0; lab-build) | String only; no network capability observed |
| Domains, IPs, URLs | None observed | Checked strings, FLOSS output and resources |
6. Detection content
MAL_Win_LabImplant_Strings version 1.2 (full rule in the YARA lesson).
Tested: matches the v1 (-O0) and v2 (-O2, version 1.1) builds; no
matches in a scan of /usr/bin with the header check disabled; does not
match the UPX-packed copy (SHA-256
4b956e927c41f7c4d9499840733f8cc44bad66b5893309d2d5d56d5dc92e9532), which is
detected only by the generic SUSP_PE_FewSections_NoText hunting rule.
Expected noise: none known.
7. Recommended actions
- Lab team: add the rule to the lab scanner with the tag
training, so lab copies are auto-closed rather than escalated. - SOC: no action on production systems.
- Detection engineering: use this sample and rule as the regression test for the YARA test harness.
8. Open questions and next steps
- Does any future build import networking or synchronisation APIs? If so, re-triage: the verdict rests on their absence.
- The UPX-packed variant is not covered by the string rule. If packed builds are distributed, add a memory-scan deployment of the rule or a rule for the unpacked image.
9. Analyst notes and tooling
Static analysis only; the sample was not executed. Tools: file, strings,
FLOSS, Python pefile, x86_64-w64-mingw32-objdump, YARA 4.5.4, Detect It
Easy. Analysis host: isolated lab VM, no network.
Lab: a 30-minute timed triage
Take one of the binaries you built in this module: build/v2/lab_implant.exe,
the UPX-packed lab_implant_upx.exe, or a variant from the hashing, strings,
imports or packer labs. If you want an honest test, ask a colleague to pick one
and rename it sample.bin. Start a timer and work through the checklist; stop
at 30 minutes whatever the state, because triage is defined by its time box.
-
Work through the checklist, writing findings straight into the template:
Minutes Step Tools Record in report 0–3 Hash and identify the file; note size and real type sha256sum,file, Detect It EasySection 3 3–5 Reputation lookup by hash only; check your own repository VirusTotal search, internal repo Section 3, notes 5–9 Headers and sections: timestamp, section names, sizes, entropy pefile, PE-bear Section 3; packer yes/no 9–12 If packed: identify the packer, note it, decide whether to unpack now Detect It Easy, upx -tSections 2 and 8 12–17 Strings: ASCII, UTF-16 and decoded, including stackstrings and XOR-encrypted strings; shortlist artefacts strings, FLOSSSection 5 candidates 17–21 Imports and capabilities; separate runtime noise pefile, capa Section 4 21–23 Resources and overlay pefile, Resource Hacker Sections 4 and 5 23–27 Draft and test a YARA rule against the sample and a goodware folder yara -sSection 6 27–30 Verdict, confidence, summary, next steps Your judgement Sections 1, 2, 7, 8 -
Fill every template heading. Use "none observed" rather than deleting a section.
-
Label every statement in sections 4 and 5 as observed or inferred, and attach a confidence level to the verdict.
-
Defang every network indicator, then swap reports with a colleague (or wait a day) and score it with the rubric below.
Self-review rubric. Score each line 0 (missing), 1 (partial) or 2 (done); aim for 16 or more out of 20.
| # | Criterion | 2 points means |
|---|---|---|
| 1 | Bottom line up front | Verdict and main action are in the first two sentences, without jargon |
| 2 | Explicit confidence | The verdict uses low / moderate / high and the rationale supports that level |
| 3 | Complete identification | All three cryptographic hashes, one similarity hash, size, type, source |
| 4 | Facts versus inference | Every capability is marked observed, inferred or not supported |
| 5 | ATT&CK mapping | IDs are correct, current and backed by cited evidence |
| 6 | Useful IOCs | Split into host and network, each with type and context |
| 7 | Defanging and TLP | All network IOCs defanged; TLP label at the top |
| 8 | Tested detection | Rule tested on the sample, a variant or packed copy, and a goodware set, with results stated |
| 9 | Actionable recommendations | Concrete, ordered, proportionate to confidence |
| 10 | Honest limits | Open questions name what triage could not answer and how to answer it; tools and versions listed |
Questions to answer: Did you finish in 30 minutes, and which step overran? Which of your capability claims would change if the sample turned out to resolve its imports at runtime? If you triaged the UPX-packed copy, what could you honestly say about its capabilities, and at what confidence? Would your executive summary make sense to someone who has never heard of a PE file?
Key takeaways
- A triage report supports decisions: is it malicious, what does it do, what do we block and hunt, does it need deeper analysis.
- Lead with the bottom line and a verdict with explicit confidence; order the rest from least to most technical so each reader finds their part.
- Keep observed facts separate from inferences, and attach confidence levels to judgements, not to facts.
- Map capabilities to ATT&CK only where evidence supports them, split IOCs into host and network, and defang every network indicator.
- Include tested detection content, concrete recommended actions, open questions and tool versions, and label the report with TLP before sharing it or its STIX/MISP export.