Skip to content

Honeypots: trap events

A honeypot is a network event that does not exist on your server, registered by Bastrel under a name cheat menus like to fire: an old payday event, a give-money event, a bank event that your framework no longer has. No honest player ever triggers it, because no screen of your server sends it. A player who triggers it is running a menu that fires events blindly, so the detection is certain by construction (BAS-NE-099, critical, deterministic): in enforce the player is banned, with the event name and the arguments as proof.

Bastrel ships no name. The list is empty and is yours to fill: Bastrel cannot know which events your server really uses.

In config/whitelists.lua:

honeypots = { events = { 'oldbank:giveMoney', 'payday:collect' }, calibrated = false },

Pick names that none of your scripts uses, on the client or on the server. A name that one of your scripts does use would ban honest players, which is why the three safeguards below exist and why the list is not trusted before you calibrate it.

Limits (config/modules.lua, block honeypots; an invalid value takes its default, with a warning): maxEvents (100 names), maxNameLen (128 characters), reportMs (10000: the same player triggering the same trap again within this window is ignored, without a new detection). In config/whitelists.lua, scan.honeypotYieldBytes (4194304, optional) makes the scan give the server a tick back each time that many bytes were searched for trap names.

A list with a single invalid entry (not a string, empty, too long, written twice, a name Bastrel or the game handles such as playerDropped or bastrel:..., or more than maxEvents names) is refused as a whole, with a warning in the log: no trap rather than a wrong trap.

Until you set calibrated = true, BAS-NE-099 stays in observe even under mode = 'enforce': the detection is logged and nobody is banned. Read your log for a few days. When every trigger comes from a menu and none from a script or an honest player, check the list against your scripts one last time, then set calibrated = true.

BAS-NE-099 is also marked unverified for now. Bastrel counts a trigger only when GetInvokingResource() is empty in the handler, and FiveM does not document what it returns there (see “Known limits”). Until an in-game capture settles it, the code stays in observe whatever your configuration says, calibrated = true included: nothing is applied, the detection is only logged, it adds nothing to the score and it is not posted to Discord. This note goes away when the capture is done.

  1. Collision with an event you register through Bastrel. If a resource registers a trap name through exports.bastrel:registerNetEvent or registerCallback, the trap is disabled and Bastrel reports BAS-CV-106 (kind = registered). Your event is not touched.
  2. Collision with a file. Some seconds after start, the scan of the other resources (see convars.md) looks for each trap name as plain text in the files it can read. A match disables the trap and reports BAS-CV-106 (kind = file, with the resource, the file and the line). A comment counts too: remove the name from your list or from the file.
  3. Armed after the scan. A trap is not reported before the scan ended (the console shows Resource scan done). If the scan fails, or if it loses a resource halfway, the traps stay unarmed until bastrel restarts, and the log says honeypots.arm_skipped.
  4. A resource started later is read before the traps come back. When a resource starts after the scan (ensure, restart), the traps are suspended at once (honeypots.suspended), only that resource is scanned, then the traps are armed again, except the ones the scan disabled. If several resources start close together, the traps stay suspended until all of them are read.

What Bastrel cannot see: a resource under escrow, a manifest entry written as a glob (client/*.lua), a file too large or unreadable (the scan lists them as “not scanned”), or a name built in code ('pay' .. 'day'). No alert does not guarantee that a name is unused: that is why the list is yours, and why you calibrate it.

At start, a refused list or an invalid honeypots block prints a [bastrel] WARNING honeypots: ... line. In enforce, the usual “list not calibrated” warning mentions the honeypots only when the list holds at least one name.

A trap triggered by a server script (TriggerEvent from one of your resources) is never charged to a player: Bastrel only reports a trigger that comes from the network, from a player it tracks.

The trap names are never sent to players (not in the SDK, not in any client file): a trap a cheat knows is no trap. A CI test checks the client files.

Code Meaning Mode
BAS-NE-099 A player triggered a honeypot event critical, deterministic, no score weight (the sanctions matrix decides alone, so sanctions.overrides['BAS-NE-099'] = { 'alert' } gives an alert and nothing else). Unverified for now: logged only, whatever the configuration (see “Calibrate, then enforce”). Once verified: enforce + calibrated list = ban and alert, otherwise logged with would_have
BAS-CV-106 A trap collides with a registered event or a readable file: disabled finding about the server, never about a player, never a sanction

Evidence of BAS-NE-099: event (the name), argCount and args (the arguments received; a missing argument is written <nil>). The size limits of network events apply (netevents.limits: maxArgs, maxDepth, maxKeys, maxStringLen, maxTotalBytes): over them, the evidence keeps only argCount, truncated = true and argTypes (the Lua type of each argument), never the values. Staff with the bastrel.bypass.netevents ACE are never sanctioned.

  • Whether GetInvokingResource() is empty in the handler of an event sent by a client is not documented (it is expected to be); if it is not, the trap never fires and the log says honeypots.not_attributed. No false positive, but no protection either: tell us.
  • Traps need ox_lib and oxmysql like the rest of Bastrel; nothing else.