YARA Rule Best Practices

How to write a YARA rule that stays precise: string length, file-type guards, naming, and what to change when a rule matches far too much.

A good YARA rule does two things: it matches the family it was written for and almost nothing else, and it can be read a year later by someone who was not there. Everything on this page serves one of those two properties. If you have not written a rule yet, Writing Your First YARA Rule comes first; this page is what you reach for when a rule is working but wrong, or working and nobody can tell why.

The one sentence to keep: a rule that matches everything is worse than no rule, because it spends your team's attention permanently and teaches them that rule matches are noise.

How long should my strings be?

Longer than feels necessary. A short string matches by accident, and accidents at corpus scale are not rare events.

"FNoC" looks distinctive and is not. Four characters turn up inside compressed data, inside base64, and inside the middle of unrelated binaries, so the rule fires on files that have nothing to do with your target. "FNoC3haB" is long enough that arbitrary data does not produce it. As a rule of thumb, treat anything under about six bytes as a string that cannot carry a rule on its own.

When a short string is genuinely the signal, do not give it any of them. Require it alongside something else: a file-type guard, a size bound, and a second string. Precision comes out of the combination, not out of the individual pattern.

How do I use a regular expression without making the rule useless?

Give it fixed text to anchor on. A regular expression with no literal substring in it has nothing to look for, so it gets evaluated over the whole file and it matches a great deal of arbitrary data.

strings:
    // Nothing fixed to look for. Avoid.
    $bad  = /[a-zA-Z0-9\s]{5}/

    // The literal part is what makes this workable.
    $good = /exdl_cfg_[0-9]{5,43}/

In the second case exdl_cfg_ is a fixed sequence, and the variable part is only considered where that sequence appears. That is the difference between a regular expression you can put in a production rule and one you cannot.

This is not only advice. Stairwell will not save a rule carrying a pattern it judges too slow to run, and an unanchored regular expression is the usual cause. The editor reports it as a blocking diagnostic with the line number, so you will find out before you can do any damage with it.

How do I keep a rule inside the right file types?

Test the file's type in the condition, every time. It is the cheapest precision available and it is the guard most missing rules were missing.

// Windows executable, from the MZ header
uint16(0) == 0x5A4D

// ELF binary
uint32(0) == 0x464C457F

// Mach-O, thin and fat, both byte orders
uint32(0) == 0xFEEDFACE or uint32(0) == 0xCEFAEDFE or
uint32(0) == 0xFEEDFACF or uint32(0) == 0xCFFAEDFE or
uint32(0) == 0xCAFEBABE or uint32(0) == 0xBEBAFECA

The modules give you the same tests in a more readable form, and they need no import line: pe.is_pe, elf.type == elf.ET_EXEC, dotnet.is_dotnet. Pick one style and stay with it across your rule set so that a reader is not decoding a different idiom on every rule.

Exclusions work the same way. If you know the target is not a Windows executable, say so with uint16(0) != 0x5A4D rather than leaving the rule open to every file type in the estate.

Prefer several narrow rules over one broad one. If your detection idea applies to both a PE and an ELF build, write two rules rather than one rule with an or between two file types. Each is easier to read, each can be turned off independently when one turns out noisy, and the match counts tell you which platform is actually affected.

Should I use nocase and wide?

Use wide freely and nocase rarely.

wide is often necessary rather than optional. Windows programs frequently store strings as two bytes per character, and a plain text string will not match that form at all. Use ascii wide when you do not know which form the target uses.

nocase is the modifier that quietly ruins rules. Consider $a = "KeRnEl32.dLl". That exact mixed casing is genuinely unusual and worth matching on. Add nocase and you are now matching every casing variant, including the ordinary kernel32.dll that appears in nearly every Windows executable ever built. The distinctive thing about the string was the casing, and nocase is precisely the instruction to throw it away.

Reach for nocase when the case really does vary between builds and the string is long enough to survive the loss, and leave it off otherwise.

What do I do when a rule matches far too much?

Look at what it is matching before you change anything. In the Rules list, click the rule's My objects count to open the matching files in search. Nine times out of ten the answer is visible in the first screen: one common string, one file type you did not intend, or one vendor's software that shares an artifact with your target.

Then apply the smallest change that fixes it, in roughly this order:

  1. Add or tighten the file-type guard. The most common cause of a runaway rule is no guard at all.
  2. Bound the size. filesize < 500KB removes every large installer that happens to embed your string. For a rule that has to cover a wide range, splitting it into size bands (one rule under 1 MB, one from 1 to 5 MB) keeps each rule's match count reviewable.
  3. Raise the bar in the condition. Move from any of them to 2 of them, or from 2 of them to 3 of them. This is usually the highest-value single edit available.
  4. Replace the weakest string. If one of your strings is doing all the false matching, a longer or more specific replacement fixes the rule without restructuring it.
  5. Exclude signed software. pe.signatures.len() == 0 limits the rule to unsigned files, which removes most legitimate commercial software in one clause. The older pe.number_of_signatures == 0 still works and the editor flags it as deprecated, so prefer the first form in new rules.
  6. Split the rule. Two narrow rules that each match one thing beat one rule that matches both plus a hundred others.

If the rule is matching legitimate software that you cannot exclude by any of the above, the problem may not be the rule. Remember that a rule matching a dual-use tool has produced a match, not a detection, and the right response can be to keep the rule and handle the tool through opinions rather than to weaken the rule until it catches nothing.

While you are working on a rule, turn it off rather than deleting it. You keep the body and the version history, and you stop the noise in the same click.

How should I name a rule?

So that the name alone tells an analyst what the rule claims. The name is on every match, on every notification, and in every export, and it is the only part of the rule most readers will ever see.

Use one convention across your whole rule set. Two that work well in practice:

ConventionExamples
ACTOR_MALWARE_ROLE_FILETYPE_DETAILAPT41_DEADEYE_Backdoor_PE_Strings, DARKSIDE_Ransomware_PE_Imphash
Methodology_TECHNIQUE_DETAIL_FILETYPEMethodology_XOREncoding_DOSStrings_PE, Methodology_RemoteTemplates_URIRegex_RTF

Both put the most general term first, which means an alphabetical sort groups related rules together. That is not incidental: the Rules list sorts by name, and a convention that clusters a campaign's rules into adjacent rows is worth more than a convention that reads better in isolation.

Rule names cannot contain spaces. Changing the name in the rule body renames the rule by creating a new one and deleting the old, so settle on a convention before you have two hundred rules rather than after.

What belongs in the metadata?

Enough that the rule explains itself. author, date, description, the malware family, and a reference link to the report or the analysis it came from. None of it affects matching, and all of it affects whether anyone trusts the rule at 2am.

Two entries do real work beyond documentation:

  • Hashes. A metadata entry whose key starts with hash, sha256, sha1, or md5 and whose value is exactly one hash records that file as a reference file for the rule. Reference files and the files that resemble them are looked at sooner than the ordinary queue, and the hashes are offered to you in Test Scan as the Must match list. Use one entry per hash, and put nothing but the hash in the value.
  • Comments. // comments are allowed inside the rule body, and a condition clause that is not self-evident deserves one:
condition:
    uint32be(0) != 0x52617221 and  // exclude Rar! archives
    filesize < 2MB and
    2 of them

Since rules cannot reference each other, how do I share logic?

Inline it, and use naming and tags to carry the relationship instead.

Every rule in Stairwell stands alone: a condition can refer to its own strings and to the available modules, and not to another rule. So the shared helper rule that several detections reference has to become a clause copied into each of them. Three things make that less painful than it sounds:

  • Keep a template per file type. A skeleton with your metadata keys and your standard file-type and size guards already in place means the copied clause is written once and reused by hand rather than rewritten each time.
  • Tag the family. Rule tags group rules across rule sets, and rule.tag is queryable, so the twelve rules that used to share a helper can still be selected and enabled or disabled together.
  • Name them into a cluster. A shared prefix does most of what a shared helper rule did, from the reader's point of view.

The payoff for the duplication is real: any rule can be read, tested, disabled, exported, or handed to a peer at another organization without dragging a dependency behind it.

Which diagnostics block a save, and which do not?

The editor lists both kinds, each anchored to its line, and tells you which you are looking at.

  • Errors block the save. Syntax problems, a missing condition, more than one rule in the editor, and a rule marked global or private.
  • One warning is treated as an error: a pattern too slow to scan with. Anchor it or replace it.
  • Every other warning is advisory. The rule saves. Read them anyway, because they are usually pointing at something that will matter later, such as a hex pattern that would be clearer written as text, or a deprecated module field.

There is no reason to save a rule with unread diagnostics. They are the cheapest review you will get.

How do I tell whether a rule is behaving after I save it?

Read the two count columns in the Rules list together. High Malware objects with low My objects means the rule recognizes the family and your estate is clean of it, which is a rule doing its job. High My objects with near-zero Malware objects means the rule found something ordinary in your own software, and it wants tightening.

Also check for a warning icon on the rule. A rule that matched too many known-good files is excluded from scanning and says so, which is the platform telling you that the condition is too broad before your team has to.

What should I read next?

  • Manage YARA Rules, for turning a noisy rule off, editing it, and reading its match counts.
  • YARA Rule Feeds, whose rule bodies are worth reading as worked examples of most of the advice above.
  • Prevalence, because rarity is the context that makes a broad rule's matches triageable.

Did this page help you?