From cba5a70a92e294581c72e2c679f5be8db7075b69 Mon Sep 17 00:00:00 2001 From: Wasabules <39313803+Wasabules@users.noreply.github.com> Date: Tue, 27 Oct 2026 10:53:56 +0100 Subject: [PATCH] feat(sources): templates of your own, rules shared as files, "sends no hostname" Templates. "Start from" in the rule editor fills the format and the corrections from a template, keeping the rule's name and addresses. Seven come with SyslogStudio (sources.Templates), each for what the reading as received cannot guess: a device that sends no hostname, a camera, the level in the text, JSON behind a syslog header, Windows-1252, an untrustworthy clock, a format of its own. Each is read on a message of its kind in templates_test.go, which also refuses a template that reads it as the reading as received does - why FortiGate has none: its key=value is read already. Their names and hints are in the 8 locales, which the same test checks. Your own. "Save as a template..." keeps a way of reading - the format, a regular expression included, and the corrections, never the devices - under a name of one's own, from the rule editor or from a file import's format. They are offered in both, listed under Settings > Sources to be deleted, and kept in the window's own settings (sourceTemplates), so they are the person's whichever engine the window drives. Sharing. Export... writes the rules and one's templates to a JSON file (format syslogstudio-source-rules, version 1, without ids); Import... reads one, refuses it whole if one rule or template would not be saved (a newer version too), adds the templates, and saves the rules after the others and turned off, since the addresses they name are another network's until someone has looked. "It sends none" (SourceRule.NoHostname): an RFC 3164 message from a device that sends no hostname has the word after its timestamp taken for one. That word goes back to the tag or the text (syslog.RereadWithoutHost) and the hostname is the source address. The import and rule dialogs, like Settings before, no longer squeeze their content to fit: the file's name and sample were cut in the import. The import-format screenshots are regenerated. --- README.md | 2 +- app_sources.go | 121 +++++++++++++ app_sources_test.go | 55 ++++++ docs/USER_GUIDE.md | 29 ++- docs/assets/img/import-format-dark.png | Bin 302060 -> 305974 bytes docs/assets/img/import-format-light.png | Bin 316489 -> 319545 bytes docs/documentation.html | 6 +- frontend/screenshots/bridge/App.js | 12 +- frontend/screenshots/fixtures.js | 15 ++ frontend/src/components/ImportDialog.svelte | 41 +++++ frontend/src/components/SaveTemplate.svelte | 66 +++++++ .../src/components/SourceRuleEditor.svelte | 56 +++++- frontend/src/components/SourceRules.svelte | 86 ++++++++- frontend/src/lib/i18n/de.json | 49 +++++- frontend/src/lib/i18n/en.json | 49 +++++- frontend/src/lib/i18n/es.json | 49 +++++- frontend/src/lib/i18n/fr.json | 49 +++++- frontend/src/lib/i18n/it.json | 49 +++++- frontend/src/lib/i18n/ja.json | 49 +++++- frontend/src/lib/i18n/pt.json | 49 +++++- frontend/src/lib/i18n/zh.json | 49 +++++- frontend/src/lib/sources.ts | 51 ++++++ frontend/wailsjs/go/main/App.d.ts | 11 ++ frontend/wailsjs/go/main/App.js | 20 +++ frontend/wailsjs/go/models.ts | 77 ++++++++ internal/models/models.go | 3 + internal/models/sources.go | 25 ++- internal/sources/file.go | 106 +++++++++++ internal/sources/sources.go | 7 +- internal/sources/templates.go | 27 +++ internal/sources/templates_test.go | 166 ++++++++++++++++++ internal/storage/config.go | 16 ++ internal/syslog/parser.go | 23 ++- internal/syslog/parser_test.go | 30 ++++ 34 files changed, 1415 insertions(+), 28 deletions(-) create mode 100644 app_sources.go create mode 100644 app_sources_test.go create mode 100644 frontend/src/components/SaveTemplate.svelte create mode 100644 internal/sources/file.go create mode 100644 internal/sources/templates.go create mode 100644 internal/sources/templates_test.go diff --git a/README.md b/README.md index 433688b..154f575 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ starts with the computer, before anyone signs in, and that the window then drive - **Source rules** for the devices that bend it further: right-click a message, say how that device is read (any import format, a regex), fix its hostname, application, clock, severity or encoding, and watch its latest messages change before saving — then read the stored - history again with it + history again with it. Start from a template, save your own, and export them to share - **Mutual TLS** where senders must prove who they are - **Allowed sources** by IP literal or CIDR (hygiene, not authentication — see the note below) - **Per-IP connection cap** so one host cannot occupy every slot diff --git a/app_sources.go b/app_sources.go new file mode 100644 index 0000000..fcaef64 --- /dev/null +++ b/app_sources.go @@ -0,0 +1,121 @@ +package main + +import ( + "fmt" + "io" + "os" + "strings" + + "github.com/google/uuid" + + "SyslogStudio/internal/models" + "SyslogStudio/internal/sources" + + wailsRuntime "github.com/wailsapp/wails/v2/pkg/runtime" +) + +// --- Source rules: templates, and files to share them ------------------------ +// +// Window-only: the templates are the same everywhere, and a file is read or +// written where the person sits. Rules read from a file are saved through the +// Backend like any other, so they reach the service the window drives. + +// GetSourceTemplates returns the rules offered to start from: those that +// come with SyslogStudio, then those this person saved. +func (a *App) GetSourceTemplates() []models.SourceTemplate { + return append(sources.Templates(), a.prefs.LoadSourceTemplates()...) +} + +// SaveSourceTemplate saves a rule's way of reading as one of this person's +// templates — a new one, or in place of the one with its ID — and returns it. +// The devices the rule names are not part of it. +func (a *App) SaveSourceTemplate(t models.SourceTemplate) (models.SourceTemplate, error) { + t.Name = strings.TrimSpace(t.Name) + t.Rule = t.Rule.AsTemplate() + if err := sources.CheckTemplate(t); err != nil { + return models.SourceTemplate{}, err + } + mine := a.prefs.LoadSourceTemplates() + replaced := false + for i := range mine { + if t.ID != "" && mine[i].ID == t.ID { + mine[i], replaced = t, true + } + } + if !replaced { + t.ID = "mine-" + uuid.NewString() + mine = append(mine, t) + } + a.prefs.SaveSourceTemplates(mine) + return t, nil +} + +// DeleteSourceTemplate removes one of this person's templates. The rules +// started from it keep what they took. +func (a *App) DeleteSourceTemplate(id string) { + mine := a.prefs.LoadSourceTemplates() + kept := mine[:0] + for _, t := range mine { + if t.ID != id { + kept = append(kept, t) + } + } + a.prefs.SaveSourceTemplates(kept) +} + +// ExportSourceRules asks where, then writes the rules there, with this +// person's templates. "" when the person cancelled. +func (a *App) ExportSourceRules(rules []models.SourceRule) (string, error) { + data, err := sources.EncodeFile(rules, a.prefs.LoadSourceTemplates()) + if err != nil { + return "", err + } + path, err := wailsRuntime.SaveFileDialog(a.ctx, wailsRuntime.SaveDialogOptions{ + Title: "Export source rules", + DefaultFilename: "syslogstudio-source-rules.json", + Filters: []wailsRuntime.FileFilter{{DisplayName: "Source rules (*.json)", Pattern: "*.json"}}, + }) + if err != nil || path == "" { + return "", err + } + if err := os.WriteFile(path, data, 0o644); err != nil { + return "", fmt.Errorf("failed to write the rules: %w", err) + } + return path, nil +} + +// ReadSourceRulesFile asks for a file of exported rules and reads it, every +// rule and template checked as it would be saved. The templates are added to +// this person's; the rules are returned, for the window to save — through the +// Backend, so that they reach the engine it drives. Nothing when the person +// cancelled. +func (a *App) ReadSourceRulesFile() (sources.RuleFile, error) { + path, err := wailsRuntime.OpenFileDialog(a.ctx, wailsRuntime.OpenDialogOptions{ + Title: "Import source rules", + Filters: []wailsRuntime.FileFilter{{DisplayName: "Source rules (*.json)", Pattern: "*.json"}}, + }) + if err != nil || path == "" { + return sources.RuleFile{}, err + } + f, err := os.Open(path) + if err != nil { + return sources.RuleFile{}, err + } + defer f.Close() + data, err := io.ReadAll(io.LimitReader(f, sources.MaxFileSize+1)) + if err != nil { + return sources.RuleFile{}, err + } + file, err := sources.DecodeFile(data) + if err != nil { + return sources.RuleFile{}, err + } + for i, t := range file.Templates { + saved, err := a.SaveSourceTemplate(t) + if err != nil { + return sources.RuleFile{}, err + } + file.Templates[i] = saved + } + return file, nil +} diff --git a/app_sources_test.go b/app_sources_test.go new file mode 100644 index 0000000..c50a5e7 --- /dev/null +++ b/app_sources_test.go @@ -0,0 +1,55 @@ +package main + +import ( + "testing" + + "SyslogStudio/internal/models" + "SyslogStudio/internal/sources" + "SyslogStudio/internal/storage" +) + +// A person's templates: saved without the devices a rule names, offered after +// those that come with SyslogStudio, replaced by ID, deleted. +func TestSourceTemplates_Mine(t *testing.T) { + a := newAppIn(storage.NewConfigStoreAt(t.TempDir())) + builtIn := len(sources.Templates()) + + rule := models.SourceRule{ID: "source-1", Name: "Lobby cameras", Enabled: true, + Match: models.SourceMatch{Sources: []string{"10.10.7.0/24"}}, + Format: models.ImportFormat{Mode: models.ImportCustom, Pattern: `^(?P