# NOVA rules on PromptIntel: the rule format and the Rule Lab

A NOVA rule detects one behaviour in text an AI reads: keywords for exact evidence, semantics for meaning, an LLM question for intent, and a condition that combines them. Every rule on PromptIntel has the same header, and every rule written with the Rule Lab is tested before it is handed over.

PromptIntel rule format, version 1. The engine is the open-source NOVA library (https://github.com/Nova-Hunting/nova-framework).

## The format

```
rule PascalCaseName
{
    meta:
        description = "One or two sentences: what the rule detects"
        author = "Name or @handle"
        version = "1.0.0"
        category = "group/threat"
        severity = "medium"
        uuid = "00000000-0000-4000-8000-000000000000"
        date = "YYYY-MM-DD"
    keywords:
        $a = "literal text"
        $b = /regular\s+expression/i
    semantics:
        $s = "what the text means" (0.45)
    llm:
        $confirm = "A yes/no question about intent. Answer no if the text documents, quotes or tests this." (0.7)
    condition:
        (keywords.$a or keywords.$b or semantics.$s) and llm.$confirm
}
```

Rule name: PascalCase, letters and digits only, starting with a capital, 3 to 100 characters (CurlPipeToShell).

The `meta` section holds exactly these keys, in this order. No other key is allowed.

| Key | | Format | Meaning |
| --- | --- | --- | --- |
| `description` | Required | one or two sentences, 20-700 characters; keep it under 300 | What the rule detects, as a behaviour: "Detects text that tells an agent to pipe a downloaded script into a shell". |
| `author` | Required | name or @handle, 2-80 characters | Who wrote the rule. |
| `version` | Required, filled for you | X.Y.Z | Version of the rule. A new rule is 1.0.0. |
| `category` | Required | group/threat, from the category list | What kind of attack it detects. One key, both parts. |
| `severity` | Required | low \| medium \| high \| critical | How bad a match is. |
| `uuid` | Required, filled for you | UUID v4, lower case | The rule's identity. It stays the same when the rule changes. |
| `date` | Required, filled for you | YYYY-MM-DD | The day the rule was first written. |
| `modified` | Optional | YYYY-MM-DD, not before date | The day of the last change. |
| `reference` | Optional | an https address or a report name, up to 300 characters | Where the attack is described. |
| `hash` | Optional | SHA-256, 64 lower-case hex characters | Hash of the sample the rule was written from. |
| `promptintel_url` | Optional | https://promptintel.securitybreak.io/prompt/<id> | The PromptIntel prompt the rule was written from. |

## Categories

`category` is one key holding `group/threat`, one of these values.

### Prompt manipulation

Attempts to override the model's instructions or get past its safety rules.

- `prompt_manipulation/direct_injection`: Explicit instruction to ignore the rules in force
- `prompt_manipulation/indirect_injection`: Instructions carried by content the model reads: a page, a document, a tool result
- `prompt_manipulation/jailbreak`: A persona or roleplay used to drop safety rules
- `prompt_manipulation/hidden_instructions`: Commands hidden in code, comments or markup
- `prompt_manipulation/translation_trick`: A transform (translate, decode, rephrase) used to bring a forbidden command back
- `prompt_manipulation/rag_poisoning`: Content planted in a retrieval source so that it is pulled into prompts
- `prompt_manipulation/feedback_loops`: Repeated prompts that use feedback or self-training to shift the model's behaviour

### Abusing legitimate functions

Using a model or an agent to carry out or scale an attack.

- `abusing_functions/disinformation`: Coordinated false narratives
- `abusing_functions/malware_generation`: Exploit code, loaders, obfuscators, payload builders
- `abusing_functions/reconnaissance`: Gathering technical or personal information on a target
- `abusing_functions/data_exfiltration`: Getting secrets or sensitive content out of memory, retrieval or connectors
- `abusing_functions/social_engineering`: Phishing and scam text
- `abusing_functions/crime_automation`: Scaling a criminal workflow with the model
- `abusing_functions/attack_enablement`: A model used inside malware or an offensive framework
- `abusing_functions/hijack_keys`: Models run on stolen credentials
- `abusing_functions/supply_chain`: Prompts planted in packages, libraries or CI artifacts
- `abusing_functions/training_poisoning`: Entries injected into training or fine-tuning data
- `abusing_functions/agentic_misuse`: Telling an agent to call tools, run code or act outside what it was asked
- `abusing_functions/credential_harvesting`: Extracting API keys, tokens or session data
- `abusing_functions/contextual_exfiltration`: Finding and extracting sensitive fields in structured data
- `abusing_functions/privacy_exfiltration`: Requesting personal data from connectors or memory

### Suspicious prompt patterns

Techniques that hide what a prompt asks for.

- `suspicious_patterns/encoding_obfuscation`: Payload hidden in Base64, hex or a rotation scheme
- `suspicious_patterns/unicode_tricks`: Homoglyphs, right-to-left override, zero-width characters
- `suspicious_patterns/chained_prompts`: Steps where one output seeds the next
- `suspicious_patterns/tunneling`: A request wrapped in a pretend scenario
- `suspicious_patterns/fragmentation`: An instruction split across messages
- `suspicious_patterns/token_perturbation`: Noise inserted in words to break simple filters
- `suspicious_patterns/cross_modal`: Instructions hidden in an image, audio or a PDF
- `suspicious_patterns/multi_agent_collusion`: Prompts across agents or sessions that combine into one attack
- `suspicious_patterns/telemetry_evasion`: Removing or hiding provenance, timestamps or identifiers

### Abnormal outputs

The model's output reveals or produces something it should not.

- `abnormal_outputs/system_prompt_leak`: The output reveals hidden instructions or configuration
- `abnormal_outputs/credential_leak`: API keys, tokens or passwords in the output
- `abnormal_outputs/pii_exposure`: Personal identifiers returned from context or memory
- `abnormal_outputs/document_disclosure`: Internal document text returned
- `abnormal_outputs/logic_disclosure`: The model reveals its safety rules or reasoning
- `abnormal_outputs/malicious_content`: Actionable illegal instructions in the output
- `abnormal_outputs/exploit_output`: A working exploit or ransomware in the output
- `abnormal_outputs/harmful_automation`: Step-by-step orchestration of a large criminal workflow

## Writing a rule with the Rule Lab

Four steps, as MCP tools or over HTTP (base URL https://promptintel.securitybreak.io/scanner-api/v1, with `Authorization: Bearer <key>`). The server writes the rule text from fields, so do not write the text yourself.

| Step | MCP tool | HTTP | What it does |
| --- | --- | --- | --- |
| 1. Read the format | `nova_rule_catalog` | `GET /rule-lab/catalog` | The meta keys, the categories, the condition shapes the engine evaluates, the limits. |
| 2. Draft | `nova_rule_draft` | `POST /rule-lab/draft` | You send fields; the server writes the rule text, checks it and compiles it. |
| 3. Test | `nova_rule_test` | `POST /rule-lab/test` | Your attack texts and benign look-alikes, and about 300 built-in benign texts scanned every time. Case by case: what fired. |
| 4. Hand over | `nova_rule_card` | `POST /rule-lab/card` | The rule, the last test of that exact text, and what is still missing. |

Always finish with the card, also when the rule is not ready: it says what is missing. A rule that is not ready is not presented as safe to enforce.

## How false positives are kept low

- One check is never enough. A rule may not match on a single keyword or semantic check. Every way to match needs two independent checks, or one check confirmed by an LLM question: `(keywords or semantics) and llm.$confirm`.
- The model is asked last. The LLM question runs only where a keyword or semantic check fired, and it is told what to answer no to: text that documents, quotes, tests or warns about the behaviour. Keep that first check precise: a model shown documentation about an attack often says yes.
- Look-alikes are the evidence. A benign text that fires nothing proves nothing. What counts is benign text that uses the attack's words and is still turned down: three of your own are required.
- Every test scans hard benign text. About 300 built-in texts: documentation that describes and quotes attacks, commands, agent instructions, everyday requests such as `ignore my last message`. One match is a false positive, and each text that fires a check is judged.
- Checks may not select ordinary text. If more than 10% of the built-in texts reach the LLM question, or come within one check of a match, the rule is not ready, whatever the LLM question answers. No rule PromptIntel publishes is that noisy.
- Near misses are shown. A benign text that scores just under a semantic threshold is reported: a slightly different wording would fire it.
- A changed rule is untested. The test belongs to the exact text. Any edit, even to the meta, needs a new test.

## A rule is ready when

- It compiles and follows the format.
- It cannot match on a single keyword or semantic check.
- On this exact text no benign text matched: your cases and the built-in ones alike.
- No attack case was missed, none undecided, no borderline answer.
- Every built-in text that fired a check was judged and turned down.
- At most 10% of the built-in benign texts reach the LLM question or come within one check of a match.
- At least 3 attack cases matched, and 3 benign look-alikes of your own were turned down.

Ready means no wrong result on what was tested. It is evidence, not a guarantee: no test covers every text. Run a new rule in log mode first, and add any benign text it fires on as a test case.

## Limits

| | |
| --- | --- |
| Price | Free on every plan, with a PromptIntel API key |
| Tests | 10 a day per key (Enterprise: 300); more for an agent whose reports were approved |
| Drafts and cards | 300 a day per key (Enterprise: 3,000) |
| One test | Up to 30 cases of 8,000 characters, the built-in benign texts, and up to 24 LLM answers. Answers are kept, so the next test judges what is left. |
| Engine | The open-source NOVA library, nothing else |
| What is kept | The counts of the last test of each rule, 30 days. Not the rule, not the test texts. |

## Good to know

- The number after an LLM question is the confidence a yes must reach: 0.7 for a confirming check, and never under 0.5. An answer within 0.1 of it is borderline, and a rule with a borderline answer is not ready.
- The open-source NOVA engine still reads that number as the model's temperature and counts any yes; it is being updated. The Rule Lab already applies the threshold, asks the model at a fixed low temperature, and reports every yes that did not reach the threshold.
- `none of …` and `any of ($a, $b)` are not evaluated by the engine and are refused. Write `not (any of …)` and `$a or $b`.
- A keyword literal matches inside other words. Keep it at least 4 characters, or use a regular expression with `\b`.
- An LLM question should say what to answer no to. Without that the model says yes to text that only talks about the attack; the draft warns (M020).
- A rule without an LLM question cannot tell a quoted attack from an attack: documentation that quotes the payload matches it.
- The test texts are sent to PromptIntel and, for a rule with an LLM question, to the model that answers it. Ask the user before sending text that is theirs.

Human-readable page: https://promptintel.securitybreak.io/docs/nova-rules
