--- /dev/null
+# Xbits Keyword Engine Analysis Test
+
+This test verifies the engine analysis output for the `xbits` keyword.
+
+## Purpose
+
+Tests that the `xbits` keyword is properly reported in the `rules.json`
+output when using `--engine-analysis` mode.
+
+## Coverage
+
+This test covers:
+- `xbits:set` with different tracking modes (ip_src, ip_dst, ip_pair)
+- `xbits:isset`
+- `xbits:isnotset`
+- `xbits:unset`
+- `xbits:toggle`
+- `xbits` with expire values (default, 60s, 300s)
+- Multiple xbits in a single rule
+
+The test verifies all four properties exposed by the engine analysis:
+- **cmd**: The xbits command (set, isset, isnotset, unset, toggle)
+- **name**: The xbit name being tracked
+- **track**: The tracking mode (ip_src, ip_dst, ip_pair)
+- **expire**: The expiration time in seconds (when specified)
+
+## Reference
+
+Similar to the flowbits engine analysis test, but for the xbits keyword
+which tracks state across hosts/networks rather than within a single flow.
+
+## Ticket
+
+https://redmine.openinfosecfoundation.org/issues/6351
--- /dev/null
+alert ip any any -> any any (msg:"Xbit set"; xbits:set,xb1,track ip_src; sid:1;)
+alert ip any any -> any any (msg:"Xbit set"; xbits:set,xb2,track ip_dst; sid:2;)
+alert ip any any -> any any (msg:"Xbit isset"; xbits:isset,xb1,track ip_src; sid:3;)
+alert ip any any -> any any (msg:"Xbit isset"; xbits:isset,xb2,track ip_dst; sid:4;)
+alert ip any any -> any any (msg:"Xbit isnotset"; xbits:isnotset,xb3,track ip_src; sid:5;)
+alert ip any any -> any any (msg:"Xbit unset"; xbits:unset,xb1,track ip_src; sid:6;)
+alert ip any any -> any any (msg:"Xbit toggle"; xbits:toggle,xb2,track ip_dst; sid:7;)
+alert ip any any -> any any (msg:"Xbit isset ip_pair"; xbits:isset,xb4,track ip_pair; sid:8;)
+alert ip any any -> any any (msg:"Xbit set both"; xbits:set,xb5,track ip_src; xbits:set,xb6,track ip_dst; sid:9;)
+alert ip any any -> any any (msg:"Xbit set with expire"; xbits:set,xb7,track ip_src,expire 60; sid:10;)
+alert ip any any -> any any (msg:"Xbit set with expire 300"; xbits:set,xb8,track ip_dst,expire 300; sid:11;)
--- /dev/null
+requires:
+ min-version: 9.0
+ pcap: false
+
+args:
+ - --engine-analysis
+
+checks:
+- filter:
+ filename: rules.json
+ count: 1
+ match:
+ id: 1
+ lists.postmatch.matches[0].name: "xbits"
+ lists.postmatch.matches[0].xbits.cmd: "set"
+ lists.postmatch.matches[0].xbits.name: "xb1"
+ lists.postmatch.matches[0].xbits.track: "ip_src"
+- filter:
+ filename: rules.json
+ count: 1
+ match:
+ id: 2
+ lists.postmatch.matches[0].name: "xbits"
+ lists.postmatch.matches[0].xbits.cmd: "set"
+ lists.postmatch.matches[0].xbits.name: "xb2"
+ lists.postmatch.matches[0].xbits.track: "ip_dst"
+- filter:
+ filename: rules.json
+ count: 1
+ match:
+ id: 3
+ lists.packet.matches[0].name: "xbits"
+ lists.packet.matches[0].xbits.cmd: "isset"
+ lists.packet.matches[0].xbits.name: "xb1"
+ lists.packet.matches[0].xbits.track: "ip_src"
+- filter:
+ filename: rules.json
+ count: 1
+ match:
+ id: 4
+ lists.packet.matches[0].name: "xbits"
+ lists.packet.matches[0].xbits.cmd: "isset"
+ lists.packet.matches[0].xbits.name: "xb2"
+ lists.packet.matches[0].xbits.track: "ip_dst"
+- filter:
+ filename: rules.json
+ count: 1
+ match:
+ id: 5
+ lists.packet.matches[0].name: "xbits"
+ lists.packet.matches[0].xbits.cmd: "isnotset"
+ lists.packet.matches[0].xbits.name: "xb3"
+ lists.packet.matches[0].xbits.track: "ip_src"
+- filter:
+ filename: rules.json
+ count: 1
+ match:
+ id: 6
+ lists.postmatch.matches[0].name: "xbits"
+ lists.postmatch.matches[0].xbits.cmd: "unset"
+ lists.postmatch.matches[0].xbits.name: "xb1"
+ lists.postmatch.matches[0].xbits.track: "ip_src"
+- filter:
+ filename: rules.json
+ count: 1
+ match:
+ id: 7
+ lists.postmatch.matches[0].name: "xbits"
+ lists.postmatch.matches[0].xbits.cmd: "toggle"
+ lists.postmatch.matches[0].xbits.name: "xb2"
+ lists.postmatch.matches[0].xbits.track: "ip_dst"
+ lists.postmatch.matches[0].xbits.expire: 30
+- filter:
+ filename: rules.json
+ count: 1
+ match:
+ id: 8
+ lists.packet.matches[0].name: "xbits"
+ lists.packet.matches[0].xbits.cmd: "isset"
+ lists.packet.matches[0].xbits.name: "xb4"
+ lists.packet.matches[0].xbits.track: "ip_pair"
+- filter:
+ filename: rules.json
+ count: 1
+ match:
+ id: 9
+ lists.postmatch.matches[0].name: "xbits"
+ lists.postmatch.matches[0].xbits.cmd: "set"
+ lists.postmatch.matches[0].xbits.name: "xb5"
+ lists.postmatch.matches[0].xbits.track: "ip_src"
+ lists.postmatch.matches[1].name: "xbits"
+ lists.postmatch.matches[1].xbits.cmd: "set"
+ lists.postmatch.matches[1].xbits.name: "xb6"
+ lists.postmatch.matches[1].xbits.track: "ip_dst"
+- filter:
+ filename: rules.json
+ count: 1
+ match:
+ id: 10
+ lists.postmatch.matches[0].name: "xbits"
+ lists.postmatch.matches[0].xbits.cmd: "set"
+ lists.postmatch.matches[0].xbits.name: "xb7"
+ lists.postmatch.matches[0].xbits.track: "ip_src"
+ lists.postmatch.matches[0].xbits.expire: 60
+- filter:
+ filename: rules.json
+ count: 1
+ match:
+ id: 11
+ lists.postmatch.matches[0].name: "xbits"
+ lists.postmatch.matches[0].xbits.cmd: "set"
+ lists.postmatch.matches[0].xbits.name: "xb8"
+ lists.postmatch.matches[0].xbits.track: "ip_dst"
+ lists.postmatch.matches[0].xbits.expire: 300