From 00cf9490f222f45210c548f0b618ed5ef3b3959c Mon Sep 17 00:00:00 2001 From: Wasabules <39313803+Wasabules@users.noreply.github.com> Date: Tue, 27 Oct 2026 10:20:16 +0100 Subject: [PATCH] fix(filters): a whole address is that address; CIDR and patterns too The Source IP box kept every address containing what was typed, so 10.10.1.1 also showed 10.10.1.10-19 and 10.10.1.100-199 (#98). It now reads what is typed as an address: - a whole address is that address, an IPv6 one however written; - a CIDR (10.10.1.0/24) is its range; - "*" and "?" make a pattern (10.10.1.*), matched whole; - anything else is still text the address contains, while it is typed. One definition, search.Address, for the filter in the live view, the history and exports, and for the addresses of source rules and routes. The history answers a CIDR through an ss_addr SQL function, after the index has narrowed the rows to its whole octets. The window repeats it in lib/search.ts: parseIP and ipString follow Go's netip step by step, held to it by testdata/addresses.json (52 texts, 42 cases), and the filters spec gains the address cases. The case that said 10.0.0.1 finds 10.0.0.10 was the bug, and is gone. Source rules: an address entry that is none of the above (half an address, a name) is refused when saved, since it could match nothing. The editor's preview follows the addresses as they are typed, CIDR and patterns included, and right-clicking a message opens the rule that reads its device by a range too: a new rule added after it would never be reached. The Source IP box and the rule's addresses say what they take, in the 8 languages. --- docs/USER_GUIDE.md | 19 +- frontend/screenshots/bridge/App.js | 6 +- frontend/src/components/FilterBar.svelte | 2 +- .../src/components/SourceRuleEditor.svelte | 11 +- frontend/src/lib/i18n/de.json | 2 + frontend/src/lib/i18n/en.json | 2 + frontend/src/lib/i18n/es.json | 2 + frontend/src/lib/i18n/fr.json | 2 + frontend/src/lib/i18n/it.json | 2 + frontend/src/lib/i18n/ja.json | 2 + frontend/src/lib/i18n/pt.json | 2 + frontend/src/lib/i18n/zh.json | 2 + frontend/src/lib/search.ts | 215 +++++++- frontend/src/lib/sources.ts | 23 +- frontend/tests/search.test.ts | 17 +- internal/models/sources.go | 15 +- internal/notify/route.go | 43 +- internal/search/address.go | 242 +++++++++ internal/search/address_test.go | 177 +++++++ internal/search/filter.go | 13 +- internal/search/filter_test.go | 22 +- internal/search/testdata/addresses.json | 466 ++++++++++++++++++ internal/search/testdata/filters.json | 258 +++++++++- internal/sources/sources.go | 13 +- internal/sources/sources_test.go | 30 ++ internal/storage/query.go | 56 ++- 26 files changed, 1570 insertions(+), 74 deletions(-) create mode 100644 internal/search/address.go create mode 100644 internal/search/address_test.go create mode 100644 internal/search/testdata/addresses.json diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 8e71d0b..05cea8d 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -159,7 +159,9 @@ the address, that rule opens instead. Rules are listed, ordered, turned off and A rule has three parts. **Which messages** -- **Addresses**: an IP, a CIDR (`10.0.7.0/24`) or a pattern, comma-separated. +- **Addresses**: comma-separated, each matched whole: an address (`10.0.7.12`), a CIDR range + (`10.0.7.0/24`) or a pattern (`10.0.7.*`). An address is that device only; a rule naming + `10.0.7.1` does not read `10.0.7.12`. - **Hostnames** and **Applications**: patterns such as `fw-*`, matched against the message as read without the rule. - **Raw text contains**: a regular expression, case-insensitive. Behind a relay every device @@ -232,13 +234,24 @@ Select one or more facilities to filter by the source subsystem. Common faciliti ### Hostname, App Name and Source IP Filters -Enter part of a hostname, an application name or an IP address. Messages whose field contains the -entered text are displayed: +Enter part of a hostname or an application name. Messages whose field contains the entered text are +displayed: - Case does not matter, and neither does an accent on a Latin letter: `cafe` finds `CAFÉ-01`. - Everything else counts as typed, spaces included. `web_01` finds `web_01` and not `web-01`, and `50%` is a percent sign, not a wildcard. +The **Source IP** box takes an address the way a network is described: + +| Typed | Shows the messages from | +|---|---| +| `10.10.1.1` | that address only — not `10.10.1.10` or `10.10.1.100` | +| `10.10.1.0/24` | the CIDR range, here `10.10.1.0` to `10.10.1.255` | +| `10.10.1.*` | the addresses matching the pattern: `*` stands for any characters, `?` for one | +| `10.10.1.` | the addresses containing the text, while it is being typed | + +An IPv6 address matches however it is written: `fe80:0::1` finds `fe80::1`. + ### Search The search box looks in each message's text and in its raw line. The button beside it switches diff --git a/frontend/screenshots/bridge/App.js b/frontend/screenshots/bridge/App.js index 8fc9949..b0e8776 100644 --- a/frontend/screenshots/bridge/App.js +++ b/frontend/screenshots/bridge/App.js @@ -24,7 +24,7 @@ import './runtime.js'; import { isDemo, override, delayed } from './scene.js'; -import { words, filterMatcher, sortMessages, groupMessages } from '../../src/lib/search'; +import { words, filterMatcher, sortMessages, groupMessages, addressMatcher } from '../../src/lib/search'; import { MESSAGES, STATS, SERVER_CONFIG, SERVER_STATUS, ALERT_RULES, ALERT_HISTORY, @@ -103,8 +103,8 @@ let sourceRules = clone(SOURCE_RULES); * applies. Reading a format is the engine's; the demo does not pretend to. */ function trySource(rule, samples) { - const named = (rule.match.sources || []).map((s) => s.trim()).filter(Boolean); - const matches = (ip) => named.some((s) => s === ip || (s.endsWith('.0/24') && ip.startsWith(s.slice(0, -4)))); + const named = (rule.match.sources || []).filter((s) => s.trim()).map((s) => addressMatcher(s, false)); + const matches = (ip) => named.some((take) => take(ip)); return samples.map((s) => { const before = messages.find((m) => m.rawMessage === s.raw) || { ...messages[0], rawMessage: s.raw, message: s.raw, sourceIP: s.sourceIP }; diff --git a/frontend/src/components/FilterBar.svelte b/frontend/src/components/FilterBar.svelte index f2f8ea2..9371dc6 100644 --- a/frontend/src/components/FilterBar.svelte +++ b/frontend/src/components/FilterBar.svelte @@ -210,7 +210,7 @@ + title={$_('filter.sourceIPHint')} on:input={setSourceIP} class="filter-input" /> diff --git a/frontend/src/components/SourceRuleEditor.svelte b/frontend/src/components/SourceRuleEditor.svelte index 00836d1..e4b2518 100644 --- a/frontend/src/components/SourceRuleEditor.svelte +++ b/frontend/src/components/SourceRuleEditor.svelte @@ -7,13 +7,15 @@ * preview is here to prevent. */ import { onDestroy, onMount } from 'svelte'; + import { get } from 'svelte/store'; import { _ } from 'svelte-i18n'; + import { messages } from '../lib/stores'; import { SEVERITY_COLORS } from '../lib/constants'; import { activeZone, formatInZone } from '../lib/timezone'; import { toastError, toastSuccess } from '../lib/toast'; import FormatFields from './FormatFields.svelte'; import { - deleteSourceRule, listText, parseList, ruleDraft, rulesVersion, saveSourceRule, trySourceRule, + deleteSourceRule, listText, parseList, ruleDraft, rulesVersion, samplesForRule, saveSourceRule, trySourceRule, } from '../lib/sources'; import type { SourceRule, SourceSample, SourceTrial } from '../lib/sources'; @@ -28,6 +30,9 @@ rule.levelFrom = rule.levelFrom || ''; rule.encoding = rule.encoding || ''; let sourcesText = listText(rule.match.sources); + // The samples it opened with, until the addresses change: then the + // latest messages from the addresses as now written. + const openedWith = sourcesText; let hostnamesText = listText(rule.match.hostnames); let appNamesText = listText(rule.match.appNames); let hostMode: 'sent' | 'source' | 'fixed' = @@ -62,6 +67,7 @@ clearTimeout(debounce); debounce = setTimeout(async () => { const mine = ++generation; + if (sourcesText !== openedWith) samples = samplesForRule(get(messages), current()); try { const got = samples.length ? await trySourceRule(current(), samples) : []; if (mine !== generation) return; @@ -156,8 +162,9 @@
{$_('sources.which')}
+

{$_('sources.sourcesHint')}