MENATER Security operations
EnglishEN FrançaisFR
Front page GitHub

Theme

Documentation

Everything MENATER does, and why it does it that way.

The front page is the short version. This is the long one: what each part does, what it refuses to do, and the reasoning behind both. Read it in order or jump to what you need — nothing below depends on what precedes it.

What MENATER is

MENATER is a security operations console you install on your own machine. It takes the alerts your detection tools raise, works out which ones matter, and puts the ones that need a decision in front of a person. It is free, open source, and there is no company behind it.

It has two halves that share one screen. The alert side deals with what has already happened — an alert is a thing that occurred. The code side deals with what is about to: a flaw in your source code, before it ships. They share an interface and a login; they do not yet feed each other.

The design idea in one sentence. An AI reading a security alert is reading text an attacker wrote, so the AI is allowed to read and to propose, and a person is the only thing allowed to act.

Three programs run: a database, the console itself, and the code scanner. Nothing else is needed and nothing leaves your machine except the requests you configured — to the AI provider whose key you supplied, and to the reputation services you chose to enable.

Install it

Docker is the normal way to install MENATER, not one option among several. You need Docker and nothing else: no database to configure, no schema to apply by hand, no second machine.

# fill in the two passwords it asks for
cp .env.example .env

# database + console + code scanner
docker compose up -d --build

→ http://localhost:4400

Useful afterwards: docker compose logs -f console follows what the console is doing, docker compose down stops everything and keeps your data, and docker compose down -v erases everything including the database.

The database and the code scanner are reachable only from inside the stack. One port is published, and it serves both the interface and the API — a second entry point would mean a lock protecting half the application.

Running it without Docker? npm run dev inside dashboard/ starts the three processes with live reload. That is a development mode: it assumes you already have Node, a running database and the schema applied.

Make it work

It comes up in three minutes and sorts nothing. That is deliberate: each of the four things below is a decision the console refuses to make for you. A checklist at the top of the settings page tracks all four until they are done.

# What you have to do What happens if you skip it
1Set a shared secret for the alert endpointThe door stays closed. An empty secret closes it rather than opening it — "no authentication configured" must never read as "no authentication required".
2Turn the endpoint onEvery alert is refused.
3Give it a key for an AI modelEvery alert lands on a human with a confidence of zero, and says why. Nothing is lost; nothing is sorted.
4Point a detection tool at itNo alerts arrive.

Reputation-service keys and chat notifications are optional. A service you gave no key to is shown as Not asked — a question nobody put, which is a different thing from a question that came back empty.

The connectivity test results: a red banner reading “at least one link is broken, 9 checks green, 5 to watch, 1 failing, 0 undetermined — in 14 ms”, then per-check rows for the database, the SQL schema, the triage model key and four enrichment sources.
The connectivity test on the same install. Nine green, five to watch, one failing, zero undetermined — four states, so “checking” is never reported as “broken”. Each line names the consequence and then the fix: the missing model key says every alert will take the fail-safe verdict, and where to put the key.

The Health tab tests every link in the chain on demand and names, for each one, the consequence and then the fix. It reports four states rather than two, so "still checking" is never announced as "broken".

The five steps

MENATER runs its own engine. There is no workflow platform to install and no second product that has to be reachable for an alert to be sorted — so a workflow cannot be half-deployed, renamed in an editor, or left unpublished. The console reads its own record of what ran.

  1. 01 · Intake Checked and de-duplicated Required fields verified, repeats dropped, and the sender told what actually happened: rejected, already seen, or accepted.
  2. 02 · Context Reputation and tagging Address reputation, location, and technique tagging. A service with no key is marked not asked, and the AI is told that.
  3. 03 · Opinion The AI decides A verdict, a confidence, and its reasoning. With no key it returns needs_human at confidence 0 — safe by failing, never open by failing.
  4. 04 · Decision Human Approval and routing The request goes to the console and to Slack or Discord. Below the notification threshold nobody is asked — so nothing waits, and it escalates at once.
  5. 05 · Record Sealed into the log One row that can be added to and never edited, sealed to the row before it under a lock.

A sixth exists and never runs in sequence. The error handler is declared on the other five and called only from their failure branches. No failure is ever silent: every branch logs, notifies, or both.

Three consequences worth knowing

  • A junction waits A step with several inputs does not start when one of them has answered. Step 02 would otherwise assemble a third of its sources — with no error, no trace, and a plausible result.
  • Retry, then fail by name Every outside call retries with a growing delay, treating any wait the other end suggests as a floor rather than shortening it. When it still fails, it says which service and why: a source nobody could reach is not a source that said nothing.
  • The unreduced view The queue stitches runs into cases, and two kinds of failure vanish in that stitching. The Tracking tab shows them raw: broken chains, runs attached to no alert, a searchable log, and a replay that refuses when the original alert is unknown rather than replaying a reconstruction.
The Tracking tab: 120 runs inspected, 118 attached to an alert, a warning that the observed window is full, and three sub-tabs for processing chains, orphan runs and the run log.
The unreduced view. It counts the runs the queue could not show you, and warns when the observed window is full — beyond it, the console does not know — rather than letting a bounded read look like a complete one.

How alerts get in

There are two ways to deliver an alert, and they fail differently. Your tool can push it to MENATER the moment it happens, or MENATER can ask your tool for new alerts on a schedule. Neither is better.

Push

One network hop, so the alert arrives instantly — and you are exactly as reliable as your tool's own retry policy. An alert nobody received is an alert nobody knows about.

Pull

Half a polling interval of delay on average — and nothing is lost to a call that never arrived, because the bookmark is still where it was.

The default uses both, because the cost lands on different alerts. A critical alert contained four minutes late was not contained; a low-priority one sorted four minutes late is still low priority. So the urgent two levels take the instant path and the other two are polled. An alert whose priority could not be read takes the fast path — quietly parking something that might be critical is the failure this product exists to prevent.

The Ingestion tab: three delivery architectures side by side with their costs stated, hybrid selected, and a table mapping P1 to P4 onto webhook or polling with the delay each implies.
Choosing a delivery architecture. Each option states what it buys and what it costs, and the lane table quotes half a polling interval — the average added delay, not the interval, because that is the number a P1 gets weighed against.

Rules the transport obeys

  • Nothing is refused for taking the wrong path A low-priority alert sent to the instant endpoint is accepted and sorted; the reply says which path the policy expected. Dropping a detection over a routing preference replaces it with nothing.
  • The bookmark only moves onto delivered data Not onto data that was read, and never after an empty or failed poll — a source that is down looks exactly like a source where nothing happened. Each poll also reaches back a little past the bookmark, because a tool that stamps an alert at detection and files it a second later would otherwise lose that second forever.
  • A poll that placed nothing is a failed poll Whether the network request worked is not the question being asked. It reports how many alerts it read, how many it placed, and names the cause when those differ.
  • Polls do not pile up One in flight per source. A source slower than the interval simply gets polled less often, which is the honest consequence of being slow; the alternative accumulates requests until memory runs out.
  • A source that refuses us is backed off, out loud The wait doubles per consecutive failure up to thirty minutes, and it is shown on screen — a skipped source is otherwise indistinguishable from a poller that stopped. The "poll now" button ignores the wait, because a diagnostic control that answers not yet, wait eight minutes is one nobody presses twice.

What an alert must contain

Two tiers, and the difference between them is the difference between we cannot use this and this detection simply had no destination address.

Tier Fields Rule
Identity alert_id · rule_name · severity · timestamp · raw_log Required. Missing means rejected — and the rejection names the fields.
Observables source_ip · dest_ip · user · host · process · file_path · url Optional. Absent stays absent: null, never an empty string, never invented. Most real detections carry no destination address.
Everything else whatever your tool sends Kept whole, on the case and in the audit row. Nothing your tool sends is discarded.

The consequence to hold onto. Isolating a machine refuses to run when it has no machine to name. An action whose target is unknown is not an action.

Alerts arrive at POST /api/ingest/<source>, and the route decides how the payload is read — never the payload itself, so a sender cannot claim to be something else. The Wazuh integration maps nothing: it forwards the alert as written and the console translates.

The ten tabs

Grouped rather than trimmed. Ten identical buttons cost a decision every time you look at them; the remedy is categories, not a second menu inside the first.

Tab What is on it
AlertsThe queue and the incident card. Five columns carry eight facts: how bad it is sits inside what it is, and the confidence sits inside the verdict, because 0.58 means nothing without the verdict it qualifies. Every value in an alert carries a jump into Lookup with the question already asked.
CodeRun a code scan and read the report. It is loaded the first time you open it and kept loaded — changing tab during a scan does not cut it.
IngestionHow alerts get in: the delivery policy, the sources on each path, and the pipeline as it runs, with its editable variables.
RulesDeclare what is normal here — the authorised scanner, the backup window — with a dry run that answers which rule would match this alert without touching the pipeline. Eight templates ship disabled and unowned.
TrackingThe unreduced view: broken chains, runs attached to no alert, a searchable log, replay. Expected orphans — the diagnostic probe fires an invalid alert on purpose — are listed and never counted, or the tab would blink after every connectivity test and a permanent alarm stops being read.
MetricsTwo rates, never added together. See Watch-only mode.
HealthOne button that tests every link in the chain, and nine test alerts to send through it — one per code path rather than per story: no destination, with a hash, malformed, duplicate.
LookupAsk about one value — an address, a domain, a web address, a file fingerprint, an email — plus a breached-password check whose fingerprint never leaves your browser. Obfuscated input (1.2.3[.]4, hxxp://) is an accepted format, and both forms are shown.
GuideThe in-console documentation, searchable — and the search reads the glossary too, so a word looked up in the wrong vocabulary still finds its section.
SettingsKeys, log sources, the machine-to-code inventory, the scan engines, the assistant — and the checklist naming what is still missing.
The Lookup tab after querying 8.8.8.8: a verdict reading “nothing against it”, the line “1 of 4 sources answered”, and two source cards marked “not asked”.
A verdict that states its own scope. One source answered, and the panel says so beside the verdict; the two sources with no key are not asked — a question nobody put, not a hole in what is known.

Two rules the Lookup tab holds to

  • "Clean" needs a source that answered With no keys configured every service is skipped, and printing nothing found over a value nobody looked at is reassurance built on a hole. The count of services that actually replied sits beside the verdict, never below the fold.
  • Not every non-answer is a failure Several services say this address appears in no breach with an HTTP 404, which is the best news the endpoint has. A blanket "anything but 200 is broken" rule would hide the most common and most welcome outcome behind the word unavailable, so each service declares its own answering codes.

The built-in assistant

A chat panel docked to the side of whatever screen you are on. It answers questions about this installation — explain alert 1234 simply, why is this still waiting?, what is left to configure? — and it receives the page you are on, so "this alert" needs nothing typed.

Its entire security model is that it cannot act. Its tool list is closed at seven read-only lookups: the queue, one alert, one chain, the rules, the metrics, health, and setup state. There is nothing that approves, isolates, closes, replays, edits a rule or writes a setting.

That is not caution for its own sake. Someone asking about an alert is asking about a log an attacker composed, and unrestricted model access to log content is measured at an 87 % prompt-injection success rate. Three layers answer it, in the order they actually hold:

  • No writes in the list An instruction hidden in a log arrives at a model holding seven getters. This is the one that matters.
  • Untrusted text is fenced, not deleted Log content travels inside markers carrying a number generated fresh for that request, so a log cannot forge a closing marker for something that did not exist when it was written. Nothing is stripped: rewriting evidence is the same mistake as filling a blank with a guess.
  • The instructions say what a fence means Advisory, and third for a reason — a prompt is not a boundary.

Four AI providers are supported, one key each, chosen in settings. Nothing is kept: the conversation travels with each question and is stored nowhere. Storing threads would create a fourth copy of alert content, outside the audit record, with no retention policy. Every answer shows what it looked up — an answer you cannot check is an answer believed for the wrong reasons.

Connect your own AI

The same read-only lookups can be exposed to an AI assistant you run yourself — Claude Desktop, Claude Code, Cursor, VS Code — over the Model Context Protocol. It is off by default, and switched off or without a token it answers as though the address did not exist.

Settings has a four-step install page: turn it on, generate a token, copy the block for your client, press the test button. The address in that block comes from the address you reached the console on, not from a document — install it on a colleague's machine and the block names their host. The token is shown once, and the test runs on the server, because the browser is deliberately refused by the same guard that stops a hostile web page reaching a service on your machine.

What it exposes: 14 tools, 8 prompts, 9 resources, all read-only. The prompts matter more than they look — a client that sees ten tool names sees ten getters and no reason to use them, whereas explain this alert, how dangerous is this, why is it waiting and write me a shift handover are the questions this console exists to answer, offered rather than remembered.

The token is the whole access control. Anyone who can reach the console and holds it can read the queue, the alerts and the rules. Treat it like the console password. They can change nothing: there is no write in the list.

Themes and readability

Six colour schemes, switchable from the header of this page too. Every one of them redefines the entire palette: inheriting three colours from another theme produces an accident that review does not catch, such as a dark grey left on a cream background.

Contrast is measured, not judged by eye. A test computes the standard contrast ratios and refuses a palette that falls below them — 4.5:1 for text on every surface, for text placed on the accent colour, and for the risk colours inside their own badges. It caught a failure red at 2.5:1, on the screen that depends on red most, and it found a long-standing grey at 2.77:1 in the original theme.

  • One accent, one job The accent colour means exactly one thing — a person is blocked here — plus the primary button. Red says critical or failed, orange worth a look, green handled. A red button would teach an analyst to ignore red on the one screen where red saves time.
  • Colour never carries meaning alone Every risk colour is paired with a word, so the console still works for someone who cannot tell them apart.
  • Density is a feature The alert table is meant to show forty rows. A fixed-width font means this string came from the machine and it is exact; prose about a value is not in it, the value is. An analyst scrolling is an analyst who has lost the overview.
  • What explains folds, what reports does not A definition or a method note goes behind a fold that says how much is inside. A state, a count, a failure or an action awaiting approval stays on screen. Filing a failure behind a silent fold reproduces exactly the defect the Tracking tab exists to expose.

The interface works on a phone: the tab bar scrolls sideways and says so, the alert table folds into cards, touch targets are 44 px, and form fields are 16 px so iOS does not zoom when you tap one.

The four closed lists

Four lists in this product are closed — nothing can be added to them, not even behind an approval. Each is closed for the same reason: an operator points this console at attacker-composed text all day, and an open list is one instruction away from being somebody else's.

List Size What being closed prevents
Actions 4 Isolate a machine temporarily (with an expiry and an automatic undo), open a ticket, escalate, close as a false positive. No tuning rule can reach past this list either.
Assistant tools 7 read-only The chat panel can read the queue, an alert, a chain, the rules, the metrics, health and setup state, and write nothing. An injected instruction arrives at a model with nothing to obey it with.
Credential names named list Both key stores accept only names on a fixed list. An arbitrary name would turn the settings endpoint into a way to inject any environment variable into the process, PATH included.
Reputation services 7 The lookup endpoint takes a value and a kind — never a web address. A route that fetched whatever it was handed would be a request-forgery hole with a button on it.
An incident card for a critical ransomware alert: fallback verdict at confidence 0, four pipeline steps with their timings, and an approval block proposing an escalation.
One incident. The verdict names its own cause, the destination is — because the detection recorded none, and the proposed action sits behind Approval required. Nothing was executed.

A fifth limit sits on the tuning rules. A team can declare what is normal here — the authorised scanner, the backup window — and the console refuses a permanent allow keyed on one identity field alone, a suppression with no expiry, and any rule with no owner or no stated reason.

Watch-only mode

A new install executes nothing. It records the decision it would have made and carries on. Leaving that mode is not a switch to flip on your first day: it waits on a number measured over real alerts that went through the real guardrails.

Two rates are measured, and they are never added together, because they answer different questions:

How often the AI said "false positive"

A measure of distribution, not of correctness. A model that called everything a false positive would score beautifully here.

How often a person said no

The share of decisions put to a human that they rejected. This is the one that opens the gate — it is the only stand-in for correctness available without ground truth.

Two things not to compare. Two calls to the same model on the same alert return 0.88 and 0.91 — comparing them measures the model's noise, not anything about your setup. And comparing a case the pipeline did not produce — a test alert injected halfway through, a fallback verdict written when no key was configured — yields disagreements that are not disagreements, and blocks the gate for nothing.

The default is safe by failing. Missing configuration cannot enable execution, and only the exact word false turns watch-only off. An approval that times out executes nothing — silence is never consent.

The audit record

Every decision writes one row to a table that can be added to and never edited. Each row is sealed to the one before it, so removing or altering a row anywhere in the chain shows up the next time the chain is verified. It is the product's only tamper evidence.

A verifier that cries wolf is one an auditor stops believing — which would hide a real deletion in the noise. So a false alarm here costs more than no alarm at all.

That is not theoretical. A stress pass found the verifier reporting broken links on 22 perfectly healthy rows out of 3 195. The cause: the row number was handed out before the lock that orders the sealing, so under load two transactions could seal in one order while carrying numbers in the other. The chain was an intact list that was simply no longer in number order, and the verifier walks by number. Fixed by moving the number inside the lock; re-verified across ~500 rows written 40 ways at once, with zero breaks.

Rows sealed before that fix keep their broken links. The table is append-only and immutable by design, which is the right trade: they cannot be rewritten, and the verifier goes on naming them honestly.

Measured numbers

Run against a plain docker compose up — database, console, code scanner, nothing else. This is a quality pass on one machine, not a published benchmark; what matters is that it is reproducible and that the conditions are stated.

400 / 400 alerts accepted under load 50 at once · ~134 per second · 95 % answered within 424 ms · every chain settled, nothing left running
1 of 12 duplicates accepted 12 simultaneous copies of one alert → 1 accepted, 11 already seen, zero errors
0 broken links in the audit chain re-verified across 2 644 rows written under concurrent load
1,278 tests passing 913 console · 365 code scanner · type checking clean

That pass found eight defects and they were fixed. Three are worth naming, because each is a failure that looked like a success:

  • Refused alerts reported as accepted The entry point returned "accepted" for every alert it managed to start, including ones the pipeline had rejected for missing fields — so the sender had no reason to fix anything, and the reason, already computed, never left the server.
  • The duplicate check was not safe under a burst "Insert unless a row exists" reads as one atomic step and is not: two transactions both saw the row absent, both inserted, one died. Twelve simultaneous copies of one alert produced one server error — and a server error tells the sender to retry an alert that was already accepted.
  • The audit verifier cried wolf See The audit record.

What it does not do

Stated here for the same reason the console states it: a page that lists only what works is a green that was never earned.

  • The two halves do not talk A code scan creates no alert in the queue, and an alert starts no scan. What does connect them is a table you fill in mapping machine names to the code that runs on them, so an incident card can name the project and hand it to the scanner with one click — it fills a form in, it never launches anything. Matching is exact, and an ambiguous entry is refused at save time rather than guessed at.
  • The login identifies nobody Access is a single password. It protects the console; it does not tell one approver from another, and the audit row says exactly that rather than implying a name it does not have.
  • Autonomy waits on measurement Leaving watch-only mode is gated on a disagreement rate measured over real alerts, not on a date. See Watch-only mode.
  • It is a project, not a product Self-hosted, open source, no support contract and no company behind it. Read the code before you point it at anything that matters — which is the right thing to do with any tool in this category.

Words used here

The front page avoids these on purpose. They appear on this page and inside the console, so they are defined once, here.

Alert
A single warning raised by a detection tool: this file was blocked, this account failed to sign in twelve times. It is a thing that already happened, not a prediction.
Triage
Deciding, quickly, which alerts deserve a person's time. Borrowed from hospitals, and it means the same thing here.
False positive
An alert that turned out to be nothing. Most alerts are. The cost of getting this wrong runs both ways, which is why the console is careful about the word.
SIEM
The tool that collects logs from everything you run and raises alerts from them. It is what feeds MENATER. Wazuh is the one this project is tested against.
Enrichment
Adding context to an alert before anyone judges it — is this address known for attacks, where is it, which known technique does this match.
Watch-only mode
The default. The console notes what it would have done and does nothing. Called shadow mode in the source code.
Observable
A concrete value inside an alert you can go and look up: an address, a machine name, a file fingerprint, a user.
Webhook
A web address you give to another tool so it can push data to you the instant something happens, instead of you asking.
Polling
The opposite: asking another tool for anything new, on a schedule.
Prompt injection
Hiding instructions inside text an AI will read, so it follows them instead of yours. A log an attacker can write into is the textbook case, and it is the reason this console's AI is allowed to read and not to act.