From 1fb806ebb1b93175d9c80a68e4828a584b0b7fe0 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Sat, 24 May 2025 17:48:00 +0200 Subject: [PATCH 01/42] refactor: Trim the `k` prefix from types and packages Historically, Fibratus has been envisioned as a tool to exclusively interact with the NT Kernel Logger ETW provider. Since then, more providers have been integrated, some of them operating in userspace. Thus, the assumption that the provenance of all events is coming out the kernel is no longer valid. It is semantically more correct to represent all types, packages, and identifier in a generic way. The most prominent example is the `Kevent` structure being renamed to `Event`. --- .github/workflows/master.yml | 4 +- .github/workflows/pr.yml | 4 +- .github/workflows/release.yml | 2 +- .golangci.yml | 2 +- cmd/fibratus/app/capture/capture_windows.go | 2 +- cmd/fibratus/app/list/list.go | 15 +- cmd/fibratus/app/replay/replay_windows.go | 2 +- cmd/fibratus/app/stats/stats.go | 53 +- filaments/fishy_netio.py | 31 +- filaments/teamviewer_remote_file_copy.py | 6 +- filaments/top_in_packets.py | 6 +- filaments/top_keys.py | 6 +- filaments/top_out_packets.py | 6 +- filaments/utils/dotdict.py | 8 +- filaments/watch_files.py | 8 +- internal/bootstrap/bootstrap.go | 34 +- internal/bootstrap/source.go | 10 +- internal/etw/consumer.go | 29 +- internal/etw/processors/chain.go | 12 +- internal/etw/processors/fs_windows.go | 111 ++-- internal/etw/processors/fs_windows_test.go | 243 ++++--- internal/etw/processors/handle_windows.go | 29 +- .../etw/processors/handle_windows_test.go | 51 +- internal/etw/processors/image_windows.go | 22 +- internal/etw/processors/image_windows_test.go | 81 ++- internal/etw/processors/mem_windows.go | 27 +- internal/etw/processors/mem_windows_test.go | 59 +- internal/etw/processors/net_windows.go | 27 +- internal/etw/processors/net_windows_test.go | 61 +- internal/etw/processors/processor.go | 4 +- internal/etw/processors/ps_windows.go | 43 +- internal/etw/processors/ps_windows_test.go | 165 +++-- internal/etw/processors/registry_windows.go | 39 +- .../etw/processors/registry_windows_test.go | 105 ++- internal/etw/source.go | 73 +-- internal/etw/source_test.go | 255 ++++---- internal/etw/stackext.go | 40 +- internal/etw/stackext_test.go | 16 +- internal/etw/trace.go | 2 +- make.bat | 2 +- pkg/aggregator/aggregator.go | 40 +- pkg/aggregator/aggregator_test.go | 39 +- pkg/aggregator/submitter.go | 4 +- pkg/aggregator/transformers/config.go | 2 +- pkg/aggregator/transformers/remove/config.go | 8 +- pkg/aggregator/transformers/remove/remove.go | 10 +- .../transformers/remove/remove_test.go | 27 +- pkg/aggregator/transformers/rename/config.go | 4 +- pkg/aggregator/transformers/rename/rename.go | 16 +- .../transformers/rename/rename_test.go | 33 +- pkg/aggregator/transformers/replace/config.go | 10 +- .../transformers/replace/replace.go | 14 +- .../transformers/replace/replace_test.go | 21 +- pkg/aggregator/transformers/tags/tags.go | 6 +- pkg/aggregator/transformers/tags/tags_test.go | 31 +- pkg/aggregator/transformers/transformer.go | 10 +- pkg/aggregator/transformers/trim/config.go | 6 +- pkg/aggregator/transformers/trim/trim.go | 24 +- pkg/aggregator/transformers/trim/trim_test.go | 39 +- pkg/aggregator/worker_test.go | 16 +- pkg/alertsender/alert.go | 6 +- pkg/alertsender/alert_test.go | 29 +- pkg/alertsender/eventlog/eventlog_test.go | 37 +- pkg/alertsender/mail/renderer_test.go | 37 +- pkg/alertsender/mail/template.go | 2 +- pkg/alertsender/systray/systray.go | 4 +- .../cap.kcap => cap/_fixtures/cap.cap} | Bin .../cap1.kcap => cap/_fixtures/cap1.cap} | Bin .../cap2.kcap => cap/_fixtures/cap2.cap} | Bin pkg/{kcap => cap}/config.go | 2 +- pkg/{kcap => cap}/header.go | 18 +- pkg/{kcap => cap}/reader.go | 26 +- pkg/{kcap => cap}/reader_unsupported.go | 10 +- pkg/{kcap => cap}/reader_windows.go | 103 ++- pkg/{kcap => cap}/reader_windows_test.go | 16 +- pkg/{kcap => cap}/section/section.go | 10 +- pkg/{kcap => cap}/section/section_windows.go | 10 +- .../section/section_windows_test.go | 2 +- pkg/{kcap => cap}/types_linux.go | 16 +- pkg/{kcap => cap}/types_windows.go | 24 +- pkg/{kcap => cap}/version/version_windows.go | 8 +- pkg/{kcap => cap}/writer.go | 22 +- pkg/{kcap => cap}/writer_unsupported.go | 8 +- pkg/{kcap => cap}/writer_windows.go | 52 +- pkg/{kcap => cap}/writer_windows_test.go | 47 +- pkg/config/_fixtures/fibratus.json | 2 +- pkg/config/_fixtures/fibratus.yml | 14 +- .../filters/default-with-template.yml | 2 +- pkg/config/_fixtures/filters/default.yml | 2 +- pkg/config/_fixtures/filters/default1.yml | 2 +- pkg/config/_fixtures/transformers.yml | 4 +- pkg/config/config_windows.go | 28 +- pkg/config/config_windows_test.go | 4 +- pkg/config/filters.go | 13 +- pkg/config/filters_test.go | 6 +- pkg/config/kstream.go | 30 +- pkg/config/kstream_test.go | 7 +- pkg/config/schema_windows.go | 18 +- pkg/errors/errors.go | 12 +- pkg/errors/errors_windows.go | 12 +- pkg/{kevent => event}/_fixtures/handles.json | 0 pkg/{kevent => event}/batch.go | 12 +- pkg/{kevent => event}/batch_test.go | 79 ++- pkg/{kevent/ktypes => event}/category.go | 4 +- pkg/{kevent => event}/doc.go | 6 +- pkg/{kevent => event}/enum.go | 2 +- pkg/{kevent/kevent.go => event/event.go} | 79 ++- .../event_windows.go} | 465 +++++++------- .../event_windows_test.go} | 51 +- pkg/{kevent/ktypes => event}/eventset.go | 8 +- pkg/{kevent/ktypes => event}/eventset_test.go | 4 +- pkg/{kevent => event}/flags.go | 2 +- pkg/{kevent => event}/flags_test.go | 2 +- pkg/{kevent => event}/formatter.go | 112 ++-- pkg/{kevent => event}/formatter_test.go | 34 +- pkg/{kevent => event}/formatter_windows.go | 38 +- pkg/{kevent => event}/marshaller.go | 2 +- pkg/{kevent => event}/marshaller_test.go | 234 +++---- pkg/{kevent => event}/marshaller_windows.go | 305 +++++---- .../ktypes => event}/metainfo_windows.go | 199 +++--- .../ktypes => event}/metainfo_windows_test.go | 30 +- pkg/{kevent/kparam.go => event/param.go} | 381 ++++++----- pkg/event/param_test.go | 60 ++ .../param_windows.go} | 533 ++++++++-------- .../params/params_windows.go} | 2 +- pkg/event/params/types.go | 27 + .../kparams => event/params}/types_windows.go | 14 +- pkg/{kevent => event}/queue.go | 26 +- pkg/{kevent => event}/queue_test.go | 113 ++-- pkg/{kevent => event}/sequencer_windows.go | 10 +- .../sequencer_windows_test.go | 2 +- pkg/{kevent => event}/stackwalk.go | 20 +- pkg/event/stackwalk_test.go | 129 ++++ pkg/{kevent => event}/template.go | 84 +-- .../types_windows.go} | 92 +-- .../types_windows_test.go} | 24 +- pkg/filament/_fixtures/test_filter.py | 2 +- pkg/filament/_fixtures/test_on_next_kevent.py | 4 +- pkg/filament/_fixtures/top_hives_io.py | 2 +- pkg/filament/_fixtures/top_keys_io_table.py | 2 +- .../cpython/_fixtures/top_hives_io.py | 4 +- pkg/filament/dict.go | 114 ++++ pkg/filament/{kdict_test.go => dict_test.go} | 39 +- pkg/filament/filament.go | 84 +-- pkg/filament/filament_test.go | 43 +- pkg/filament/filament_unsupported.go | 4 +- pkg/filament/kdict.go | 114 ---- pkg/filament/types.go | 4 +- pkg/filter/accessor.go | 94 +-- pkg/filter/accessor_windows.go | 529 ++++++++------- pkg/filter/accessor_windows_test.go | 39 +- pkg/filter/fields/fields.go | 6 +- pkg/filter/fields/fields_windows.go | 544 ++++++++-------- pkg/filter/fields/fields_windows_test.go | 4 +- pkg/filter/filter.go | 34 +- pkg/filter/filter_test.go | 603 +++++++++--------- pkg/filter/filter_windows.go | 2 +- pkg/filter/ql/error_test.go | 4 +- pkg/filter/ql/function.go | 4 +- pkg/filter/ql/literal.go | 21 +- pkg/filter/ql/parser_test.go | 132 ++-- pkg/filter/util.go | 15 +- pkg/handle/snapshotter.go | 26 +- pkg/handle/snapshotter_mock.go | 10 +- pkg/handle/types/marshaller.go | 2 +- pkg/handle/types/types.go | 2 +- pkg/kevent/README.md | 27 - pkg/kevent/kparam_test.go | 60 -- pkg/kevent/stackwalk_test.go | 130 ---- pkg/outputs/amqp/amqp.go | 4 +- pkg/outputs/amqp/amqp_test.go | 81 ++- pkg/outputs/client.go | 4 +- pkg/outputs/console/config.go | 2 +- pkg/outputs/console/console.go | 18 +- pkg/outputs/elasticsearch/elasticsearch.go | 14 +- .../elasticsearch/elasticsearch_test.go | 79 ++- pkg/outputs/elasticsearch/index.go | 6 +- pkg/outputs/elasticsearch/index_test.go | 10 +- pkg/outputs/elasticsearch/template.go | 2 +- pkg/outputs/eventlog/config.go | 4 +- pkg/outputs/eventlog/eventlog.go | 34 +- pkg/outputs/eventlog/eventlog_test.go | 35 +- pkg/outputs/eventlog/mc/gen.go | 10 +- pkg/outputs/http/http.go | 4 +- pkg/outputs/http/http_test.go | 79 ++- pkg/outputs/null/null.go | 4 +- pkg/pe/marshaller.go | 8 +- pkg/pe/marshaller_test.go | 2 +- pkg/ps/snapshotter.go | 16 +- pkg/ps/snapshotter_mock.go | 30 +- pkg/ps/snapshotter_windows.go | 111 ++-- pkg/ps/snapshotter_windows_test.go | 431 +++++++------ pkg/ps/types/marshaller_windows.go | 24 +- pkg/ps/types/marshaller_windows_test.go | 4 +- pkg/ps/types/types_windows.go | 2 +- .../_fixtures/default/microsoft_edge.yml | 2 +- .../default/sequence_rule_simple.yml | 4 +- .../_fixtures/default/suspicious_domains.yml | 2 +- .../default/suspicious_module_loaded.yml | 2 +- ...suspicious_network_connecting_binaries.yml | 2 +- ..._error_reporting_and_wmi_provider_host.yml | 2 +- pkg/rules/_fixtures/kill_action.yml | 2 +- .../_fixtures/merged_filters/filter1.yml | 2 +- .../_fixtures/merged_filters/filter2.yml | 2 +- .../_fixtures/merged_filters/filter3.yml | 2 +- .../_fixtures/merged_filters/filter4.yml | 2 +- .../min_engine_version/fail/filter1.yml | 2 +- .../min_engine_version/fail/filter2.yml | 2 +- .../min_engine_version/fail/filter3.yml | 2 +- .../min_engine_version/ok/filter1.yml | 2 +- .../min_engine_version/ok/filter2.yml | 2 +- .../min_engine_version/ok/filter3.yml | 2 +- pkg/rules/_fixtures/sequence_rule_complex.yml | 6 +- pkg/rules/_fixtures/sequence_rule_ps_uuid.yml | 4 +- .../command_shell_spawned_chrome_browser.yml | 4 +- .../powershell_created_temp_file.yml | 4 +- .../process_spawned_by_powershell.yml | 2 +- .../spawn_chrome_browser.yml | 2 +- pkg/rules/_fixtures/simple_emit_alert.yml | 2 +- pkg/rules/_fixtures/simple_matches.yml | 2 +- .../_fixtures/simple_matches/filter1.yml | 2 +- .../_fixtures/simple_matches/filter2.yml | 2 +- .../_fixtures/simple_matches/filter3.yml | 2 +- .../_fixtures/simple_matches/filter4.yml | 2 +- pkg/rules/compiler.go | 34 +- pkg/rules/compiler_test.go | 12 +- pkg/rules/engine.go | 35 +- pkg/rules/engine_test.go | 227 ++++--- pkg/rules/sequence.go | 55 +- pkg/rules/sequence_test.go | 451 +++++++------ pkg/{ksource => source}/doc.go | 4 +- pkg/{ksource => source}/source.go | 10 +- pkg/symbolize/symbolizer.go | 67 +- pkg/symbolize/symbolizer_test.go | 113 ++-- pkg/sys/etw/etw.go | 20 +- pkg/util/eventlog/eventlog.go | 4 +- pkg/yara/config/config.go | 13 +- pkg/yara/config/config_test.go | 27 +- pkg/yara/scanner.go | 81 ++- pkg/yara/scanner_test.go | 255 ++++---- pkg/yara/types.go | 6 +- 241 files changed, 5506 insertions(+), 5585 deletions(-) rename pkg/{kcap/_fixtures/cap.kcap => cap/_fixtures/cap.cap} (100%) rename pkg/{kcap/_fixtures/cap1.kcap => cap/_fixtures/cap1.cap} (100%) rename pkg/{kcap/_fixtures/cap2.kcap => cap/_fixtures/cap2.cap} (100%) rename pkg/{kcap => cap}/config.go (98%) rename pkg/{kcap => cap}/header.go (70%) rename pkg/{kcap => cap}/reader.go (59%) rename pkg/{kcap => cap}/reader_unsupported.go (84%) rename pkg/{kcap => cap}/reader_windows.go (69%) rename pkg/{kcap => cap}/reader_windows_test.go (72%) rename pkg/{kcap => cap}/section/section.go (83%) rename pkg/{kcap => cap}/section/section_windows.go (89%) rename pkg/{kcap => cap}/section/section_windows_test.go (94%) rename pkg/{kcap => cap}/types_linux.go (64%) rename pkg/{kcap => cap}/types_windows.go (71%) rename pkg/{kcap => cap}/version/version_windows.go (90%) rename pkg/{kcap => cap}/writer.go (67%) rename pkg/{kcap => cap}/writer_unsupported.go (90%) rename pkg/{kcap => cap}/writer_windows.go (83%) rename pkg/{kcap => cap}/writer_windows_test.go (83%) rename pkg/{kevent => event}/_fixtures/handles.json (100%) rename pkg/{kevent => event}/batch.go (86%) rename pkg/{kevent => event}/batch_test.go (78%) rename pkg/{kevent/ktypes => event}/category.go (96%) rename pkg/{kevent => event}/doc.go (81%) rename pkg/{kevent => event}/enum.go (99%) rename pkg/{kevent/kevent.go => event/event.go} (79%) rename pkg/{kevent/kevent_windows.go => event/event_windows.go} (53%) rename pkg/{kevent/kevent_windows_test.go => event/event_windows_test.go} (59%) rename pkg/{kevent/ktypes => event}/eventset.go (93%) rename pkg/{kevent/ktypes => event}/eventset_test.go (98%) rename pkg/{kevent => event}/flags.go (99%) rename pkg/{kevent => event}/flags_test.go (98%) rename pkg/{kevent => event}/formatter.go (72%) rename pkg/{kevent => event}/formatter_test.go (79%) rename pkg/{kevent => event}/formatter_windows.go (66%) rename pkg/{kevent => event}/marshaller.go (99%) rename pkg/{kevent => event}/marshaller_test.go (71%) rename pkg/{kevent => event}/marshaller_windows.go (74%) rename pkg/{kevent/ktypes => event}/metainfo_windows.go (74%) rename pkg/{kevent/ktypes => event}/metainfo_windows_test.go (57%) rename pkg/{kevent/kparam.go => event/param.go} (61%) create mode 100644 pkg/event/param_test.go rename pkg/{kevent/kparam_windows.go => event/param_windows.go} (50%) rename pkg/{kevent/kparams/fields_windows.go => event/params/params_windows.go} (99%) create mode 100644 pkg/event/params/types.go rename pkg/{kevent/kparams => event/params}/types_windows.go (93%) rename pkg/{kevent => event}/queue.go (90%) rename pkg/{kevent => event}/queue_test.go (51%) rename pkg/{kevent => event}/sequencer_windows.go (92%) rename pkg/{kevent => event}/sequencer_windows_test.go (99%) rename pkg/{kevent => event}/stackwalk.go (92%) create mode 100644 pkg/event/stackwalk_test.go rename pkg/{kevent => event}/template.go (59%) rename pkg/{kevent/ktypes/ktypes_windows.go => event/types_windows.go} (92%) rename pkg/{kevent/ktypes/ktypes_windows_test.go => event/types_windows_test.go} (89%) create mode 100644 pkg/filament/dict.go rename pkg/filament/{kdict_test.go => dict_test.go} (76%) delete mode 100644 pkg/filament/kdict.go delete mode 100644 pkg/kevent/README.md delete mode 100644 pkg/kevent/kparam_test.go delete mode 100644 pkg/kevent/stackwalk_test.go rename pkg/{ksource => source}/doc.go (87%) rename pkg/{ksource => source}/source.go (91%) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 780abe967..95ce6d833 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -84,7 +84,7 @@ jobs: ./make.bat mc ./make.bat env: - TAGS: kcap,filament,yara,yara_static + TAGS: cap,filament,yara,yara_static - uses: actions/upload-artifact@v4 with: name: "fibratus-amd64.exe" @@ -176,7 +176,7 @@ jobs: export PKG_CONFIG_PATH=$(pwd)/pkg-config ./make.bat test env: - TAGS: kcap,yara,yara_static + TAGS: cap,yara,yara_static lint: runs-on: windows-latest diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 656aca943..1f08d0888 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -86,7 +86,7 @@ jobs: ./make.bat mc ./make.bat env: - TAGS: kcap,filament,yara,yara_static + TAGS: cap,filament,yara,yara_static - uses: actions/upload-artifact@v4 with: name: "fibratus-amd64.exe" @@ -158,7 +158,7 @@ jobs: export PKG_CONFIG_PATH=$(pwd)/pkg-config ./make.bat test env: - TAGS: kcap,yara,yara_static + TAGS: cap,yara,yara_static lint: runs-on: windows-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 43944be4d..df7a782eb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,7 +85,7 @@ jobs: ./make.bat mc ./make.bat env: - TAGS: kcap,filament,yara,yara_static + TAGS: cap,filament,yara,yara_static - name: Install Wix shell: bash run: | diff --git a/.golangci.yml b/.golangci.yml index b107ad968..5cded955a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,6 +1,6 @@ run: build-tags: - - kcap + - cap - filament deadline: 10m diff --git a/cmd/fibratus/app/capture/capture_windows.go b/cmd/fibratus/app/capture/capture_windows.go index 98dae1631..99b7ea2ba 100644 --- a/cmd/fibratus/app/capture/capture_windows.go +++ b/cmd/fibratus/app/capture/capture_windows.go @@ -29,7 +29,7 @@ import ( var Command = &cobra.Command{ Use: "capture [filter]", - Short: "Capture event stream to the kcap (capture) file", + Short: "Capture event stream to the cap (capture) file", RunE: capture, } diff --git a/cmd/fibratus/app/list/list.go b/cmd/fibratus/app/list/list.go index d9aeb124a..92ab1eff6 100644 --- a/cmd/fibratus/app/list/list.go +++ b/cmd/fibratus/app/list/list.go @@ -24,8 +24,8 @@ import ( "github.com/jedib0t/go-pretty/v6/table" "github.com/rabbitstack/fibratus/internal/bootstrap" "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/filter/fields" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/spf13/cobra" "os" "path/filepath" @@ -34,7 +34,7 @@ import ( var Command = &cobra.Command{ Use: "list", - Short: "Show info about filaments, filter fields or kernel event types", + Short: "Show info about filaments, filter fields or event types", } var listFilamentsCmd = &cobra.Command{ @@ -50,10 +50,9 @@ var listFieldsCmd = &cobra.Command{ } var listEventsCmd = &cobra.Command{ - Use: "kevents", - Aliases: []string{"events"}, - Short: "List supported kernel event types", - Run: listEvents, + Use: "events", + Short: "List supported event types", + Run: listEvents, } var cfg = config.NewWithOpts(config.WithList()) @@ -131,8 +130,8 @@ func listEvents(cmd *cobra.Command, args []string) { t.AppendHeader(table.Row{"Name", "Category", "Description"}) t.SetStyle(table.StyleLight) - for _, ktyp := range ktypes.GetKtypesMeta() { - t.AppendRow(table.Row{ktyp.Name, ktyp.Category, ktyp.Description}) + for _, ev := range event.GetTypesMeta() { + t.AppendRow(table.Row{ev.Name, ev.Category, ev.Description}) } t.Render() diff --git a/cmd/fibratus/app/replay/replay_windows.go b/cmd/fibratus/app/replay/replay_windows.go index e30301217..f9e891184 100644 --- a/cmd/fibratus/app/replay/replay_windows.go +++ b/cmd/fibratus/app/replay/replay_windows.go @@ -28,7 +28,7 @@ import ( var Command = &cobra.Command{ Use: "replay", - Short: "Replay event stream from the kcap (capture) file", + Short: "Replay event stream from the cap (capture) file", RunE: replay, } diff --git a/cmd/fibratus/app/stats/stats.go b/cmd/fibratus/app/stats/stats.go index cd29c088d..1335217ed 100644 --- a/cmd/fibratus/app/stats/stats.go +++ b/cmd/fibratus/app/stats/stats.go @@ -47,13 +47,13 @@ func init() { type Stats struct { AggregatorBatchEvents int `json:"aggregator.batch.events"` AggregatorFlushesCount int `json:"aggregator.flushes.count"` - AggregatorKeventErrors int `json:"aggregator.kevent.errors"` + AggregatorEventErrors int `json:"aggregator.event.errors"` AggregatorTransformerErrors map[string]int `json:"aggregator.transformer.errors"` AggregatorWorkerClientPublishErrors int `json:"aggregator.worker.client.publish.errors"` - FilamentKdictErrors int `json:"filament.kdict.errors"` - FilamentKeventBatchFlushes int `json:"filament.kevent.batch.flushes"` - FilamentKeventErrors map[string]int `json:"filament.kevent.errors"` - FilamentKeventProcessErrors int `json:"filament.kevent.process.errors"` + FilamentDictErrors int `json:"filament.dict.errors"` + FilamentEventBatchFlushes int `json:"filament.event.batch.flushes"` + FilamentEventErrors map[string]int `json:"filament.event.errors"` + FilamentEventProcessErrors int `json:"filament.event.process.errors"` FilterAccessorErrors map[string]int `json:"filter.accessor.errors"` FsFileObjectHandleHits int `json:"fs.file.object.handle.hits"` FsFileObjectMisses int `json:"fs.file.object.misses"` @@ -67,28 +67,27 @@ type Stats struct { HandleTypeNameMisses int `json:"handle.type.name.misses"` HandleWaitTimeouts int `json:"handle.wait.timeouts"` HostnameErrors map[string]int `json:"hostname.errors"` - KcapFlusherErrors map[string]int `json:"kcap.flusher.errors"` - KcapHandleWriteErrors int `json:"kcap.handle.write.errors"` - KcapKeventUnmarshalErrors int `json:"kcap.kevent.unmarshal.errors"` - KcapKeventWriteErrors int `json:"kcap.kevent.write.errors"` - KcapKstreamConsumerErrors int `json:"kcap.kstream.consumer.errors"` - KcapOverflowErrors int `json:"kcap.overflow.errors"` - KcapReadBytes int `json:"kcap.read.bytes"` - KcapReadKevents int `json:"kcap.read.kevents"` - KcapReaderDroppedByFilter int `json:"kcap.reader.dropped.by.filter"` - KcapReaderHandleUnmarshalErrors int `json:"kcap.reader.handle.unmarshal.errors"` - KeventPrcoessorFailures int `json:"kevent.processor.failures"` - KeventSeqInitErrors map[string]int `json:"kevent.seq.init.errors"` - KeventSeqStoreErrors int `json:"kevent.seq.store.errors"` - KeventTimestampUnmarshalErrors int `json:"kevent.timestamp.unmarshal.errors"` - KstreamDroppedKevents int `json:"kstream.dropped.kevents"` - KstreamKbuffersRead int `json:"kstream.kbuffers.read"` - KstreamKeventsEnqueued int `json:"kstream.kevents.enqueued"` - KstreamKeventsDequeued int `json:"kstream.kevents.dequeued"` - KstreamUnknownKevents int `json:"kstream.kevents.unknown"` - KstreamKeventsProcessed int `json:"kstream.kevents.processed"` - KstreamExcludedKevents int `json:"kstream.excluded.kevents"` - KstreamKeventsFailures map[string]int `json:"kstream.kevents.failures"` + CapFlusherErrors map[string]int `json:"cap.flusher.errors"` + CapHandleWriteErrors int `json:"cap.handle.write.errors"` + CapEventUnmarshalErrors int `json:"cap.event.unmarshal.errors"` + CapEventWriteErrors int `json:"cap.event.write.errors"` + CapEventSourceConsumerErrors int `json:"cap.eventsource.consumer.errors"` + CapOverflowErrors int `json:"cap.overflow.errors"` + CapReadBytes int `json:"cap.read.bytes"` + CapReadEvents int `json:"cap.read.events"` + CapReaderDroppedByFilter int `json:"cap.reader.dropped.by.filter"` + CapReaderHandleUnmarshalErrors int `json:"cap.reader.handle.unmarshal.errors"` + EventProcessorFailures int `json:"event.processor.failures"` + EventSeqInitErrors map[string]int `json:"event.seq.init.errors"` + EventSeqStoreErrors int `json:"event.seq.store.errors"` + EventTimestampUnmarshalErrors int `json:"event.timestamp.unmarshal.errors"` + EventSourceBuffersRead int `json:"eventsource.buffers.read"` + EventSourceEventsEnqueued int `json:"eventsource.events.enqueued"` + EventSourceEventsDequeued int `json:"eventsource.events.dequeued"` + EventSourceUnknownEvents int `json:"eventsource.events.unknown"` + EventSourceEventsProcessed int `json:"eventsource.events.processed"` + EventSourceExcludedEvents int `json:"eventsource.excluded.events"` + EventSourceEventsFailures map[string]int `json:"eventsource.events.failures"` LoggerErrors map[string]int `json:"logger.errors"` OutputAMQPChannelFailures int `json:"output.amqp.channel.failures"` OutputAMQPConnectionFailures int `json:"output.amqp.connection.failures"` diff --git a/filaments/fishy_netio.py b/filaments/fishy_netio.py index 87451de3f..a15570019 100644 --- a/filaments/fishy_netio.py +++ b/filaments/fishy_netio.py @@ -29,24 +29,23 @@ def on_init(): - kfilter("kevt.category = 'net' and ps.name in (%s)" % (', '.join([f'\'{ps}\'' for ps in __procs__]))) + set_filter("evt.category = 'net' and ps.name in (%s)" % (', '.join([f'\'{ps}\'' for ps in __procs__]))) @dotdictify -def on_next_kevent(kevent): - print(kevent) - notify = True if kevent.pid in __pids__ else False +def on_next_event(event): + notify = True if event.pid in __pids__ else False if not notify: emit_alert( - f'Anomalous network I/O detected to {kevent.kparams.dip}:{kevent.kparams.dport}', - text(kevent), + f'Anomalous network I/O detected to {event.params.dip}:{event.params.dport}', + text(Event), severity='critical', tags=['anomalous netio'] ) - __pids__.append(kevent.pid) + __pids__.append(event.pid) -def text(kevent): +def text(event): return """ Source IP: %s @@ -63,11 +62,11 @@ def text(kevent): User: %s """ % ( - kevent.kparams.sip, - kevent.kparams.sport, - kevent.kparams.dip, - kevent.kparams.dport, - kevent.kparams.dport_name, - kevent.exe, - kevent.comm, - kevent.cwd, kevent.sid) + event.params.sip, + event.params.sport, + event.params.dip, + event.params.dport, + event.params.dport_name, + event.exe, + event.comm, + event.cwd, event.sid) diff --git a/filaments/teamviewer_remote_file_copy.py b/filaments/teamviewer_remote_file_copy.py index e103dbbc4..5e8ae067b 100644 --- a/filaments/teamviewer_remote_file_copy.py +++ b/filaments/teamviewer_remote_file_copy.py @@ -52,16 +52,16 @@ def on_init(): - kfilter("kevt.name = 'CreateFile' and ps.name = 'TeamViewer.exe' and file.operation = 'create' " + kfilter("evt.name = 'CreateFile' and ps.name = 'TeamViewer.exe' and file.operation = 'create' " "and file.extension in (%s)" % (', '.join([f'\'{ext}\'' for ext in extensions]))) @dotdictify -def on_next_kevent(kevent): +def on_next_kevent(event): emit_alert( f'Remote File Copy via TeamViewer', - f'TeamViewer downloaded an executable or script file {kevent.kparams.file_name} via transfer session', + f'TeamViewer downloaded an executable or script file {event.params.file_name} via transfer session', severity=__severity__, tags=[__tags__] ) diff --git a/filaments/top_in_packets.py b/filaments/top_in_packets.py index fa51db168..5edd1f35d 100644 --- a/filaments/top_in_packets.py +++ b/filaments/top_in_packets.py @@ -25,15 +25,15 @@ def on_init(): - kfilter("kevt.name = 'Recv'") + set_filter("evt.name = 'Recv'") columns(["Source", "Count"]) sort_by('Count') interval(1) @dotdictify -def on_next_kevent(kevent): - src = ['%s:%d' % (kevent.kparams.sip, kevent.kparams.sport)] +def on_next_event(event): + src = ['%s:%d' % (event.params.sip, event.params.sport)] __connections__.update(src) diff --git a/filaments/top_keys.py b/filaments/top_keys.py index d8580bae8..6dcf7ddf6 100644 --- a/filaments/top_keys.py +++ b/filaments/top_keys.py @@ -26,15 +26,15 @@ def on_init(): - kfilter("kevt.category = 'registry'") + set_filter("evt.category = 'registry'") columns(["Key", "#Ops"]) sort_by('#Ops') interval(1) @dotdictify -def on_next_kevent(kevent): - key = kevent.kparams.key_name +def on_next_event(event): + key = event.params.key_name if key: __keys__.update((key, )) diff --git a/filaments/top_out_packets.py b/filaments/top_out_packets.py index ac00e10b2..25bdb61ad 100644 --- a/filaments/top_out_packets.py +++ b/filaments/top_out_packets.py @@ -25,15 +25,15 @@ def on_init(): - kfilter("kevt.name = 'Send'") + set_filter("evt.name = 'Send'") columns(["Destination", "Count"]) sort_by('Count') interval(1) @dotdictify -def on_next_kevent(kevent): - dst = ['%s:%d' % (kevent.kparams.dip, kevent.kparams.dport)] +def on_next_event(event): + dst = ['%s:%d' % (event.params.dip, event.params.dport)] __connections__.update(dst) diff --git a/filaments/utils/dotdict.py b/filaments/utils/dotdict.py index bda93f304..9aec3260a 100644 --- a/filaments/utils/dotdict.py +++ b/filaments/utils/dotdict.py @@ -10,8 +10,8 @@ def dotdictify(fn): """ The decorator for converting the dict parameter to dot notation access dictionary. """ - def __wrap(kevent): - kevent = dotdict(kevent) - kevent.kparams = dotdict(kevent.kparams) - return fn(kevent) + def __wrap(event): + event = dotdict(event) + event.params = dotdict(event.params) + return fn(event) return __wrap diff --git a/filaments/watch_files.py b/filaments/watch_files.py index 4fe4a5886..69da265fd 100644 --- a/filaments/watch_files.py +++ b/filaments/watch_files.py @@ -24,15 +24,15 @@ def on_init(): - kfilter("kevt.name = 'CreateFile' and file.operation != 'OPEN'") + set_filter("evt.name = 'CreateFile' and file.operation != 'OPEN'") columns(["Process", "File"]) @dotdictify -def on_next_kevent(kevent): - file_name = kevent.kparams.file_name +def on_next_event(event): + file_name = event.params.file_name if file_name: - __files__.append((kevent.exe, file_name, )) + __files__.append((event.exe, file_name, )) for f in __files__: add_row([f[0], f[1]]) render_table() diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go index 6c9c67e5a..395c8d4b5 100644 --- a/internal/bootstrap/bootstrap.go +++ b/internal/bootstrap/bootstrap.go @@ -24,11 +24,11 @@ import ( "github.com/rabbitstack/fibratus/pkg/aggregator" "github.com/rabbitstack/fibratus/pkg/alertsender" "github.com/rabbitstack/fibratus/pkg/api" + "github.com/rabbitstack/fibratus/pkg/cap" "github.com/rabbitstack/fibratus/pkg/config" "github.com/rabbitstack/fibratus/pkg/filament" "github.com/rabbitstack/fibratus/pkg/filter" "github.com/rabbitstack/fibratus/pkg/handle" - "github.com/rabbitstack/fibratus/pkg/kcap" "github.com/rabbitstack/fibratus/pkg/ps" "github.com/rabbitstack/fibratus/pkg/rules" "github.com/rabbitstack/fibratus/pkg/symbolize" @@ -58,8 +58,8 @@ type App struct { psnap ps.Snapshotter filament filament.Filament agg *aggregator.BufferedAggregator - writer kcap.Writer - reader kcap.Reader + writer cap.Writer + reader cap.Reader signals chan struct{} } @@ -120,7 +120,7 @@ func NewApp(cfg *config.Config, options ...Option) (*App, error) { sigs = signals.Install() } if opts.isCaptureReplay { - reader, err := kcap.NewReader(cfg.KcapFile, cfg) + reader, err := cap.NewReader(cfg.KcapFile, cfg) if err != nil { return nil, err } @@ -186,12 +186,12 @@ func (f *App) Run(args []string) error { // build the filter from the CLI argument. If we got // a valid expression the filter is attached to the // event consumer - kfilter, err := filter.NewFromCLI(args, cfg) + fltr, err := filter.NewFromCLI(args, cfg) if err != nil { return err } - if kfilter != nil { - f.evs.SetFilter(kfilter) + if fltr != nil { + f.evs.SetFilter(fltr) } // user can either instruct to bootstrap a filament or // start a regular run. We'll set up the corresponding @@ -277,18 +277,18 @@ func (f *App) WriteCapture(args []string) error { return ErrAlreadyRunning } - kfilter, err := filter.NewFromCLI(args, f.config) + fltr, err := filter.NewFromCLI(args, f.config) if err != nil { return err } - if kfilter != nil { - f.evs.SetFilter(kfilter) + if fltr != nil { + f.evs.SetFilter(fltr) } err = f.evs.Open(f.config) if err != nil { return err } - f.writer, err = kcap.NewWriter(f.config.KcapFile, f.psnap, f.hsnap) + f.writer, err = cap.NewWriter(f.config.KcapFile, f.psnap, f.hsnap) if err != nil { return err } @@ -306,7 +306,7 @@ func (f *App) ReadCapture(ctx context.Context, args []string) error { if f.reader == nil { panic("reader is nil") } - kfilter, err := filter.NewFromCLIWithAllAccessors(args) + fltr, err := filter.NewFromCLIWithAllAccessors(args) if err != nil { return err } @@ -323,10 +323,10 @@ func (f *App) ReadCapture(ctx context.Context, args []string) error { if f.filament.Filter() != nil { // filament filter overrides CLI filter f.reader.SetFilter(f.filament.Filter()) - } else if kfilter != nil { - f.reader.SetFilter(kfilter) + } else if fltr != nil { + f.reader.SetFilter(fltr) } - // returns the channel where events are read from the kcap + // returns the channel where events are read from the cap evts, errs := f.reader.Read(ctx) go func() { defer f.filament.Close() @@ -337,8 +337,8 @@ func (f *App) ReadCapture(ctx context.Context, args []string) error { } }() } else { - if kfilter != nil { - f.reader.SetFilter(kfilter) + if fltr != nil { + f.reader.SetFilter(fltr) } // use the channels where events are read // from the capture as aggregator source diff --git a/internal/bootstrap/source.go b/internal/bootstrap/source.go index 6a8eb8f3e..683ee22f9 100644 --- a/internal/bootstrap/source.go +++ b/internal/bootstrap/source.go @@ -21,11 +21,11 @@ package bootstrap import ( "github.com/rabbitstack/fibratus/internal/etw" "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/filter" "github.com/rabbitstack/fibratus/pkg/handle" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/ksource" "github.com/rabbitstack/fibratus/pkg/ps" + "github.com/rabbitstack/fibratus/pkg/source" ) // EventSourceControl abstracts away the management of event sources. @@ -37,7 +37,7 @@ import ( // of telemetry than the ETW subsystem. In this scenario, the event source // control will bootstrap the instrumentation engine based on eBPF. type EventSourceControl struct { - evs ksource.EventSource + evs source.EventSource } func NewEventSourceControl( @@ -61,7 +61,7 @@ func (s *EventSourceControl) Errors() <-chan error { return s.evs.Errors() } -func (s *EventSourceControl) Events() <-chan *kevent.Kevent { +func (s *EventSourceControl) Events() <-chan *event.Event { return s.evs.Events() } @@ -69,6 +69,6 @@ func (s *EventSourceControl) SetFilter(f filter.Filter) { s.evs.SetFilter(f) } -func (s *EventSourceControl) RegisterEventListener(lis kevent.Listener) { +func (s *EventSourceControl) RegisterEventListener(lis event.Listener) { s.evs.RegisterEventListener(lis) } diff --git a/internal/etw/consumer.go b/internal/etw/consumer.go index a1b8ccefd..f3d4cff65 100644 --- a/internal/etw/consumer.go +++ b/internal/etw/consumer.go @@ -21,10 +21,9 @@ package etw import ( "github.com/rabbitstack/fibratus/internal/etw/processors" "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/filter" "github.com/rabbitstack/fibratus/pkg/handle" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/rabbitstack/fibratus/pkg/ps" "github.com/rabbitstack/fibratus/pkg/sys/etw" ) @@ -36,8 +35,8 @@ import ( // with additional attributes. The event is sent to the queue where // all registered listeners are executed. type Consumer struct { - q *kevent.Queue - sequencer *kevent.Sequencer + q *event.Queue + sequencer *event.Sequencer processors processors.Chain psnap ps.Snapshotter config *config.Config @@ -50,11 +49,11 @@ func NewConsumer( psnap ps.Snapshotter, hsnap handle.Snapshotter, config *config.Config, - sequencer *kevent.Sequencer, - evts chan *kevent.Kevent, + sequencer *event.Sequencer, + evts chan *event.Event, ) *Consumer { return &Consumer{ - q: kevent.NewQueueWithChannel(evts, config.Kstream.StackEnrichment, config.ForwardMode || config.IsCaptureSet()), + q: event.NewQueueWithChannel(evts, config.Kstream.StackEnrichment, config.ForwardMode || config.IsCaptureSet()), sequencer: sequencer, processors: processors.NewChain(psnap, hsnap, config), psnap: psnap, @@ -75,22 +74,22 @@ func (c *Consumer) ProcessEvent(ev *etw.EventRecord) error { if c.isClosing { return nil } - if kevent.IsCurrentProcDropped(ev.Header.ProcessID) { + if event.IsCurrentProcDropped(ev.Header.ProcessID) { return nil } if c.config.Kstream.ExcludeKevent(ev.Header.ProviderID, ev.HookID()) { - excludedKevents.Add(1) + eventsExcluded.Add(1) return nil } - ktype := ktypes.NewFromEventRecord(ev) - if !ktype.Exists() { - keventsUnknown.Add(1) + etype := event.NewFromEventRecord(ev) + if !etype.Exists() { + eventsUnknown.Add(1) return nil } - keventsProcessed.Add(1) - evt := kevent.New(c.sequencer.Get(), ktype, ev) + eventsProcessed.Add(1) + evt := event.New(c.sequencer.Get(), etype, ev) // Dispatch each event to the processor chain. // Processors may further augment the event with @@ -131,7 +130,7 @@ func (c *Consumer) ProcessEvent(ev *etw.EventRecord) error { // decide whether it should get dropped if (evt.IsDropped(c.config.IsCaptureSet()) || c.config.Kstream.ExcludeImage(evt.PS)) && !evt.IsStackWalk() { - keventsDropped.Add(1) + eventsExcluded.Add(1) return nil } if c.filter != nil && !evt.IsStackWalk() && !c.filter.Run(evt) { diff --git a/internal/etw/processors/chain.go b/internal/etw/processors/chain.go index e2e19fa5f..81012a919 100644 --- a/internal/etw/processors/chain.go +++ b/internal/etw/processors/chain.go @@ -21,19 +21,19 @@ package processors import ( "expvar" "fmt" - "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/util/multierror" ) // processorFailures counts the number of failures caused by event processors -var processorFailures = expvar.NewInt("kevent.processor.failures") +var processorFailures = expvar.NewInt("event.processor.failures") // Chain defines the event process chain has to satisfy. type Chain interface { // ProcessEvent pushes the event into processor chain. Processors are applied sequentially, so we have to make // sure that any processor providing additional context to the next processor is defined first in the chain. If // one processor fails, the next processor in chain is invoked. - ProcessEvent(kevt *kevent.Kevent) (*kevent.Kevent, error) + ProcessEvent(evt *event.Event) (*event.Event, error) // Close closes the processor chain and frees all allocated resources. Close() error } @@ -45,14 +45,14 @@ func (c *chain) addProcessor(processor Processor) { c.processors = append(c.processors, processor) } -func (c chain) ProcessEvent(kevt *kevent.Kevent) (*kevent.Kevent, error) { +func (c chain) ProcessEvent(e *event.Event) (*event.Event, error) { var errs = make([]error, 0) - var evt *kevent.Kevent + var evt *event.Event for _, processor := range c.processors { var err error var next bool - evt, next, err = processor.ProcessEvent(kevt) + evt, next, err = processor.ProcessEvent(e) if err != nil { processorFailures.Add(1) errs = append(errs, fmt.Errorf("%q processor failed with error: %v", processor.Name(), err)) diff --git a/internal/etw/processors/fs_windows.go b/internal/etw/processors/fs_windows.go index 0f46dca0b..b1090ee60 100644 --- a/internal/etw/processors/fs_windows.go +++ b/internal/etw/processors/fs_windows.go @@ -21,12 +21,11 @@ package processors import ( "expvar" "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/fs" "github.com/rabbitstack/fibratus/pkg/handle" htypes "github.com/rabbitstack/fibratus/pkg/handle/types" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/rabbitstack/fibratus/pkg/ps" "github.com/rabbitstack/fibratus/pkg/sys" "github.com/rabbitstack/fibratus/pkg/util/va" @@ -56,14 +55,14 @@ type fsProcessor struct { psnap ps.Snapshotter // irps contains a mapping between the IRP (I/O request packet) and CreateFile events - irps map[uint64]*kevent.Kevent + irps map[uint64]*event.Event devMapper fs.DevMapper devPathResolver fs.DevPathResolver config *config.Config // buckets stores stack walk events per stack id - buckets map[uint64][]*kevent.Kevent + buckets map[uint64][]*event.Event mu sync.Mutex purger *time.Ticker @@ -87,13 +86,13 @@ func newFsProcessor( ) Processor { f := &fsProcessor{ files: make(map[uint64]*FileInfo), - irps: make(map[uint64]*kevent.Kevent), + irps: make(map[uint64]*event.Event), hsnap: hsnap, psnap: psnap, devMapper: devMapper, devPathResolver: devPathResolver, config: config, - buckets: make(map[uint64][]*kevent.Kevent), + buckets: make(map[uint64][]*event.Event), purger: time.NewTicker(time.Second * 5), quit: make(chan struct{}, 1), lim: rate.NewLimiter(30, 40), // allow 30 parse ops per second or bursts of 40 ops @@ -104,8 +103,8 @@ func newFsProcessor( return f } -func (f *fsProcessor) ProcessEvent(e *kevent.Kevent) (*kevent.Kevent, bool, error) { - if e.Category == ktypes.File || e.IsStackWalk() { +func (f *fsProcessor) ProcessEvent(e *event.Event) (*event.Event, bool, error) { + if e.Category == event.File || e.IsStackWalk() { evt, err := f.processEvent(e) return evt, false, err } @@ -119,14 +118,14 @@ func (f *fsProcessor) getFileInfo(name string, opts uint32) *FileInfo { return &FileInfo{Name: name, Type: fs.GetFileType(name, opts)} } -func (f *fsProcessor) processEvent(e *kevent.Kevent) (*kevent.Kevent, error) { +func (f *fsProcessor) processEvent(e *event.Event) (*event.Event, error) { switch e.Type { - case ktypes.FileRundown: + case event.FileRundown: // when the file rundown event comes in we store the file info // in internal state in order to augment the rest of file events // that lack the file path field - filepath := e.GetParamAsString(kparams.FilePath) - fileObject, err := e.Kparams.GetUint64(kparams.FileObject) + filepath := e.GetParamAsString(params.FilePath) + fileObject, err := e.Params.GetUint64(params.FileObject) if err != nil { return nil, err } @@ -134,35 +133,35 @@ func (f *fsProcessor) processEvent(e *kevent.Kevent) (*kevent.Kevent, error) { totalRundownFiles.Add(1) f.files[fileObject] = &FileInfo{Name: filepath, Type: fs.GetFileType(filepath, 0)} } - case ktypes.MapFileRundown: - fileKey := e.Kparams.MustGetUint64(kparams.FileKey) + case event.MapFileRundown: + fileKey := e.Params.MustGetUint64(params.FileKey) fileinfo := f.files[fileKey] if fileinfo != nil { totalMapRundownFiles.Add(1) - e.AppendParam(kparams.FilePath, kparams.Path, fileinfo.Name) + e.AppendParam(params.FilePath, params.Path, fileinfo.Name) } else { // if the view of section is backed by the data/image file // try to get the mapped file name and append it to params - sec := e.Kparams.MustGetUint32(kparams.FileViewSectionType) + sec := e.Params.MustGetUint32(params.FileViewSectionType) isMapped := sec != va.SectionPagefile && sec != va.SectionPhysical if isMapped { totalMapRundownFiles.Add(1) - addr := e.Kparams.MustGetUint64(kparams.FileViewBase) + (e.Kparams.MustGetUint64(kparams.FileOffset)) - e.AppendParam(kparams.FilePath, kparams.Path, f.getMappedFile(e.PID, addr)) + addr := e.Params.MustGetUint64(params.FileViewBase) + (e.Params.MustGetUint64(params.FileOffset)) + e.AppendParam(params.FilePath, params.Path, f.getMappedFile(e.PID, addr)) } } return e, f.psnap.AddMmap(e) - case ktypes.CreateFile: + case event.CreateFile: // we defer the processing of the CreateFile event until we get // the matching FileOpEnd event. This event contains the operation // that was done on behalf of the file, e.g. create or open. - irp := e.Kparams.MustGetUint64(kparams.FileIrpPtr) + irp := e.Params.MustGetUint64(params.FileIrpPtr) e.WaitEnqueue = true f.irps[irp] = e - case ktypes.StackWalk: - if !kevent.IsCurrentProcDropped(e.PID) { + case event.StackWalk: + if !event.IsCurrentProcDropped(e.PID) { f.mu.Lock() defer f.mu.Unlock() @@ -170,18 +169,18 @@ func (f *fsProcessor) processEvent(e *kevent.Kevent) (*kevent.Kevent, error) { id := e.StackID() q, ok := f.buckets[id] if !ok { - f.buckets[id] = []*kevent.Kevent{e} + f.buckets[id] = []*event.Event{e} } else { f.buckets[id] = append(q, e) } } - case ktypes.FileOpEnd: + case event.FileOpEnd: // get the CreateFile pending event by IRP identifier // and fetch the file create disposition value var ( - irp = e.Kparams.MustGetUint64(kparams.FileIrpPtr) - dispo = e.Kparams.MustGetUint64(kparams.FileExtraInfo) - status = e.Kparams.MustGetUint32(kparams.NTStatus) + irp = e.Params.MustGetUint64(params.FileIrpPtr) + dispo = e.Params.MustGetUint64(params.FileExtraInfo) + status = e.Params.MustGetUint32(params.NTStatus) ) if dispo > windows.FILE_MAXIMUM_DISPOSITION { @@ -196,28 +195,28 @@ func (f *fsProcessor) processEvent(e *kevent.Kevent) (*kevent.Kevent, error) { // reset the wait status to allow passage of this event to // the aggregator queue. Additionally, append params to it ev.WaitEnqueue = false - fileObject := ev.Kparams.MustGetUint64(kparams.FileObject) + fileObject := ev.Params.MustGetUint64(params.FileObject) // try to get extended file info. If the file object is already // present in the map, we'll reuse the existing file information fileinfo, ok := f.files[fileObject] if !ok { - opts := ev.Kparams.MustGetUint32(kparams.FileCreateOptions) + opts := ev.Params.MustGetUint32(params.FileCreateOptions) opts &= 0xFFFFFF - filepath := ev.GetParamAsString(kparams.FilePath) + filepath := ev.GetParamAsString(params.FilePath) fileinfo = f.getFileInfo(filepath, opts) f.files[fileObject] = fileinfo } if f.config.Kstream.EnableHandleKevents { - f.devPathResolver.AddPath(ev.GetParamAsString(kparams.FilePath)) + f.devPathResolver.AddPath(ev.GetParamAsString(params.FilePath)) } - ev.AppendParam(kparams.NTStatus, kparams.Status, status) + ev.AppendParam(params.NTStatus, params.Status, status) if fileinfo.Type != fs.Unknown { - ev.AppendEnum(kparams.FileType, uint32(fileinfo.Type), fs.FileTypes) + ev.AppendEnum(params.FileType, uint32(fileinfo.Type), fs.FileTypes) } - ev.AppendEnum(kparams.FileOperation, uint32(dispo), fs.FileCreateDispositions) + ev.AppendEnum(params.FileOperation, uint32(dispo), fs.FileCreateDispositions) // attach stack walk return addresses. CreateFile events // represent an edge case in callstack enrichment. Since @@ -236,10 +235,10 @@ func (f *fsProcessor) processEvent(e *kevent.Kevent) (*kevent.Kevent, error) { id := ev.StackID() q, ok := f.buckets[id] if ok && len(q) > 0 { - var s *kevent.Kevent + var s *event.Event s, f.buckets[id] = q[len(q)-1], q[:len(q)-1] - callstack := s.Kparams.MustGetSlice(kparams.Callstack) - ev.AppendParam(kparams.Callstack, kparams.Slice, callstack) + callstack := s.Params.MustGetSlice(params.Callstack) + ev.AppendParam(params.Callstack, params.Slice, callstack) } } @@ -249,30 +248,30 @@ func (f *fsProcessor) processEvent(e *kevent.Kevent) (*kevent.Kevent, error) { fsFileCharacteristicsRateLimits.Add(1) return ev, nil } - path := ev.GetParamAsString(kparams.FilePath) + path := ev.GetParamAsString(params.FilePath) c, err := parseImageFileCharacteristics(path) if err != nil { return ev, nil } - ev.AppendParam(kparams.FileIsDLL, kparams.Bool, c.isDLL) - ev.AppendParam(kparams.FileIsDriver, kparams.Bool, c.isDriver) - ev.AppendParam(kparams.FileIsExecutable, kparams.Bool, c.isExe) - ev.AppendParam(kparams.FileIsDotnet, kparams.Bool, c.isDotnet) + ev.AppendParam(params.FileIsDLL, params.Bool, c.isDLL) + ev.AppendParam(params.FileIsDriver, params.Bool, c.isDriver) + ev.AppendParam(params.FileIsExecutable, params.Bool, c.isExe) + ev.AppendParam(params.FileIsDotnet, params.Bool, c.isDotnet) } return ev, nil - case ktypes.ReleaseFile: + case event.ReleaseFile: fileReleaseCount.Add(1) // delete file metadata by file object address - fileObject := e.Kparams.MustGetUint64(kparams.FileObject) + fileObject := e.Params.MustGetUint64(params.FileObject) delete(f.files, fileObject) - case ktypes.UnmapViewFile: + case event.UnmapViewFile: ok, proc := f.psnap.Find(e.PID) - addr := e.Kparams.TryGetAddress(kparams.FileViewBase) + addr := e.Params.TryGetAddress(params.FileViewBase) if ok { mmap := proc.FindMmap(addr) if mmap != nil { - e.AppendParam(kparams.FilePath, kparams.Path, mmap.File) + e.AppendParam(params.FilePath, params.Path, mmap.File) } } @@ -281,10 +280,10 @@ func (f *fsProcessor) processEvent(e *kevent.Kevent) (*kevent.Kevent, error) { return e, f.psnap.RemoveMmap(e.PID, addr) default: var fileObject uint64 - fileKey := e.Kparams.MustGetUint64(kparams.FileKey) + fileKey := e.Params.MustGetUint64(params.FileKey) if !e.IsMapViewFile() { - fileObject = e.Kparams.MustGetUint64(kparams.FileObject) + fileObject = e.Params.MustGetUint64(params.FileObject) } // attempt to get the file by file key. If there is no such file referenced @@ -294,12 +293,12 @@ func (f *fsProcessor) processEvent(e *kevent.Kevent) (*kevent.Kevent, error) { // try to resolve mapped file name if not found in internal state if fileinfo == nil && e.IsMapViewFile() { - sec := e.Kparams.MustGetUint32(kparams.FileViewSectionType) + sec := e.Params.MustGetUint32(params.FileViewSectionType) isMapped := sec != va.SectionPagefile && sec != va.SectionPhysical if isMapped { totalMapRundownFiles.Add(1) - addr := e.Kparams.MustGetUint64(kparams.FileViewBase) + (e.Kparams.MustGetUint64(kparams.FileOffset)) - e.AppendParam(kparams.FilePath, kparams.Path, f.getMappedFile(e.PID, addr)) + addr := e.Params.MustGetUint64(params.FileViewBase) + (e.Params.MustGetUint64(params.FileOffset)) + e.AppendParam(params.FilePath, params.Path, f.getMappedFile(e.PID, addr)) } } @@ -313,16 +312,16 @@ func (f *fsProcessor) processEvent(e *kevent.Kevent) (*kevent.Kevent, error) { } if e.IsEnumDirectory() { if fileinfo != nil { - e.AppendParam(kparams.FileDirectory, kparams.Path, fileinfo.Name) + e.AppendParam(params.FileDirectory, params.Path, fileinfo.Name) } return e, nil } if fileinfo != nil { if fileinfo.Type != fs.Unknown { - e.AppendEnum(kparams.FileType, uint32(fileinfo.Type), fs.FileTypes) + e.AppendEnum(params.FileType, uint32(fileinfo.Type), fs.FileTypes) } - e.AppendParam(kparams.FilePath, kparams.Path, fileinfo.Name) + e.AppendParam(params.FilePath, params.Path, fileinfo.Name) } if e.IsMapViewFile() { diff --git a/internal/etw/processors/fs_windows_test.go b/internal/etw/processors/fs_windows_test.go index d9e675834..6b754c7b4 100644 --- a/internal/etw/processors/fs_windows_test.go +++ b/internal/etw/processors/fs_windows_test.go @@ -20,12 +20,11 @@ package processors import ( "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/fs" "github.com/rabbitstack/fibratus/pkg/handle" htypes "github.com/rabbitstack/fibratus/pkg/handle/types" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/rabbitstack/fibratus/pkg/ps" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" "github.com/rabbitstack/fibratus/pkg/util/va" @@ -43,19 +42,19 @@ func TestFsProcessor(t *testing.T) { var tests = []struct { name string - e *kevent.Kevent + e *event.Event setupProcessor func(Processor) hsnap func() *handle.SnapshotterMock - assertions func(*kevent.Kevent, *testing.T, *handle.SnapshotterMock, Processor) + assertions func(*event.Event, *testing.T, *handle.SnapshotterMock, Processor) }{ { "process file rundown", - &kevent.Kevent{ - Type: ktypes.FileRundown, - Category: ktypes.File, - Kparams: kevent.Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(124567380264)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, + &event.Event{ + Type: event.FileRundown, + Category: event.File, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(124567380264)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, }, }, nil, @@ -63,7 +62,7 @@ func TestFsProcessor(t *testing.T) { hsnap := new(handle.SnapshotterMock) return hsnap }, - func(e *kevent.Kevent, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { + func(e *event.Event, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { fsProcessor := p.(*fsProcessor) assert.Contains(t, fsProcessor.files, uint64(124567380264)) file := fsProcessor.files[124567380264] @@ -73,15 +72,15 @@ func TestFsProcessor(t *testing.T) { }, { "process mapped file rundown", - &kevent.Kevent{ + &event.Event{ PID: 10233, - Type: ktypes.MapFileRundown, - Category: ktypes.File, - Kparams: kevent.Kparams{ - kparams.FileKey: {Name: kparams.FileKey, Type: kparams.Uint64, Value: uint64(124567380264)}, - kparams.FileViewSize: {Name: kparams.FileViewSize, Type: kparams.Uint64, Value: uint64(3098)}, - kparams.FileViewBase: {Name: kparams.FileViewBase, Type: kparams.Uint64, Value: uint64(0xffff23433)}, - kparams.FileViewSectionType: {Name: kparams.FileViewSectionType, Type: kparams.Enum, Value: uint32(va.SectionImage), Enum: kevent.ViewSectionTypes}, + Type: event.MapFileRundown, + Category: event.File, + Params: event.Params{ + params.FileKey: {Name: params.FileKey, Type: params.Uint64, Value: uint64(124567380264)}, + params.FileViewSize: {Name: params.FileViewSize, Type: params.Uint64, Value: uint64(3098)}, + params.FileViewBase: {Name: params.FileViewBase, Type: params.Uint64, Value: uint64(0xffff23433)}, + params.FileViewSectionType: {Name: params.FileViewSectionType, Type: params.Enum, Value: uint32(va.SectionImage), Enum: event.ViewSectionTypes}, }, }, func(p Processor) { @@ -92,10 +91,10 @@ func TestFsProcessor(t *testing.T) { hsnap := new(handle.SnapshotterMock) return hsnap }, - func(e *kevent.Kevent, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { + func(e *event.Event, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { fsProcessor := p.(*fsProcessor) - assert.Equal(t, "C:\\Windows\\System32\\kernel32.dll", e.GetParamAsString(kparams.FilePath)) + assert.Equal(t, "C:\\Windows\\System32\\kernel32.dll", e.GetParamAsString(params.FilePath)) psnap := fsProcessor.psnap.(*ps.SnapshotterMock) psnap.AssertNumberOfCalls(t, "AddMmap", 1) @@ -103,16 +102,16 @@ func TestFsProcessor(t *testing.T) { }, { "wait enqueue for create file events", - &kevent.Kevent{ - Type: ktypes.CreateFile, - Category: ktypes.File, - Kparams: kevent.Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(18446738026482168384)}, - kparams.ThreadID: {Name: kparams.ThreadID, Type: kparams.Uint32, Value: uint32(1484)}, - kparams.FileCreateOptions: {Name: kparams.FileCreateOptions, Type: kparams.Uint32, Value: uint32(1223456)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\kernel32.dll"}, - kparams.FileShareMask: {Name: kparams.FileShareMask, Type: kparams.Uint32, Value: uint32(5)}, - kparams.FileIrpPtr: {Name: kparams.FileIrpPtr, Type: kparams.Uint64, Value: uint64(1234543123112321)}, + &event.Event{ + Type: event.CreateFile, + Category: event.File, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(18446738026482168384)}, + params.ThreadID: {Name: params.ThreadID, Type: params.Uint32, Value: uint32(1484)}, + params.FileCreateOptions: {Name: params.FileCreateOptions, Type: params.Uint32, Value: uint32(1223456)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\kernel32.dll"}, + params.FileShareMask: {Name: params.FileShareMask, Type: params.Uint32, Value: uint32(5)}, + params.FileIrpPtr: {Name: params.FileIrpPtr, Type: params.Uint64, Value: uint64(1234543123112321)}, }, }, nil, @@ -120,7 +119,7 @@ func TestFsProcessor(t *testing.T) { hsnap := new(handle.SnapshotterMock) return hsnap }, - func(e *kevent.Kevent, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { + func(e *event.Event, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { fsProcessor := p.(*fsProcessor) assert.True(t, e.WaitEnqueue) assert.Contains(t, fsProcessor.irps, uint64(1234543123112321)) @@ -129,27 +128,27 @@ func TestFsProcessor(t *testing.T) { }, { "get IRP completion for create file event", - &kevent.Kevent{ - Type: ktypes.FileOpEnd, - Category: ktypes.File, - Kparams: kevent.Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(18446738026482168384)}, - kparams.FileExtraInfo: {Name: kparams.FileExtraInfo, Type: kparams.Uint64, Value: uint64(2)}, - kparams.FileIrpPtr: {Name: kparams.FileIrpPtr, Type: kparams.Uint64, Value: uint64(1334543123112321)}, - kparams.NTStatus: {Name: kparams.NTStatus, Type: kparams.Status, Value: uint32(0)}, + &event.Event{ + Type: event.FileOpEnd, + Category: event.File, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(18446738026482168384)}, + params.FileExtraInfo: {Name: params.FileExtraInfo, Type: params.Uint64, Value: uint64(2)}, + params.FileIrpPtr: {Name: params.FileIrpPtr, Type: params.Uint64, Value: uint64(1334543123112321)}, + params.NTStatus: {Name: params.NTStatus, Type: params.Status, Value: uint32(0)}, }, }, func(p Processor) { fsProcessor := p.(*fsProcessor) - fsProcessor.irps[1334543123112321] = &kevent.Kevent{ - Type: ktypes.CreateFile, - Category: ktypes.File, - Kparams: kevent.Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12446738026482168384)}, - kparams.FileCreateOptions: {Name: kparams.FileCreateOptions, Type: kparams.Uint32, Value: uint32(18874368)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: exe}, - kparams.FileShareMask: {Name: kparams.FileShareMask, Type: kparams.Uint32, Value: uint32(5)}, - kparams.FileIrpPtr: {Name: kparams.FileIrpPtr, Type: kparams.Uint64, Value: uint64(1334543123112321)}, + fsProcessor.irps[1334543123112321] = &event.Event{ + Type: event.CreateFile, + Category: event.File, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12446738026482168384)}, + params.FileCreateOptions: {Name: params.FileCreateOptions, Type: params.Uint32, Value: uint32(18874368)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: exe}, + params.FileShareMask: {Name: params.FileShareMask, Type: params.Uint32, Value: uint32(5)}, + params.FileIrpPtr: {Name: params.FileIrpPtr, Type: params.Uint64, Value: uint64(1334543123112321)}, }, } }, @@ -157,30 +156,30 @@ func TestFsProcessor(t *testing.T) { hsnap := new(handle.SnapshotterMock) return hsnap }, - func(e *kevent.Kevent, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { + func(e *event.Event, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { fsProcessor := p.(*fsProcessor) - assert.Equal(t, ktypes.CreateFile, e.Type) + assert.Equal(t, event.CreateFile, e.Type) assert.NotContains(t, fsProcessor.irps, uint64(1334543123112321)) assert.False(t, e.WaitEnqueue) assert.Contains(t, fsProcessor.files, uint64(12446738026482168384)) assert.Equal(t, exe, fsProcessor.files[12446738026482168384].Name) - assert.Equal(t, "Success", e.GetParamAsString(kparams.NTStatus)) - assert.Equal(t, "File", e.GetParamAsString(kparams.FileType)) - assert.Equal(t, "CREATE", e.GetParamAsString(kparams.FileOperation)) - assert.True(t, e.Kparams.MustGetBool(kparams.FileIsExecutable)) - assert.False(t, e.Kparams.MustGetBool(kparams.FileIsDotnet)) - assert.False(t, e.Kparams.MustGetBool(kparams.FileIsDLL)) - assert.False(t, e.Kparams.MustGetBool(kparams.FileIsDriver)) + assert.Equal(t, "Success", e.GetParamAsString(params.NTStatus)) + assert.Equal(t, "File", e.GetParamAsString(params.FileType)) + assert.Equal(t, "CREATE", e.GetParamAsString(params.FileOperation)) + assert.True(t, e.Params.MustGetBool(params.FileIsExecutable)) + assert.False(t, e.Params.MustGetBool(params.FileIsDotnet)) + assert.False(t, e.Params.MustGetBool(params.FileIsDLL)) + assert.False(t, e.Params.MustGetBool(params.FileIsDriver)) }, }, { "release file and remove file info", - &kevent.Kevent{ - Type: ktypes.ReleaseFile, - Category: ktypes.File, - Kparams: kevent.Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(18446738026482168384)}, - kparams.FileKey: {Name: kparams.FileKey, Type: kparams.Uint64, Value: uint64(14446538026482168384)}, + &event.Event{ + Type: event.ReleaseFile, + Category: event.File, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(18446738026482168384)}, + params.FileKey: {Name: params.FileKey, Type: params.Uint64, Value: uint64(14446538026482168384)}, }, }, func(p Processor) { @@ -191,24 +190,24 @@ func TestFsProcessor(t *testing.T) { hsnap := new(handle.SnapshotterMock) return hsnap }, - func(e *kevent.Kevent, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { + func(e *event.Event, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { fsProcessor := p.(*fsProcessor) assert.Empty(t, fsProcessor.files) }, }, { "parse created file characteristics", - &kevent.Kevent{ - Type: ktypes.CreateFile, - Category: ktypes.File, - Kparams: kevent.Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(18446738026482168384)}, - kparams.ThreadID: {Name: kparams.ThreadID, Type: kparams.Uint32, Value: uint32(1484)}, - kparams.FileCreateOptions: {Name: kparams.FileCreateOptions, Type: kparams.Uint32, Value: uint32(1223456)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: exe}, - kparams.FileShareMask: {Name: kparams.FileShareMask, Type: kparams.Uint32, Value: uint32(5)}, - kparams.FileIrpPtr: {Name: kparams.FileIrpPtr, Type: kparams.Uint64, Value: uint64(1234543123112321)}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.Uint64, Value: uint64(2)}, + &event.Event{ + Type: event.CreateFile, + Category: event.File, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(18446738026482168384)}, + params.ThreadID: {Name: params.ThreadID, Type: params.Uint32, Value: uint32(1484)}, + params.FileCreateOptions: {Name: params.FileCreateOptions, Type: params.Uint32, Value: uint32(1223456)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: exe}, + params.FileShareMask: {Name: params.FileShareMask, Type: params.Uint32, Value: uint32(5)}, + params.FileIrpPtr: {Name: params.FileIrpPtr, Type: params.Uint64, Value: uint64(1234543123112321)}, + params.FileOperation: {Name: params.FileOperation, Type: params.Uint64, Value: uint64(2)}, }, }, nil, @@ -216,7 +215,7 @@ func TestFsProcessor(t *testing.T) { hsnap := new(handle.SnapshotterMock) return hsnap }, - func(e *kevent.Kevent, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { + func(e *event.Event, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { fsProcessor := p.(*fsProcessor) assert.True(t, e.WaitEnqueue) assert.Contains(t, fsProcessor.irps, uint64(1234543123112321)) @@ -225,15 +224,15 @@ func TestFsProcessor(t *testing.T) { }, { "unmap view file", - &kevent.Kevent{ + &event.Event{ PID: 10233, - Type: ktypes.UnmapViewFile, - Category: ktypes.File, - Kparams: kevent.Kparams{ - kparams.FileKey: {Name: kparams.FileKey, Type: kparams.Uint64, Value: uint64(124567380264)}, - kparams.FileViewSize: {Name: kparams.FileViewSize, Type: kparams.Uint64, Value: uint64(3098)}, - kparams.FileViewBase: {Name: kparams.FileViewBase, Type: kparams.Uint64, Value: uint64(0xffff23433)}, - kparams.FileViewSectionType: {Name: kparams.FileViewSectionType, Type: kparams.Enum, Value: uint32(va.SectionImage), Enum: kevent.ViewSectionTypes}, + Type: event.UnmapViewFile, + Category: event.File, + Params: event.Params{ + params.FileKey: {Name: params.FileKey, Type: params.Uint64, Value: uint64(124567380264)}, + params.FileViewSize: {Name: params.FileViewSize, Type: params.Uint64, Value: uint64(3098)}, + params.FileViewBase: {Name: params.FileViewBase, Type: params.Uint64, Value: uint64(0xffff23433)}, + params.FileViewSectionType: {Name: params.FileViewSectionType, Type: params.Enum, Value: uint32(va.SectionImage), Enum: event.ViewSectionTypes}, }, }, nil, @@ -241,7 +240,7 @@ func TestFsProcessor(t *testing.T) { hsnap := new(handle.SnapshotterMock) return hsnap }, - func(e *kevent.Kevent, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { + func(e *event.Event, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { fsProcessor := p.(*fsProcessor) psnap := fsProcessor.psnap.(*ps.SnapshotterMock) @@ -250,13 +249,13 @@ func TestFsProcessor(t *testing.T) { }, { "process write file", - &kevent.Kevent{ - Type: ktypes.WriteFile, - Category: ktypes.File, - Kparams: kevent.Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(18446738026482168384)}, - kparams.FileKey: {Name: kparams.FileKey, Type: kparams.Uint64, Value: uint64(14446538026482168384)}, - kparams.FileIoSize: {Name: kparams.FileIoSize, Type: kparams.Uint32, Value: uint32(1024)}, + &event.Event{ + Type: event.WriteFile, + Category: event.File, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(18446738026482168384)}, + params.FileKey: {Name: params.FileKey, Type: params.Uint64, Value: uint64(14446538026482168384)}, + params.FileIoSize: {Name: params.FileIoSize, Type: params.Uint32, Value: uint32(1024)}, }, }, func(p Processor) { @@ -267,22 +266,22 @@ func TestFsProcessor(t *testing.T) { hsnap := new(handle.SnapshotterMock) return hsnap }, - func(e *kevent.Kevent, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { - assert.Equal(t, ktypes.WriteFile, e.Type) - assert.Contains(t, e.Kparams, kparams.FilePath, kparams.FileType) - assert.Equal(t, "C:\\Windows\\temp\\idxx.exe", e.GetParamAsString(kparams.FilePath)) - assert.Equal(t, "File", e.GetParamAsString(kparams.FileType)) + func(e *event.Event, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { + assert.Equal(t, event.WriteFile, e.Type) + assert.Contains(t, e.Params, params.FilePath, params.FileType) + assert.Equal(t, "C:\\Windows\\temp\\idxx.exe", e.GetParamAsString(params.FilePath)) + assert.Equal(t, "File", e.GetParamAsString(params.FileType)) }, }, { "process write file consult handle snapshotter", - &kevent.Kevent{ - Type: ktypes.WriteFile, - Category: ktypes.File, - Kparams: kevent.Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(18446738026482168384)}, - kparams.FileKey: {Name: kparams.FileKey, Type: kparams.Uint64, Value: uint64(14446538026482168384)}, - kparams.FileIoSize: {Name: kparams.FileIoSize, Type: kparams.Uint32, Value: uint32(1024)}, + &event.Event{ + Type: event.WriteFile, + Category: event.File, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(18446738026482168384)}, + params.FileKey: {Name: params.FileKey, Type: params.Uint64, Value: uint64(14446538026482168384)}, + params.FileIoSize: {Name: params.FileIoSize, Type: params.Uint32, Value: uint32(1024)}, }, }, nil, @@ -291,23 +290,23 @@ func TestFsProcessor(t *testing.T) { hsnap.On("FindByObject", uint64(18446738026482168384)).Return(htypes.Handle{Type: handle.File, Name: "C:\\Windows\\temp\\doc.docx"}, true) return hsnap }, - func(e *kevent.Kevent, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { - assert.Equal(t, ktypes.WriteFile, e.Type) + func(e *event.Event, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { + assert.Equal(t, event.WriteFile, e.Type) hsnap.AssertNumberOfCalls(t, "FindByObject", 1) - assert.Contains(t, e.Kparams, kparams.FilePath, kparams.FileType) - assert.Equal(t, "C:\\Windows\\temp\\doc.docx", e.GetParamAsString(kparams.FilePath)) - assert.Equal(t, "File", e.GetParamAsString(kparams.FileType)) + assert.Contains(t, e.Params, params.FilePath, params.FileType) + assert.Equal(t, "C:\\Windows\\temp\\doc.docx", e.GetParamAsString(params.FilePath)) + assert.Equal(t, "File", e.GetParamAsString(params.FileType)) }, }, { "process enum directory", - &kevent.Kevent{ - Type: ktypes.EnumDirectory, - Category: ktypes.File, - Kparams: kevent.Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(18446738026482168384)}, - kparams.FileKey: {Name: kparams.FileKey, Type: kparams.Uint64, Value: uint64(14446538026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "*"}, + &event.Event{ + Type: event.EnumDirectory, + Category: event.File, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(18446738026482168384)}, + params.FileKey: {Name: params.FileKey, Type: params.Uint64, Value: uint64(14446538026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "*"}, }, }, func(p Processor) { @@ -318,10 +317,10 @@ func TestFsProcessor(t *testing.T) { hsnap := new(handle.SnapshotterMock) return hsnap }, - func(e *kevent.Kevent, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { - assert.Equal(t, ktypes.EnumDirectory, e.Type) - assert.Contains(t, e.Kparams, kparams.FilePath, kparams.FileDirectory) - assert.Equal(t, "C:\\Windows\\temp", e.GetParamAsString(kparams.FileDirectory)) + func(e *event.Event, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { + assert.Equal(t, event.EnumDirectory, e.Type) + assert.Contains(t, e.Params, params.FilePath, params.FileDirectory) + assert.Equal(t, "C:\\Windows\\temp", e.GetParamAsString(params.FileDirectory)) }, }, } diff --git a/internal/etw/processors/handle_windows.go b/internal/etw/processors/handle_windows.go index e054a24a4..453996c9a 100644 --- a/internal/etw/processors/handle_windows.go +++ b/internal/etw/processors/handle_windows.go @@ -19,11 +19,10 @@ package processors import ( + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/fs" "github.com/rabbitstack/fibratus/pkg/handle" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/rabbitstack/fibratus/pkg/ps" "github.com/rabbitstack/fibratus/pkg/util/key" "strings" @@ -50,28 +49,28 @@ func newHandleProcessor( } } -func (h *handleProcessor) ProcessEvent(e *kevent.Kevent) (*kevent.Kevent, bool, error) { - if e.Category == ktypes.Handle { +func (h *handleProcessor) ProcessEvent(e *event.Event) (*event.Event, bool, error) { + if e.Category == event.Handle { evt, err := h.processEvent(e) return evt, false, err } return e, true, nil } -func (h *handleProcessor) processEvent(e *kevent.Kevent) (*kevent.Kevent, error) { - if e.Type == ktypes.DuplicateHandle { +func (h *handleProcessor) processEvent(e *event.Event) (*event.Event, error) { + if e.Type == event.DuplicateHandle { // enrich event with process parameters - pid := e.Kparams.MustGetPid() + pid := e.Params.MustGetPid() proc := h.psnap.FindAndPut(pid) if proc != nil { - e.AppendParam(kparams.Exe, kparams.Path, proc.Exe) - e.AppendParam(kparams.ProcessName, kparams.AnsiString, proc.Name) + e.AppendParam(params.Exe, params.Path, proc.Exe) + e.AppendParam(params.ProcessName, params.AnsiString, proc.Name) } return e, nil } - name := e.GetParamAsString(kparams.HandleObjectName) - typ := e.GetParamAsString(kparams.HandleObjectTypeID) + name := e.GetParamAsString(params.HandleObjectName) + typ := e.GetParamAsString(params.HandleObjectTypeID) if name != "" { switch typ { @@ -93,15 +92,15 @@ func (h *handleProcessor) processEvent(e *kevent.Kevent) (*kevent.Kevent, error) driverPath = driverName } h.devPathResolver.RemovePath(driverName) - e.Kparams.Append(kparams.ImagePath, kparams.Path, driverPath) + e.Params.Append(params.ImagePath, params.Path, driverPath) } // assign the formatted handle name - if err := e.Kparams.SetValue(kparams.HandleObjectName, name); err != nil { + if err := e.Params.SetValue(params.HandleObjectName, name); err != nil { return e, err } } - if e.Type == ktypes.CreateHandle { + if e.Type == event.CreateHandle { return e, h.hsnap.Write(e) } diff --git a/internal/etw/processors/handle_windows_test.go b/internal/etw/processors/handle_windows_test.go index e6c4258a8..c3b6b437d 100644 --- a/internal/etw/processors/handle_windows_test.go +++ b/internal/etw/processors/handle_windows_test.go @@ -19,11 +19,10 @@ package processors import ( + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/fs" "github.com/rabbitstack/fibratus/pkg/handle" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/rabbitstack/fibratus/pkg/ps" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -34,56 +33,56 @@ import ( func TestHandleProcessor(t *testing.T) { var tests = []struct { name string - e *kevent.Kevent + e *event.Event hsnap func() *handle.SnapshotterMock - assertions func(*kevent.Kevent, *testing.T, *handle.SnapshotterMock) + assertions func(*event.Event, *testing.T, *handle.SnapshotterMock) }{ { "process create handle", - &kevent.Kevent{ - Type: ktypes.CreateHandle, + &event.Event{ + Type: event.CreateHandle, Tid: 2484, PID: 859, - Category: ktypes.Handle, - Kparams: kevent.Kparams{ - kparams.HandleID: {Name: kparams.HandleID, Type: kparams.Uint32, Value: uint32(21)}, - kparams.HandleObjectTypeID: {Name: kparams.HandleObjectTypeID, Type: kparams.AnsiString, Value: "Key"}, - kparams.HandleObject: {Name: kparams.HandleObject, Type: kparams.Uint64, Value: uint64(18446692422059208560)}, - kparams.HandleObjectName: {Name: kparams.HandleObjectName, Type: kparams.UnicodeString, Value: ""}, + Category: event.Handle, + Params: event.Params{ + params.HandleID: {Name: params.HandleID, Type: params.Uint32, Value: uint32(21)}, + params.HandleObjectTypeID: {Name: params.HandleObjectTypeID, Type: params.AnsiString, Value: "Key"}, + params.HandleObject: {Name: params.HandleObject, Type: params.Uint64, Value: uint64(18446692422059208560)}, + params.HandleObjectName: {Name: params.HandleObjectName, Type: params.UnicodeString, Value: ""}, }, - Metadata: make(kevent.Metadata), + Metadata: make(event.Metadata), }, func() *handle.SnapshotterMock { hsnap := new(handle.SnapshotterMock) hsnap.On("Write", mock.Anything).Return(nil) return hsnap }, - func(e *kevent.Kevent, t *testing.T, hsnap *handle.SnapshotterMock) { + func(e *event.Event, t *testing.T, hsnap *handle.SnapshotterMock) { hsnap.AssertNumberOfCalls(t, "Write", 1) }, }, { "process close handle", - &kevent.Kevent{ - Type: ktypes.CloseHandle, + &event.Event{ + Type: event.CloseHandle, Tid: 2484, PID: 859, - Category: ktypes.Handle, - Kparams: kevent.Kparams{ - kparams.HandleID: {Name: kparams.HandleID, Type: kparams.Uint32, Value: uint32(21)}, - kparams.HandleObjectTypeID: {Name: kparams.HandleObjectTypeID, Type: kparams.AnsiString, Value: "Key"}, - kparams.HandleObject: {Name: kparams.HandleObject, Type: kparams.Uint64, Value: uint64(18446692422059208560)}, - kparams.HandleObjectName: {Name: kparams.HandleObjectName, Type: kparams.UnicodeString, Value: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`}, + Category: event.Handle, + Params: event.Params{ + params.HandleID: {Name: params.HandleID, Type: params.Uint32, Value: uint32(21)}, + params.HandleObjectTypeID: {Name: params.HandleObjectTypeID, Type: params.AnsiString, Value: "Key"}, + params.HandleObject: {Name: params.HandleObject, Type: params.Uint64, Value: uint64(18446692422059208560)}, + params.HandleObjectName: {Name: params.HandleObjectName, Type: params.UnicodeString, Value: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`}, }, - Metadata: make(kevent.Metadata), + Metadata: make(event.Metadata), }, func() *handle.SnapshotterMock { hsnap := new(handle.SnapshotterMock) hsnap.On("Remove", mock.Anything).Return(nil) return hsnap }, - func(e *kevent.Kevent, t *testing.T, hsnap *handle.SnapshotterMock) { - assert.Equal(t, `HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`, e.GetParamAsString(kparams.HandleObjectName)) + func(e *event.Event, t *testing.T, hsnap *handle.SnapshotterMock) { + assert.Equal(t, `HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`, e.GetParamAsString(params.HandleObjectName)) hsnap.AssertNumberOfCalls(t, "Remove", 1) }, }, diff --git a/internal/etw/processors/image_windows.go b/internal/etw/processors/image_windows.go index fb276e9fd..c0d262a87 100644 --- a/internal/etw/processors/image_windows.go +++ b/internal/etw/processors/image_windows.go @@ -20,8 +20,8 @@ package processors import ( "expvar" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/ps" "sync" "time" @@ -54,11 +54,11 @@ func newImageProcessor(psnap ps.Snapshotter) Processor { func (*imageProcessor) Name() ProcessorType { return Image } -func (m *imageProcessor) ProcessEvent(e *kevent.Kevent) (*kevent.Kevent, bool, error) { +func (m *imageProcessor) ProcessEvent(e *event.Event) (*event.Event, bool, error) { if e.IsLoadImage() { // is image characteristics data cached? - path := e.GetParamAsString(kparams.ImagePath) - key := path + e.GetParamAsString(kparams.ImageCheckSum) + path := e.GetParamAsString(params.ImagePath) + key := path + e.GetParamAsString(params.ImageCheckSum) m.mu.Lock() defer m.mu.Unlock() @@ -78,15 +78,15 @@ func (m *imageProcessor) ProcessEvent(e *kevent.Kevent) (*kevent.Kevent, bool, e } // append event parameters - e.AppendParam(kparams.FileIsDLL, kparams.Bool, c.isDLL) - e.AppendParam(kparams.FileIsDriver, kparams.Bool, c.isDriver) - e.AppendParam(kparams.FileIsExecutable, kparams.Bool, c.isExe) - e.AppendParam(kparams.FileIsDotnet, kparams.Bool, c.isDotnet) + e.AppendParam(params.FileIsDLL, params.Bool, c.isDLL) + e.AppendParam(params.FileIsDriver, params.Bool, c.isDriver) + e.AppendParam(params.FileIsExecutable, params.Bool, c.isExe) + e.AppendParam(params.FileIsDotnet, params.Bool, c.isDotnet) } if e.IsUnloadImage() { - pid := e.Kparams.MustGetPid() - addr := e.Kparams.TryGetAddress(kparams.ImageBase) + pid := e.Params.MustGetPid() + addr := e.Params.TryGetAddress(params.ImageBase) if pid == 0 { pid = e.PID } diff --git a/internal/etw/processors/image_windows_test.go b/internal/etw/processors/image_windows_test.go index 135fcda46..0e9cb8ba4 100644 --- a/internal/etw/processors/image_windows_test.go +++ b/internal/etw/processors/image_windows_test.go @@ -19,9 +19,8 @@ package processors import ( - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/ps" "github.com/rabbitstack/fibratus/pkg/util/signature" "github.com/rabbitstack/fibratus/pkg/util/va" @@ -36,21 +35,21 @@ import ( func TestImageProcessor(t *testing.T) { var tests = []struct { name string - e *kevent.Kevent + e *event.Event psnap func() *ps.SnapshotterMock - assertions func(*kevent.Kevent, *testing.T, *ps.SnapshotterMock) + assertions func(*event.Event, *testing.T, *ps.SnapshotterMock) }{ { "load new image", - &kevent.Kevent{ - Type: ktypes.LoadImage, - Kparams: kevent.Kparams{ - kparams.ImagePath: {Name: kparams.ImagePath, Type: kparams.UnicodeString, Value: filepath.Join(os.Getenv("windir"), "System32", "kernel32.dll")}, - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(1023)}, - kparams.ImageCheckSum: {Name: kparams.ImageCheckSum, Type: kparams.Uint32, Value: uint32(2323432)}, - kparams.ImageBase: {Name: kparams.ImageBase, Type: kparams.Address, Value: uint64(0x7ffb313833a3)}, - kparams.ImageSignatureType: {Name: kparams.ImageSignatureType, Type: kparams.Enum, Value: uint32(1), Enum: signature.Types}, - kparams.ImageSignatureLevel: {Name: kparams.ImageSignatureLevel, Type: kparams.Enum, Value: uint32(4), Enum: signature.Levels}, + &event.Event{ + Type: event.LoadImage, + Params: event.Params{ + params.ImagePath: {Name: params.ImagePath, Type: params.UnicodeString, Value: filepath.Join(os.Getenv("windir"), "System32", "kernel32.dll")}, + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1023)}, + params.ImageCheckSum: {Name: params.ImageCheckSum, Type: params.Uint32, Value: uint32(2323432)}, + params.ImageBase: {Name: params.ImageBase, Type: params.Address, Value: uint64(0x7ffb313833a3)}, + params.ImageSignatureType: {Name: params.ImageSignatureType, Type: params.Enum, Value: uint32(1), Enum: signature.Types}, + params.ImageSignatureLevel: {Name: params.ImageSignatureLevel, Type: params.Enum, Value: uint32(4), Enum: signature.Levels}, }, }, func() *ps.SnapshotterMock { @@ -58,24 +57,24 @@ func TestImageProcessor(t *testing.T) { psnap.On("AddModule", mock.Anything).Return(nil) return psnap }, - func(e *kevent.Kevent, t *testing.T, psnap *ps.SnapshotterMock) { + func(e *event.Event, t *testing.T, psnap *ps.SnapshotterMock) { psnap.AssertNumberOfCalls(t, "AddModule", 1) // should get the signature verified - assert.Equal(t, "EMBEDDED", e.GetParamAsString(kparams.ImageSignatureType)) - assert.Equal(t, "AUTHENTICODE", e.GetParamAsString(kparams.ImageSignatureLevel)) + assert.Equal(t, "EMBEDDED", e.GetParamAsString(params.ImageSignatureType)) + assert.Equal(t, "AUTHENTICODE", e.GetParamAsString(params.ImageSignatureLevel)) }, }, { "parse image characteristics", - &kevent.Kevent{ - Type: ktypes.LoadImage, - Kparams: kevent.Kparams{ - kparams.ImagePath: {Name: kparams.ImagePath, Type: kparams.UnicodeString, Value: "../_fixtures/mscorlib.dll"}, - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(1023)}, - kparams.ImageCheckSum: {Name: kparams.ImageCheckSum, Type: kparams.Uint32, Value: uint32(2323432)}, - kparams.ImageBase: {Name: kparams.ImageBase, Type: kparams.Address, Value: uint64(0x7ffb313833a3)}, - kparams.ImageSignatureType: {Name: kparams.ImageSignatureType, Type: kparams.Enum, Value: uint32(1), Enum: signature.Types}, - kparams.ImageSignatureLevel: {Name: kparams.ImageSignatureLevel, Type: kparams.Enum, Value: uint32(4), Enum: signature.Levels}, + &event.Event{ + Type: event.LoadImage, + Params: event.Params{ + params.ImagePath: {Name: params.ImagePath, Type: params.UnicodeString, Value: "../_fixtures/mscorlib.dll"}, + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1023)}, + params.ImageCheckSum: {Name: params.ImageCheckSum, Type: params.Uint32, Value: uint32(2323432)}, + params.ImageBase: {Name: params.ImageBase, Type: params.Address, Value: uint64(0x7ffb313833a3)}, + params.ImageSignatureType: {Name: params.ImageSignatureType, Type: params.Enum, Value: uint32(1), Enum: signature.Types}, + params.ImageSignatureLevel: {Name: params.ImageSignatureLevel, Type: params.Enum, Value: uint32(4), Enum: signature.Levels}, }, }, func() *ps.SnapshotterMock { @@ -83,26 +82,26 @@ func TestImageProcessor(t *testing.T) { psnap.On("AddModule", mock.Anything).Return(nil) return psnap }, - func(e *kevent.Kevent, t *testing.T, psnap *ps.SnapshotterMock) { + func(e *event.Event, t *testing.T, psnap *ps.SnapshotterMock) { psnap.AssertNumberOfCalls(t, "AddModule", 1) // should be enriched with image characteristics params - assert.True(t, e.Kparams.MustGetBool(kparams.FileIsDLL)) - assert.True(t, e.Kparams.MustGetBool(kparams.FileIsDotnet)) - assert.False(t, e.Kparams.MustGetBool(kparams.FileIsExecutable)) - assert.False(t, e.Kparams.MustGetBool(kparams.FileIsDriver)) + assert.True(t, e.Params.MustGetBool(params.FileIsDLL)) + assert.True(t, e.Params.MustGetBool(params.FileIsDotnet)) + assert.False(t, e.Params.MustGetBool(params.FileIsExecutable)) + assert.False(t, e.Params.MustGetBool(params.FileIsDriver)) }, }, { "unload image", - &kevent.Kevent{ - Type: ktypes.UnloadImage, - Kparams: kevent.Kparams{ - kparams.ImagePath: {Name: kparams.ImagePath, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\kernel32.dll"}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.AnsiString, Value: "csrss.exe"}, - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(676)}, - kparams.ImageBase: {Name: kparams.ImageBase, Type: kparams.Address, Value: uint64(0xfffb313833a3)}, - kparams.ImageSignatureType: {Name: kparams.ImageSignatureType, Type: kparams.Enum, Value: uint32(0), Enum: signature.Types}, - kparams.ImageSignatureLevel: {Name: kparams.ImageSignatureLevel, Type: kparams.Enum, Value: uint32(0), Enum: signature.Levels}, + &event.Event{ + Type: event.UnloadImage, + Params: event.Params{ + params.ImagePath: {Name: params.ImagePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\kernel32.dll"}, + params.ProcessName: {Name: params.ProcessName, Type: params.AnsiString, Value: "csrss.exe"}, + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(676)}, + params.ImageBase: {Name: params.ImageBase, Type: params.Address, Value: uint64(0xfffb313833a3)}, + params.ImageSignatureType: {Name: params.ImageSignatureType, Type: params.Enum, Value: uint32(0), Enum: signature.Types}, + params.ImageSignatureLevel: {Name: params.ImageSignatureLevel, Type: params.Enum, Value: uint32(0), Enum: signature.Levels}, }, }, func() *ps.SnapshotterMock { @@ -111,7 +110,7 @@ func TestImageProcessor(t *testing.T) { psnap.On("FindModule", mock.Anything).Return(false, nil) return psnap }, - func(e *kevent.Kevent, t *testing.T, psnap *ps.SnapshotterMock) { + func(e *event.Event, t *testing.T, psnap *ps.SnapshotterMock) { psnap.AssertNumberOfCalls(t, "RemoveModule", 1) }, }, diff --git a/internal/etw/processors/mem_windows.go b/internal/etw/processors/mem_windows.go index 459aa9207..2e51c4c49 100644 --- a/internal/etw/processors/mem_windows.go +++ b/internal/etw/processors/mem_windows.go @@ -19,15 +19,14 @@ package processors import ( - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" psnap "github.com/rabbitstack/fibratus/pkg/ps" "github.com/rabbitstack/fibratus/pkg/util/va" ) // MemPageTypes represents the type of the pages in the allocated region. -var MemPageTypes = kevent.ParamEnum{ +var MemPageTypes = event.ParamEnum{ va.MemImage: "IMAGE", va.MemMapped: "MAPPED", va.MemPrivate: "PRIVATE", @@ -48,29 +47,29 @@ func (m memProcessor) Close() { m.regionProber.Close() } -func (m memProcessor) ProcessEvent(e *kevent.Kevent) (*kevent.Kevent, bool, error) { - if e.Category == ktypes.Mem { - pid := e.Kparams.MustGetPid() +func (m memProcessor) ProcessEvent(e *event.Event) (*event.Event, bool, error) { + if e.Category == event.Mem { + pid := e.Params.MustGetPid() if e.IsVirtualAlloc() { // retrieve info about the range of pages and enrich the event // with allocation protection options and the type of pages in // the allocated region. If the region is mapped, we try to find // the backing file name - addr := e.Kparams.MustGetUint64(kparams.MemBaseAddress) + addr := e.Params.MustGetUint64(params.MemBaseAddress) region := m.regionProber.Query(pid, addr) if region != nil { if region.IsMapped() { - e.AppendParam(kparams.FilePath, kparams.DOSPath, region.GetMappedFile()) + e.AppendParam(params.FilePath, params.DOSPath, region.GetMappedFile()) } - e.AppendEnum(kparams.MemPageType, region.Type, MemPageTypes) - e.AppendFlags(kparams.MemProtect, region.Protect, kevent.MemProtectionFlags) - e.AppendParam(kparams.MemProtectMask, kparams.AnsiString, region.ProtectMask()) + e.AppendEnum(params.MemPageType, region.Type, MemPageTypes) + e.AppendFlags(params.MemProtect, region.Protect, event.MemProtectionFlags) + e.AppendParam(params.MemProtectMask, params.AnsiString, region.ProtectMask()) } } proc := m.psnap.FindAndPut(pid) if proc != nil { - e.AppendParam(kparams.Exe, kparams.Path, proc.Exe) - e.AppendParam(kparams.ProcessName, kparams.AnsiString, proc.Name) + e.AppendParam(params.Exe, params.Path, proc.Exe) + e.AppendParam(params.ProcessName, params.AnsiString, proc.Name) } return e, false, nil } diff --git a/internal/etw/processors/mem_windows_test.go b/internal/etw/processors/mem_windows_test.go index d3ef09d5c..dcf666b3e 100644 --- a/internal/etw/processors/mem_windows_test.go +++ b/internal/etw/processors/mem_windows_test.go @@ -19,9 +19,8 @@ package processors import ( - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/ps" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" "github.com/rabbitstack/fibratus/pkg/util/va" @@ -41,20 +40,20 @@ func TestMemProcessor(t *testing.T) { }() var tests = []struct { name string - e *kevent.Kevent + e *event.Event psnap func() *ps.SnapshotterMock - assertions func(*kevent.Kevent, *testing.T, *ps.SnapshotterMock) + assertions func(*event.Event, *testing.T, *ps.SnapshotterMock) }{ { "virtual alloc", - &kevent.Kevent{ - Type: ktypes.VirtualAlloc, - Category: ktypes.Mem, - Kparams: kevent.Kparams{ - kparams.MemRegionSize: {Name: kparams.MemRegionSize, Type: kparams.Uint64, Value: uint64(1024)}, - kparams.MemBaseAddress: {Name: kparams.MemBaseAddress, Type: kparams.Address, Value: uint64(base)}, - kparams.MemAllocType: {Name: kparams.MemAllocType, Type: kparams.Flags, Value: uint32(0x00001000 | 0x00002000), Flags: kevent.MemAllocationFlags}, - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(os.Getpid())}, + &event.Event{ + Type: event.VirtualAlloc, + Category: event.Mem, + Params: event.Params{ + params.MemRegionSize: {Name: params.MemRegionSize, Type: params.Uint64, Value: uint64(1024)}, + params.MemBaseAddress: {Name: params.MemBaseAddress, Type: params.Address, Value: uint64(base)}, + params.MemAllocType: {Name: params.MemAllocType, Type: params.Flags, Value: uint32(0x00001000 | 0x00002000), Flags: event.MemAllocationFlags}, + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(os.Getpid())}, }, }, func() *ps.SnapshotterMock { @@ -62,25 +61,25 @@ func TestMemProcessor(t *testing.T) { psnap.On("FindAndPut", mock.Anything).Return(&pstypes.PS{Name: "svchost.exe", Exe: "C:\\Windows\\System32\\svchost.exe"}) return psnap }, - func(e *kevent.Kevent, t *testing.T, psnap *ps.SnapshotterMock) { + func(e *event.Event, t *testing.T, psnap *ps.SnapshotterMock) { psnap.AssertNumberOfCalls(t, "FindAndPut", 1) - assert.Equal(t, "PRIVATE", e.GetParamAsString(kparams.MemPageType)) - assert.Equal(t, "EXECUTE_READWRITE", e.GetParamAsString(kparams.MemProtect)) - assert.Equal(t, "RWX", e.GetParamAsString(kparams.MemProtectMask)) - assert.Equal(t, "svchost.exe", e.GetParamAsString(kparams.ProcessName)) - assert.Equal(t, "C:\\Windows\\System32\\svchost.exe", e.GetParamAsString(kparams.Exe)) + assert.Equal(t, "PRIVATE", e.GetParamAsString(params.MemPageType)) + assert.Equal(t, "EXECUTE_READWRITE", e.GetParamAsString(params.MemProtect)) + assert.Equal(t, "RWX", e.GetParamAsString(params.MemProtectMask)) + assert.Equal(t, "svchost.exe", e.GetParamAsString(params.ProcessName)) + assert.Equal(t, "C:\\Windows\\System32\\svchost.exe", e.GetParamAsString(params.Exe)) }, }, { "virtual free", - &kevent.Kevent{ - Type: ktypes.VirtualFree, - Category: ktypes.Mem, - Kparams: kevent.Kparams{ - kparams.MemRegionSize: {Name: kparams.MemRegionSize, Type: kparams.Uint64, Value: uint64(1024)}, - kparams.MemBaseAddress: {Name: kparams.MemBaseAddress, Type: kparams.Address, Value: uint64(base)}, - kparams.MemAllocType: {Name: kparams.MemAllocType, Type: kparams.Flags, Value: uint32(0x00008000), Flags: kevent.MemAllocationFlags}, - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(os.Getpid())}, + &event.Event{ + Type: event.VirtualFree, + Category: event.Mem, + Params: event.Params{ + params.MemRegionSize: {Name: params.MemRegionSize, Type: params.Uint64, Value: uint64(1024)}, + params.MemBaseAddress: {Name: params.MemBaseAddress, Type: params.Address, Value: uint64(base)}, + params.MemAllocType: {Name: params.MemAllocType, Type: params.Flags, Value: uint32(0x00008000), Flags: event.MemAllocationFlags}, + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(os.Getpid())}, }, }, func() *ps.SnapshotterMock { @@ -88,10 +87,10 @@ func TestMemProcessor(t *testing.T) { psnap.On("FindAndPut", mock.Anything).Return(&pstypes.PS{Name: "svchost.exe", Exe: "C:\\Windows\\System32\\svchost.exe"}) return psnap }, - func(e *kevent.Kevent, t *testing.T, psnap *ps.SnapshotterMock) { + func(e *event.Event, t *testing.T, psnap *ps.SnapshotterMock) { psnap.AssertNumberOfCalls(t, "FindAndPut", 1) - assert.Equal(t, "svchost.exe", e.GetParamAsString(kparams.ProcessName)) - assert.Equal(t, "C:\\Windows\\System32\\svchost.exe", e.GetParamAsString(kparams.Exe)) + assert.Equal(t, "svchost.exe", e.GetParamAsString(params.ProcessName)) + assert.Equal(t, "C:\\Windows\\System32\\svchost.exe", e.GetParamAsString(params.Exe)) }, }, } diff --git a/internal/etw/processors/net_windows.go b/internal/etw/processors/net_windows.go index 2dc5a56dc..a09001f2b 100644 --- a/internal/etw/processors/net_windows.go +++ b/internal/etw/processors/net_windows.go @@ -19,9 +19,8 @@ package processors import ( - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/network" "github.com/rabbitstack/fibratus/pkg/util/ports" ) @@ -38,13 +37,13 @@ func (netProcessor) Name() ProcessorType { return Net } func (n netProcessor) Close() {} -func (n *netProcessor) ProcessEvent(e *kevent.Kevent) (*kevent.Kevent, bool, error) { - if e.Category == ktypes.Net { +func (n *netProcessor) ProcessEvent(e *event.Event) (*event.Event, bool, error) { + if e.Category == event.Net { if e.IsNetworkTCP() && !e.IsDNS() { - e.AppendEnum(kparams.NetL4Proto, uint32(network.TCP), network.ProtoNames) + e.AppendEnum(params.NetL4Proto, uint32(network.TCP), network.ProtoNames) } if e.IsNetworkUDP() && !e.IsDNS() { - e.AppendEnum(kparams.NetL4Proto, uint32(network.UDP), network.ProtoNames) + e.AppendEnum(params.NetL4Proto, uint32(network.UDP), network.ProtoNames) } if e.IsDNS() { @@ -60,25 +59,25 @@ func (n *netProcessor) ProcessEvent(e *kevent.Kevent) (*kevent.Kevent, bool, err // resolvePortName resolves the IANA service name for the particular port and transport protocol as // per https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml. -func (n netProcessor) resolvePortName(e *kevent.Kevent) *kevent.Kevent { - dport := e.Kparams.TryGetUint16(kparams.NetDport) - sport := e.Kparams.TryGetUint16(kparams.NetSport) +func (n netProcessor) resolvePortName(e *event.Event) *event.Event { + dport := e.Params.TryGetUint16(params.NetDport) + sport := e.Params.TryGetUint16(params.NetSport) if e.IsNetworkTCP() { if name, ok := ports.TCPPortNames[dport]; ok { - e.Kparams.Append(kparams.NetDportName, kparams.AnsiString, name) + e.Params.Append(params.NetDportName, params.AnsiString, name) } if name, ok := ports.TCPPortNames[sport]; ok { - e.Kparams.Append(kparams.NetSportName, kparams.AnsiString, name) + e.Params.Append(params.NetSportName, params.AnsiString, name) } return e } if name, ok := ports.UDPPortNames[dport]; ok { - e.Kparams.Append(kparams.NetDportName, kparams.AnsiString, name) + e.Params.Append(params.NetDportName, params.AnsiString, name) } if name, ok := ports.UDPPortNames[sport]; ok { - e.Kparams.Append(kparams.NetSportName, kparams.AnsiString, name) + e.Params.Append(params.NetSportName, params.AnsiString, name) } return e } diff --git a/internal/etw/processors/net_windows_test.go b/internal/etw/processors/net_windows_test.go index 23511cf66..6f6e36bc0 100644 --- a/internal/etw/processors/net_windows_test.go +++ b/internal/etw/processors/net_windows_test.go @@ -19,9 +19,8 @@ package processors import ( - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "net" @@ -31,47 +30,47 @@ import ( func TestNetworkProcessor(t *testing.T) { var tests = []struct { name string - e *kevent.Kevent - assertions func(*kevent.Kevent, *testing.T) + e *event.Event + assertions func(*event.Event, *testing.T) }{ { "send tcpv4", - &kevent.Kevent{ - Type: ktypes.SendTCPv4, - Category: ktypes.Net, - Kparams: kevent.Kparams{ - kparams.NetDport: {Name: kparams.NetDport, Type: kparams.Uint16, Value: uint16(443)}, - kparams.NetSport: {Name: kparams.NetSport, Type: kparams.Uint16, Value: uint16(43123)}, - kparams.NetSIP: {Name: kparams.NetSIP, Type: kparams.IPv4, Value: net.ParseIP("127.0.0.1")}, - kparams.NetDIP: {Name: kparams.NetDIP, Type: kparams.IPv4, Value: net.ParseIP("8.8.8.8")}, + &event.Event{ + Type: event.SendTCPv4, + Category: event.Net, + Params: event.Params{ + params.NetDport: {Name: params.NetDport, Type: params.Uint16, Value: uint16(443)}, + params.NetSport: {Name: params.NetSport, Type: params.Uint16, Value: uint16(43123)}, + params.NetSIP: {Name: params.NetSIP, Type: params.IPv4, Value: net.ParseIP("127.0.0.1")}, + params.NetDIP: {Name: params.NetDIP, Type: params.IPv4, Value: net.ParseIP("8.8.8.8")}, }, }, - func(e *kevent.Kevent, t *testing.T) { + func(e *event.Event, t *testing.T) { assert.Equal(t, "Send", e.Type.String()) - assert.Equal(t, "https", e.GetParamAsString(kparams.NetDportName)) - assert.Equal(t, "TCP", e.GetParamAsString(kparams.NetL4Proto)) - assert.Equal(t, "127.0.0.1", e.GetParamAsString(kparams.NetSIP)) - assert.Equal(t, "8.8.8.8", e.GetParamAsString(kparams.NetDIP)) - assert.Equal(t, "443", e.GetParamAsString(kparams.NetDport)) - assert.Equal(t, "43123", e.GetParamAsString(kparams.NetSport)) + assert.Equal(t, "https", e.GetParamAsString(params.NetDportName)) + assert.Equal(t, "TCP", e.GetParamAsString(params.NetL4Proto)) + assert.Equal(t, "127.0.0.1", e.GetParamAsString(params.NetSIP)) + assert.Equal(t, "8.8.8.8", e.GetParamAsString(params.NetDIP)) + assert.Equal(t, "443", e.GetParamAsString(params.NetDport)) + assert.Equal(t, "43123", e.GetParamAsString(params.NetSport)) }, }, { "recv udp6", - &kevent.Kevent{ - Type: ktypes.RecvUDPv6, - Category: ktypes.Net, - Kparams: kevent.Kparams{ - kparams.NetDport: {Name: kparams.NetDport, Type: kparams.Uint16, Value: uint16(53)}, - kparams.NetSport: {Name: kparams.NetSport, Type: kparams.Uint16, Value: uint16(43123)}, - kparams.NetSIP: {Name: kparams.NetSIP, Type: kparams.IPv4, Value: net.ParseIP("127.0.0.1")}, - kparams.NetDIP: {Name: kparams.NetDIP, Type: kparams.IPv4, Value: net.ParseIP("8.8.8.8")}, + &event.Event{ + Type: event.RecvUDPv6, + Category: event.Net, + Params: event.Params{ + params.NetDport: {Name: params.NetDport, Type: params.Uint16, Value: uint16(53)}, + params.NetSport: {Name: params.NetSport, Type: params.Uint16, Value: uint16(43123)}, + params.NetSIP: {Name: params.NetSIP, Type: params.IPv4, Value: net.ParseIP("127.0.0.1")}, + params.NetDIP: {Name: params.NetDIP, Type: params.IPv4, Value: net.ParseIP("8.8.8.8")}, }, }, - func(e *kevent.Kevent, t *testing.T) { + func(e *event.Event, t *testing.T) { assert.Equal(t, "Recv", e.Type.String()) - assert.Equal(t, "domain", e.GetParamAsString(kparams.NetDportName)) - assert.Equal(t, "UDP", e.GetParamAsString(kparams.NetL4Proto)) + assert.Equal(t, "domain", e.GetParamAsString(params.NetDportName)) + assert.Equal(t, "UDP", e.GetParamAsString(params.NetL4Proto)) }, }, } diff --git a/internal/etw/processors/processor.go b/internal/etw/processors/processor.go index 9f9cf1779..dcb929223 100644 --- a/internal/etw/processors/processor.go +++ b/internal/etw/processors/processor.go @@ -19,8 +19,8 @@ package processors import ( + "github.com/rabbitstack/fibratus/pkg/event" libntfs "github.com/rabbitstack/fibratus/pkg/fs/ntfs" - "github.com/rabbitstack/fibratus/pkg/kevent" "github.com/rabbitstack/fibratus/pkg/pe" "os" "time" @@ -52,7 +52,7 @@ const ( type Processor interface { // ProcessEvent receives an existing event possibly mutating its state. // If it returns true, the next processor in the chain is evaluated. - ProcessEvent(*kevent.Kevent) (*kevent.Kevent, bool, error) + ProcessEvent(*event.Event) (*event.Event, bool, error) // Name returns a human-readable name of this processor. Name() ProcessorType diff --git a/internal/etw/processors/ps_windows.go b/internal/etw/processors/ps_windows.go index b5e0fccc9..a52fe2e83 100644 --- a/internal/etw/processors/ps_windows.go +++ b/internal/etw/processors/ps_windows.go @@ -19,9 +19,8 @@ package processors import ( - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/ps" "github.com/rabbitstack/fibratus/pkg/util/cmdline" "github.com/rabbitstack/fibratus/pkg/util/multierror" @@ -40,43 +39,43 @@ func newPsProcessor(psnap ps.Snapshotter, regionProber *va.RegionProber) Process return &psProcessor{psnap: psnap, regionProber: regionProber} } -func (p psProcessor) ProcessEvent(e *kevent.Kevent) (*kevent.Kevent, bool, error) { +func (p psProcessor) ProcessEvent(e *event.Event) (*event.Event, bool, error) { switch e.Type { - case ktypes.CreateProcess, ktypes.TerminateProcess, ktypes.ProcessRundown: + case event.CreateProcess, event.TerminateProcess, event.ProcessRundown: evt, err := p.processEvent(e) if evt.IsTerminateProcess() { - p.regionProber.Remove(evt.Kparams.MustGetPid()) + p.regionProber.Remove(evt.Params.MustGetPid()) return evt, false, multierror.Wrap(err, p.psnap.Remove(evt)) } return evt, false, multierror.Wrap(err, p.psnap.Write(evt)) - case ktypes.CreateThread, ktypes.TerminateThread, ktypes.ThreadRundown: - pid, err := e.Kparams.GetPid() + case event.CreateThread, event.TerminateThread, event.ThreadRundown: + pid, err := e.Params.GetPid() if err != nil { return e, false, err } proc := p.psnap.FindAndPut(pid) if proc != nil { - e.AppendParam(kparams.Exe, kparams.UnicodeString, proc.Exe) + e.AppendParam(params.Exe, params.UnicodeString, proc.Exe) } if !e.IsTerminateThread() { return e, false, p.psnap.AddThread(e) } - tid, err := e.Kparams.GetTid() + tid, err := e.Params.GetTid() if err != nil { return e, false, err } return e, false, p.psnap.RemoveThread(pid, tid) - case ktypes.OpenProcess, ktypes.OpenThread: - pid, err := e.Kparams.GetPid() + case event.OpenProcess, event.OpenThread: + pid, err := e.Params.GetPid() if err != nil { return e, false, err } proc := p.psnap.FindAndPut(pid) if proc != nil { - e.AppendParam(kparams.Exe, kparams.Path, proc.Exe) - e.AppendParam(kparams.ProcessName, kparams.AnsiString, proc.Name) + e.AppendParam(params.Exe, params.Path, proc.Exe) + e.AppendParam(params.ProcessName, params.AnsiString, proc.Name) } return e, false, nil @@ -86,34 +85,34 @@ func (p psProcessor) ProcessEvent(e *kevent.Kevent) (*kevent.Kevent, bool, error } //nolint:unparam -func (p psProcessor) processEvent(e *kevent.Kevent) (*kevent.Kevent, error) { - cmndline := cmdline.New(e.GetParamAsString(kparams.Cmdline)). +func (p psProcessor) processEvent(e *event.Event) (*event.Event, error) { + cmndline := cmdline.New(e.GetParamAsString(params.Cmdline)). // get rid of leading/trailing quotes in the executable path CleanExe(). // expand all variations of the SystemRoot environment variable ExpandSystemRoot(). // some system processes are reported without the path in the command line, // but we can expand the path from the SystemRoot environment variable - CompleteSysProc(e.GetParamAsString(kparams.ProcessName)) + CompleteSysProc(e.GetParamAsString(params.ProcessName)) // append executable path parameter exe := cmndline.Exeline() if exe == "" { - exe = e.GetParamAsString(kparams.ProcessName) + exe = e.GetParamAsString(params.ProcessName) } - e.AppendParam(kparams.Exe, kparams.Path, exe) + e.AppendParam(params.Exe, params.Path, exe) if e.IsTerminateProcess() { return e, nil } // query process start time - pid := e.Kparams.MustGetPid() + pid := e.Params.MustGetPid() started, err := getStartTime(pid, e) if err != nil { started = e.Timestamp } - e.AppendParam(kparams.StartTime, kparams.Time, started) + e.AppendParam(params.StartTime, params.Time, started) return e, nil } @@ -121,7 +120,7 @@ func (p psProcessor) processEvent(e *kevent.Kevent) (*kevent.Kevent, error) { func (psProcessor) Name() ProcessorType { return Ps } func (p psProcessor) Close() {} -func getStartTime(pid uint32, e *kevent.Kevent) (time.Time, error) { +func getStartTime(pid uint32, e *event.Event) (time.Time, error) { proc, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid) if err != nil { return e.Timestamp, err diff --git a/internal/etw/processors/ps_windows_test.go b/internal/etw/processors/ps_windows_test.go index d00302e8a..9093e2ce7 100644 --- a/internal/etw/processors/ps_windows_test.go +++ b/internal/etw/processors/ps_windows_test.go @@ -19,9 +19,8 @@ package processors import ( - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/ps" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" "github.com/rabbitstack/fibratus/pkg/util/va" @@ -37,17 +36,17 @@ func TestPsProcessor(t *testing.T) { var tests = []struct { name string - e *kevent.Kevent + e *event.Event psnap func() *ps.SnapshotterMock - assertions func(*kevent.Kevent, *testing.T, *ps.SnapshotterMock) + assertions func(*event.Event, *testing.T, *ps.SnapshotterMock) }{ { "create exe parameter from cmdline", - &kevent.Kevent{ - Type: ktypes.CreateProcess, - Kparams: kevent.Kparams{ - kparams.Cmdline: {Name: kparams.Cmdline, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\svchost.exe -k RPCSS"}, - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(1023)}, + &event.Event{ + Type: event.CreateProcess, + Params: event.Params{ + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\svchost.exe -k RPCSS"}, + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1023)}, }, }, func() *ps.SnapshotterMock { @@ -55,20 +54,20 @@ func TestPsProcessor(t *testing.T) { psnap.On("Write", mock.Anything).Return(nil) return psnap }, - func(e *kevent.Kevent, t *testing.T, psnap *ps.SnapshotterMock) { - require.True(t, e.Kparams.Contains(kparams.Exe)) - require.Equal(t, "C:\\Windows\\system32\\svchost.exe", e.GetParamAsString(kparams.Exe)) + func(e *event.Event, t *testing.T, psnap *ps.SnapshotterMock) { + require.True(t, e.Params.Contains(params.Exe)) + require.Equal(t, "C:\\Windows\\system32\\svchost.exe", e.GetParamAsString(params.Exe)) psnap.AssertNumberOfCalls(t, "Write", 1) }, }, { "complete exe for system procs", - &kevent.Kevent{ - Type: ktypes.CreateProcess, - Kparams: kevent.Kparams{ - kparams.Cmdline: {Name: kparams.Cmdline, Type: kparams.UnicodeString, Value: "csrss.exe"}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.AnsiString, Value: "csrss.exe"}, - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(676)}, + &event.Event{ + Type: event.CreateProcess, + Params: event.Params{ + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: "csrss.exe"}, + params.ProcessName: {Name: params.ProcessName, Type: params.AnsiString, Value: "csrss.exe"}, + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(676)}, }, }, func() *ps.SnapshotterMock { @@ -76,20 +75,20 @@ func TestPsProcessor(t *testing.T) { psnap.On("Write", mock.Anything).Return(nil) return psnap }, - func(e *kevent.Kevent, t *testing.T, psnap *ps.SnapshotterMock) { - require.True(t, e.Kparams.Contains(kparams.Exe)) - require.Equal(t, "csrss.exe", e.GetParamAsString(kparams.Cmdline)) - require.Equal(t, "C:\\Windows\\System32\\csrss.exe", e.GetParamAsString(kparams.Exe)) + func(e *event.Event, t *testing.T, psnap *ps.SnapshotterMock) { + require.True(t, e.Params.Contains(params.Exe)) + require.Equal(t, "csrss.exe", e.GetParamAsString(params.Cmdline)) + require.Equal(t, "C:\\Windows\\System32\\csrss.exe", e.GetParamAsString(params.Exe)) psnap.AssertNumberOfCalls(t, "Write", 1) }, }, { "clean quoted executable path", - &kevent.Kevent{ - Type: ktypes.CreateProcess, - Kparams: kevent.Kparams{ - kparams.Cmdline: {Name: kparams.Cmdline, Type: kparams.UnicodeString, Value: "\"C:\\Windows\\System32\\smss.exe\""}, - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(760)}, + &event.Event{ + Type: event.CreateProcess, + Params: event.Params{ + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: "\"C:\\Windows\\System32\\smss.exe\""}, + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(760)}, }, }, func() *ps.SnapshotterMock { @@ -97,20 +96,20 @@ func TestPsProcessor(t *testing.T) { psnap.On("Write", mock.Anything).Return(nil) return psnap }, - func(e *kevent.Kevent, t *testing.T, psnap *ps.SnapshotterMock) { - require.True(t, e.Kparams.Contains(kparams.Exe)) - require.Equal(t, "\"C:\\Windows\\System32\\smss.exe\"", e.GetParamAsString(kparams.Cmdline)) - require.Equal(t, "C:\\Windows\\System32\\smss.exe", e.GetParamAsString(kparams.Exe)) + func(e *event.Event, t *testing.T, psnap *ps.SnapshotterMock) { + require.True(t, e.Params.Contains(params.Exe)) + require.Equal(t, "\"C:\\Windows\\System32\\smss.exe\"", e.GetParamAsString(params.Cmdline)) + require.Equal(t, "C:\\Windows\\System32\\smss.exe", e.GetParamAsString(params.Exe)) psnap.AssertNumberOfCalls(t, "Write", 1) }, }, { "expand SystemRoot in executable path", - &kevent.Kevent{ - Type: ktypes.CreateProcess, - Kparams: kevent.Kparams{ - kparams.Cmdline: {Name: kparams.Cmdline, Type: kparams.UnicodeString, Value: `\SystemRoot\System32\smss.exe`}, - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(760)}, + &event.Event{ + Type: event.CreateProcess, + Params: event.Params{ + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: `\SystemRoot\System32\smss.exe`}, + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(760)}, }, }, func() *ps.SnapshotterMock { @@ -118,21 +117,21 @@ func TestPsProcessor(t *testing.T) { psnap.On("Write", mock.Anything).Return(nil) return psnap }, - func(e *kevent.Kevent, t *testing.T, psnap *ps.SnapshotterMock) { - require.True(t, e.Kparams.Contains(kparams.Exe)) - require.Equal(t, `\SystemRoot\System32\smss.exe`, e.GetParamAsString(kparams.Cmdline)) - require.Equal(t, "C:\\Windows\\System32\\smss.exe", e.GetParamAsString(kparams.Exe)) + func(e *event.Event, t *testing.T, psnap *ps.SnapshotterMock) { + require.True(t, e.Params.Contains(params.Exe)) + require.Equal(t, `\SystemRoot\System32\smss.exe`, e.GetParamAsString(params.Cmdline)) + require.Equal(t, "C:\\Windows\\System32\\smss.exe", e.GetParamAsString(params.Exe)) psnap.AssertNumberOfCalls(t, "Write", 1) }, }, { "add process start time parameter", - &kevent.Kevent{ - Type: ktypes.CreateProcess, + &event.Event{ + Type: event.CreateProcess, Timestamp: time.Now(), - Kparams: kevent.Kparams{ - kparams.Cmdline: {Name: kparams.Cmdline, Type: kparams.UnicodeString, Value: `C:\Program Files\Fibratus\fibratus.exe`}, - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(os.Getpid())}, + Params: event.Params{ + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: `C:\Program Files\Fibratus\fibratus.exe`}, + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(os.Getpid())}, }, }, func() *ps.SnapshotterMock { @@ -140,18 +139,18 @@ func TestPsProcessor(t *testing.T) { psnap.On("Write", mock.Anything).Return(nil) return psnap }, - func(e *kevent.Kevent, t *testing.T, psnap *ps.SnapshotterMock) { - require.True(t, e.Kparams.Contains(kparams.StartTime)) - require.NotEqual(t, e.Timestamp, e.Kparams.MustGetTime(kparams.StartTime)) + func(e *event.Event, t *testing.T, psnap *ps.SnapshotterMock) { + require.True(t, e.Params.Contains(params.StartTime)) + require.NotEqual(t, e.Timestamp, e.Params.MustGetTime(params.StartTime)) }, }, { "terminate process", - &kevent.Kevent{ - Type: ktypes.TerminateProcess, - Kparams: kevent.Kparams{ - kparams.Cmdline: {Name: kparams.Cmdline, Type: kparams.UnicodeString, Value: `\SystemRoot\System32\smss.exe`}, - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(760)}, + &event.Event{ + Type: event.TerminateProcess, + Params: event.Params{ + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: `\SystemRoot\System32\smss.exe`}, + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(760)}, }, }, func() *ps.SnapshotterMock { @@ -159,21 +158,21 @@ func TestPsProcessor(t *testing.T) { psnap.On("Remove", mock.Anything).Return(nil) return psnap }, - func(e *kevent.Kevent, t *testing.T, psnap *ps.SnapshotterMock) { - require.True(t, e.Kparams.Contains(kparams.Exe)) - require.Equal(t, `\SystemRoot\System32\smss.exe`, e.GetParamAsString(kparams.Cmdline)) - require.Equal(t, "C:\\Windows\\System32\\smss.exe", e.GetParamAsString(kparams.Exe)) + func(e *event.Event, t *testing.T, psnap *ps.SnapshotterMock) { + require.True(t, e.Params.Contains(params.Exe)) + require.Equal(t, `\SystemRoot\System32\smss.exe`, e.GetParamAsString(params.Cmdline)) + require.Equal(t, "C:\\Windows\\System32\\smss.exe", e.GetParamAsString(params.Exe)) psnap.AssertNumberOfCalls(t, "Remove", 1) psnap.AssertNotCalled(t, "Write") }, }, { "create thread", - &kevent.Kevent{ - Type: ktypes.CreateThread, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(760)}, - kparams.ThreadID: {Name: kparams.ThreadID, Type: kparams.TID, Value: uint32(10234)}, + &event.Event{ + Type: event.CreateThread, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(760)}, + params.ThreadID: {Name: params.ThreadID, Type: params.TID, Value: uint32(10234)}, }, }, func() *ps.SnapshotterMock { @@ -182,19 +181,19 @@ func TestPsProcessor(t *testing.T) { psnap.On("AddThread", mock.Anything).Return(nil) return psnap }, - func(e *kevent.Kevent, t *testing.T, psnap *ps.SnapshotterMock) { - require.True(t, e.Kparams.Contains(kparams.Exe)) - require.Equal(t, "C:\\Windows\\System32\\smss.exe", e.GetParamAsString(kparams.Exe)) + func(e *event.Event, t *testing.T, psnap *ps.SnapshotterMock) { + require.True(t, e.Params.Contains(params.Exe)) + require.Equal(t, "C:\\Windows\\System32\\smss.exe", e.GetParamAsString(params.Exe)) psnap.AssertNumberOfCalls(t, "AddThread", 1) }, }, { "terminate thread", - &kevent.Kevent{ - Type: ktypes.TerminateThread, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(760)}, - kparams.ThreadID: {Name: kparams.ThreadID, Type: kparams.TID, Value: uint32(10234)}, + &event.Event{ + Type: event.TerminateThread, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(760)}, + params.ThreadID: {Name: params.ThreadID, Type: params.TID, Value: uint32(10234)}, }, }, func() *ps.SnapshotterMock { @@ -203,19 +202,19 @@ func TestPsProcessor(t *testing.T) { psnap.On("RemoveThread", uint32(760), uint32(10234)).Return(nil) return psnap }, - func(e *kevent.Kevent, t *testing.T, psnap *ps.SnapshotterMock) { - require.True(t, e.Kparams.Contains(kparams.Exe)) - require.Equal(t, "C:\\Windows\\System32\\smss.exe", e.GetParamAsString(kparams.Exe)) + func(e *event.Event, t *testing.T, psnap *ps.SnapshotterMock) { + require.True(t, e.Params.Contains(params.Exe)) + require.Equal(t, "C:\\Windows\\System32\\smss.exe", e.GetParamAsString(params.Exe)) psnap.AssertNumberOfCalls(t, "RemoveThread", 1) psnap.AssertNotCalled(t, "AddThread") }, }, { "open process", - &kevent.Kevent{ - Type: ktypes.OpenProcess, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(760)}, + &event.Event{ + Type: event.OpenProcess, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(760)}, }, }, func() *ps.SnapshotterMock { @@ -223,11 +222,11 @@ func TestPsProcessor(t *testing.T) { psnap.On("FindAndPut", uint32(760)).Return(&pstypes.PS{Name: "smss.exe", Exe: "C:\\Windows\\System32\\smss.exe"}) return psnap }, - func(e *kevent.Kevent, t *testing.T, psnap *ps.SnapshotterMock) { - require.True(t, e.Kparams.Contains(kparams.Exe)) - require.True(t, e.Kparams.Contains(kparams.ProcessName)) - require.Equal(t, "C:\\Windows\\System32\\smss.exe", e.GetParamAsString(kparams.Exe)) - require.Equal(t, "smss.exe", e.GetParamAsString(kparams.ProcessName)) + func(e *event.Event, t *testing.T, psnap *ps.SnapshotterMock) { + require.True(t, e.Params.Contains(params.Exe)) + require.True(t, e.Params.Contains(params.ProcessName)) + require.Equal(t, "C:\\Windows\\System32\\smss.exe", e.GetParamAsString(params.Exe)) + require.Equal(t, "smss.exe", e.GetParamAsString(params.ProcessName)) }, }, } diff --git a/internal/etw/processors/registry_windows.go b/internal/etw/processors/registry_windows.go index 50441be54..02f132aa7 100644 --- a/internal/etw/processors/registry_windows.go +++ b/internal/etw/processors/registry_windows.go @@ -30,10 +30,9 @@ import ( "sync/atomic" "time" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/handle" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" ) var ( @@ -75,26 +74,26 @@ func newRegistryProcessor(hsnap handle.Snapshotter) Processor { } } -func (r *registryProcessor) ProcessEvent(e *kevent.Kevent) (*kevent.Kevent, bool, error) { - if e.Category == ktypes.Registry { +func (r *registryProcessor) ProcessEvent(e *event.Event) (*event.Event, bool, error) { + if e.Category == event.Registry { evt, err := r.processEvent(e) return evt, false, err } return e, true, nil } -func (r *registryProcessor) processEvent(e *kevent.Kevent) (*kevent.Kevent, error) { +func (r *registryProcessor) processEvent(e *event.Event) (*event.Event, error) { switch e.Type { - case ktypes.RegKCBRundown, ktypes.RegCreateKCB: - khandle := e.Kparams.MustGetUint64(kparams.RegKeyHandle) - r.keys[khandle] = e.Kparams.MustGetString(kparams.RegPath) + case event.RegKCBRundown, event.RegCreateKCB: + khandle := e.Params.MustGetUint64(params.RegKeyHandle) + r.keys[khandle] = e.Params.MustGetString(params.RegPath) kcbCount.Add(1) - case ktypes.RegDeleteKCB: - khandle := e.Kparams.MustGetUint64(kparams.RegKeyHandle) + case event.RegDeleteKCB: + khandle := e.Params.MustGetUint64(params.RegKeyHandle) delete(r.keys, khandle) kcbCount.Add(-1) default: - khandle := e.Kparams.MustGetUint64(kparams.RegKeyHandle) + khandle := e.Params.MustGetUint64(params.RegKeyHandle) // we have to obey a straightforward algorithm to connect relative // key names to their root keys. If key handle is equal to zero we // have a full key name and don't have to go further resolving the @@ -104,7 +103,7 @@ func (r *registryProcessor) processEvent(e *kevent.Kevent) (*kevent.Kevent, erro // last resort is to scan process' handles and check if any of the // key handles contain the partial key name. In this case we assume // the correct key is encountered. - keyName := e.Kparams.MustGetString(kparams.RegPath) + keyName := e.Params.MustGetString(params.RegPath) if khandle != 0 { if baseKey, ok := r.keys[khandle]; ok { keyName = baseKey + "\\" + keyName @@ -112,7 +111,7 @@ func (r *registryProcessor) processEvent(e *kevent.Kevent) (*kevent.Kevent, erro kcbMissCount.Add(1) keyName = r.findMatchingKey(e.PID, keyName) } - if err := e.Kparams.SetValue(kparams.RegPath, keyName); err != nil { + if err := e.Params.SetValue(params.RegPath, keyName); err != nil { return e, err } } @@ -137,18 +136,18 @@ func (r *registryProcessor) processEvent(e *kevent.Kevent) (*kevent.Kevent, erro } return e, ErrReadValue(rootkey.String(), keyName, err) } - e.AppendEnum(kparams.RegValueType, typ, key.RegistryValueTypes) + e.AppendEnum(params.RegValueType, typ, key.RegistryValueTypes) switch typ { case registry.SZ, registry.EXPAND_SZ: - e.AppendParam(kparams.RegValue, kparams.UnicodeString, val) + e.AppendParam(params.RegValue, params.UnicodeString, val) case registry.MULTI_SZ: - e.AppendParam(kparams.RegValue, kparams.Slice, val) + e.AppendParam(params.RegValue, params.Slice, val) case registry.BINARY: - e.AppendParam(kparams.RegValue, kparams.Binary, val) + e.AppendParam(params.RegValue, params.Binary, val) case registry.QWORD: - e.AppendParam(kparams.RegValue, kparams.Uint64, val) + e.AppendParam(params.RegValue, params.Uint64, val) case registry.DWORD: - e.AppendParam(kparams.RegValue, kparams.Uint32, uint32(val.(uint64))) + e.AppendParam(params.RegValue, params.Uint32, uint32(val.(uint64))) } } } diff --git a/internal/etw/processors/registry_windows_test.go b/internal/etw/processors/registry_windows_test.go index f7347a938..16c0d88db 100644 --- a/internal/etw/processors/registry_windows_test.go +++ b/internal/etw/processors/registry_windows_test.go @@ -19,11 +19,10 @@ package processors import ( + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/handle" htypes "github.com/rabbitstack/fibratus/pkg/handle/types" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "testing" @@ -32,19 +31,19 @@ import ( func TestRegistryProcessor(t *testing.T) { var tests = []struct { name string - e *kevent.Kevent + e *event.Event setupProcessor func(Processor) hsnap func() *handle.SnapshotterMock - assertions func(*kevent.Kevent, *testing.T, *handle.SnapshotterMock, Processor) + assertions func(*event.Event, *testing.T, *handle.SnapshotterMock, Processor) }{ { "process KCB rundown", - &kevent.Kevent{ - Type: ktypes.RegKCBRundown, - Category: ktypes.Registry, - Kparams: kevent.Kparams{ - kparams.RegPath: {Name: kparams.RegPath, Type: kparams.UnicodeString, Value: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\bthserv\Parameters`}, - kparams.RegKeyHandle: {Name: kparams.RegKeyHandle, Type: kparams.Uint64, Value: uint64(18446666033549154696)}, + &event.Event{ + Type: event.RegKCBRundown, + Category: event.Registry, + Params: event.Params{ + params.RegPath: {Name: params.RegPath, Type: params.UnicodeString, Value: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\bthserv\Parameters`}, + params.RegKeyHandle: {Name: params.RegKeyHandle, Type: params.Uint64, Value: uint64(18446666033549154696)}, }, }, nil, @@ -52,7 +51,7 @@ func TestRegistryProcessor(t *testing.T) { hsnap := new(handle.SnapshotterMock) return hsnap }, - func(e *kevent.Kevent, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { + func(e *event.Event, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { registryProcessor := p.(*registryProcessor) assert.Contains(t, registryProcessor.keys, uint64(18446666033549154696)) assert.Equal(t, `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\bthserv\Parameters`, registryProcessor.keys[18446666033549154696]) @@ -60,12 +59,12 @@ func TestRegistryProcessor(t *testing.T) { }, { "process delete KCB", - &kevent.Kevent{ - Type: ktypes.RegDeleteKCB, - Category: ktypes.Registry, - Kparams: kevent.Kparams{ - kparams.RegPath: {Name: kparams.RegPath, Type: kparams.UnicodeString, Value: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\bthserv\Parameters`}, - kparams.RegKeyHandle: {Name: kparams.RegKeyHandle, Type: kparams.Uint64, Value: uint64(18446666033549154696)}, + &event.Event{ + Type: event.RegDeleteKCB, + Category: event.Registry, + Params: event.Params{ + params.RegPath: {Name: params.RegPath, Type: params.UnicodeString, Value: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\bthserv\Parameters`}, + params.RegKeyHandle: {Name: params.RegKeyHandle, Type: params.Uint64, Value: uint64(18446666033549154696)}, }, }, func(p Processor) { @@ -75,19 +74,19 @@ func TestRegistryProcessor(t *testing.T) { hsnap := new(handle.SnapshotterMock) return hsnap }, - func(e *kevent.Kevent, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { + func(e *event.Event, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { registryProcessor := p.(*registryProcessor) assert.Empty(t, registryProcessor.keys) }, }, { "full key name", - &kevent.Kevent{ - Type: ktypes.RegOpenKey, - Category: ktypes.Registry, - Kparams: kevent.Kparams{ - kparams.RegPath: {Name: kparams.RegPath, Type: kparams.Key, Value: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\bthserv\Parameters`}, - kparams.RegKeyHandle: {Name: kparams.RegKeyHandle, Type: kparams.Uint64, Value: uint64(0)}, + &event.Event{ + Type: event.RegOpenKey, + Category: event.Registry, + Params: event.Params{ + params.RegPath: {Name: params.RegPath, Type: params.Key, Value: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\bthserv\Parameters`}, + params.RegKeyHandle: {Name: params.RegKeyHandle, Type: params.Uint64, Value: uint64(0)}, }, }, nil, @@ -95,18 +94,18 @@ func TestRegistryProcessor(t *testing.T) { hsnap := new(handle.SnapshotterMock) return hsnap }, - func(e *kevent.Kevent, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { - assert.Equal(t, `HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\bthserv\Parameters`, e.GetParamAsString(kparams.RegPath)) + func(e *event.Event, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { + assert.Equal(t, `HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\bthserv\Parameters`, e.GetParamAsString(params.RegPath)) }, }, { "incomplete key name", - &kevent.Kevent{ - Type: ktypes.RegOpenKey, - Category: ktypes.Registry, - Kparams: kevent.Kparams{ - kparams.RegPath: {Name: kparams.RegPath, Type: kparams.Key, Value: `Pid`}, - kparams.RegKeyHandle: {Name: kparams.RegKeyHandle, Type: kparams.Uint64, Value: uint64(18446666033549154696)}, + &event.Event{ + Type: event.RegOpenKey, + Category: event.Registry, + Params: event.Params{ + params.RegPath: {Name: params.RegPath, Type: params.Key, Value: `Pid`}, + params.RegKeyHandle: {Name: params.RegKeyHandle, Type: params.Uint64, Value: uint64(18446666033549154696)}, }, }, func(p Processor) { @@ -116,19 +115,19 @@ func TestRegistryProcessor(t *testing.T) { hsnap := new(handle.SnapshotterMock) return hsnap }, - func(e *kevent.Kevent, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { - assert.Equal(t, `HKEY_LOCAL_MACHINE\SYSTEM\Setup\Pid`, e.GetParamAsString(kparams.RegPath)) + func(e *event.Event, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { + assert.Equal(t, `HKEY_LOCAL_MACHINE\SYSTEM\Setup\Pid`, e.GetParamAsString(params.RegPath)) }, }, { "incomplete key name consult handle snapshotter", - &kevent.Kevent{ - Type: ktypes.RegOpenKey, - Category: ktypes.Registry, + &event.Event{ + Type: event.RegOpenKey, + Category: event.Registry, PID: 23234, - Kparams: kevent.Kparams{ - kparams.RegPath: {Name: kparams.RegPath, Type: kparams.Key, Value: `Pid`}, - kparams.RegKeyHandle: {Name: kparams.RegKeyHandle, Type: kparams.Uint64, Value: uint64(18446666033549154696)}, + Params: event.Params{ + params.RegPath: {Name: params.RegPath, Type: params.Key, Value: `Pid`}, + params.RegKeyHandle: {Name: params.RegKeyHandle, Type: params.Uint64, Value: uint64(18446666033549154696)}, }, }, nil, @@ -138,20 +137,20 @@ func TestRegistryProcessor(t *testing.T) { hsnap.On("FindHandles", uint32(23234)).Return(handles, nil) return hsnap }, - func(e *kevent.Kevent, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { + func(e *event.Event, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { hsnap.AssertNumberOfCalls(t, "FindHandles", 1) - assert.Equal(t, `HKEY_LOCAL_MACHINE\SYSTEM\Setup\Pid`, e.GetParamAsString(kparams.RegPath)) + assert.Equal(t, `HKEY_LOCAL_MACHINE\SYSTEM\Setup\Pid`, e.GetParamAsString(params.RegPath)) }, }, { "process registry set value", - &kevent.Kevent{ - Type: ktypes.RegSetValue, - Category: ktypes.Registry, + &event.Event{ + Type: event.RegSetValue, + Category: event.Registry, PID: 23234, - Kparams: kevent.Kparams{ - kparams.RegPath: {Name: kparams.RegPath, Type: kparams.Key, Value: `\REGISTRY\MACHINE\SYSTEM\CurrentControlSet\Control\Windows\Directory`}, - kparams.RegKeyHandle: {Name: kparams.RegKeyHandle, Type: kparams.Uint64, Value: uint64(0)}, + Params: event.Params{ + params.RegPath: {Name: params.RegPath, Type: params.Key, Value: `\REGISTRY\MACHINE\SYSTEM\CurrentControlSet\Control\Windows\Directory`}, + params.RegKeyHandle: {Name: params.RegKeyHandle, Type: params.Uint64, Value: uint64(0)}, }, }, nil, @@ -159,10 +158,10 @@ func TestRegistryProcessor(t *testing.T) { hsnap := new(handle.SnapshotterMock) return hsnap }, - func(e *kevent.Kevent, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { - assert.Equal(t, `HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Windows\Directory`, e.GetParamAsString(kparams.RegPath)) - assert.Equal(t, `REG_EXPAND_SZ`, e.GetParamAsString(kparams.RegValueType)) - assert.Equal(t, `%SystemRoot%`, e.GetParamAsString(kparams.RegValue)) + func(e *event.Event, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { + assert.Equal(t, `HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Windows\Directory`, e.GetParamAsString(params.RegPath)) + assert.Equal(t, `REG_EXPAND_SZ`, e.GetParamAsString(params.RegValueType)) + assert.Equal(t, `%SystemRoot%`, e.GetParamAsString(params.RegValue)) }, }, } diff --git a/internal/etw/source.go b/internal/etw/source.go index 71eb328fa..3404a337a 100644 --- a/internal/etw/source.go +++ b/internal/etw/source.go @@ -23,13 +23,12 @@ import ( "expvar" "fmt" "github.com/rabbitstack/fibratus/pkg/config" - kerrors "github.com/rabbitstack/fibratus/pkg/errors" + errs "github.com/rabbitstack/fibratus/pkg/errors" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/filter" "github.com/rabbitstack/fibratus/pkg/handle" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" - "github.com/rabbitstack/fibratus/pkg/ksource" "github.com/rabbitstack/fibratus/pkg/ps" + "github.com/rabbitstack/fibratus/pkg/source" "github.com/rabbitstack/fibratus/pkg/sys/etw" "github.com/rabbitstack/fibratus/pkg/util/multierror" log "github.com/sirupsen/logrus" @@ -55,20 +54,16 @@ const ( ) var ( - // failedKevents counts the number of kevents that failed to process - failedKevents = expvar.NewMap("kstream.kevents.failures") - // keventsProcessed counts the number of total processed events - keventsProcessed = expvar.NewInt("kstream.kevents.processed") - // keventsDropped counts the number of overall dropped events - keventsDropped = expvar.NewInt("kstream.kevents.dropped") - // keventsUnknown counts the number of published events which types are not present in the internal catalog - keventsUnknown = expvar.NewInt("kstream.kevents.unknown") - - // excludedKevents counts the number of excluded events - excludedKevents = expvar.NewInt("kstream.excluded.kevents") - + // eventsFailed counts the number of events that failed to process + eventsFailed = expvar.NewMap("eventsource.events.failed") + // eventsProcessed counts the number of total processed events + eventsProcessed = expvar.NewInt("eventsource.events.processed") + // eventsUnknown counts the number of published events which types are not present in the internal catalog + eventsUnknown = expvar.NewInt("eventsource.events.unknown") + // eventsExcluded counts the number of excluded events + eventsExcluded = expvar.NewInt("eventsource.events.excluded") // buffersRead amount of buffers fetched from the ETW session - buffersRead = expvar.NewInt("kstream.kbuffers.read") + buffersRead = expvar.NewInt("eventsource.buffers.read") ) // EventSource is the core component responsible for @@ -80,8 +75,8 @@ type EventSource struct { consumers []*Consumer errs chan error - evts chan *kevent.Kevent - sequencer *kevent.Sequencer + evts chan *event.Event + sequencer *event.Sequencer config *config.Config stop chan struct{} @@ -89,7 +84,7 @@ type EventSource struct { hsnap handle.Snapshotter filter filter.Filter - listeners []kevent.Listener + listeners []event.Listener isClosed bool } @@ -100,19 +95,19 @@ func NewEventSource( hsnap handle.Snapshotter, config *config.Config, compiler *config.RulesCompileResult, -) ksource.EventSource { +) source.EventSource { evs := &EventSource{ r: compiler, traces: make([]*Trace, 0), consumers: make([]*Consumer, 0), errs: make(chan error, 1000), - evts: make(chan *kevent.Kevent, 500), - sequencer: kevent.NewSequencer(), + evts: make(chan *event.Event, 500), + sequencer: event.NewSequencer(), config: config, stop: make(chan struct{}), psnap: psnap, hsnap: hsnap, - listeners: make([]kevent.Listener, 0), + listeners: make([]event.Listener, 0), } return evs } @@ -140,29 +135,29 @@ func (e *EventSource) Open(config *config.Config) error { config.Kstream.EnableDNSEvents = config.Kstream.EnableDNSEvents && e.r.HasDNSEvents config.Kstream.EnableAuditAPIEvents = config.Kstream.EnableAuditAPIEvents && e.r.HasAuditAPIEvents config.Kstream.EnableThreadpoolEvents = config.Kstream.EnableThreadpoolEvents && e.r.HasThreadpoolEvents - for _, ktype := range ktypes.All() { - if ktype == ktypes.CreateProcess || ktype == ktypes.TerminateProcess || - ktype == ktypes.LoadImage || ktype == ktypes.UnloadImage { + for _, typ := range event.All() { + if typ == event.CreateProcess || typ == event.TerminateProcess || + typ == event.LoadImage || typ == event.UnloadImage { // always allow fundamental events continue } // allow events required for memory/file scanning - if ktype == ktypes.MapViewFile && config.Yara.Enabled && !config.Yara.SkipMmaps { + if typ == event.MapViewFile && config.Yara.Enabled && !config.Yara.SkipMmaps { continue } - if ktype == ktypes.VirtualAlloc && config.Yara.Enabled && !config.Yara.SkipAllocs { + if typ == event.VirtualAlloc && config.Yara.Enabled && !config.Yara.SkipAllocs { continue } - if ktype == ktypes.CreateFile && config.Yara.Enabled && !config.Yara.SkipFiles { + if typ == event.CreateFile && config.Yara.Enabled && !config.Yara.SkipFiles { continue } - if ktype == ktypes.RegSetValue && config.Yara.Enabled && !config.Yara.SkipRegistry { + if typ == event.RegSetValue && config.Yara.Enabled && !config.Yara.SkipRegistry { continue } - if !e.r.ContainsEvent(ktype) { - config.Kstream.SetDropMask(ktype) + if !e.r.ContainsEvent(typ) { + config.Kstream.SetDropMask(typ) } } } @@ -182,16 +177,16 @@ func (e *EventSource) Open(config *config.Config) error { for _, trace := range e.traces { err := trace.Start() switch err { - case kerrors.ErrTraceAlreadyRunning: + case errs.ErrTraceAlreadyRunning: log.Debugf("%s trace is already running. Trying to restart...", trace.Name) if err := trace.Stop(); err != nil { return err } time.Sleep(time.Millisecond * 100) if err := trace.Start(); err != nil { - return multierror.Wrap(kerrors.ErrRestartTrace, err) + return multierror.Wrap(errs.ErrRestartTrace, err) } - case kerrors.ErrTraceNoSysResources: + case errs.ErrTraceNoSysResources: // get the number of maximum allowed loggers from registry key, err := registry.OpenKey(registry.LOCAL_MACHINE, etwMaxLoggersPath, registry.QUERY_VALUE) if err != nil { @@ -238,7 +233,7 @@ func (e *EventSource) Open(config *config.Config) error { return case err := <-errch: log.Infof("stopping [%s] trace processing", trace.Name) - if err != nil && !errors.Is(err, kerrors.ErrTraceCancelled) { + if err != nil && !errors.Is(err, errs.ErrTraceCancelled) { e.errs <- fmt.Errorf("unable to process %s trace: %v", trace.Name, err) } } @@ -293,7 +288,7 @@ func (e *EventSource) Errors() <-chan error { } // Events returns the buffered event channel. -func (e *EventSource) Events() <-chan *kevent.Kevent { +func (e *EventSource) Events() <-chan *event.Event { return e.evts } @@ -306,7 +301,7 @@ func (e *EventSource) SetFilter(f filter.Filter) { // RegisterEventListener registers a new event listener for each consumer queue. // The event is pushed to the output queue if at least one of the listeners allows. -func (e *EventSource) RegisterEventListener(lis kevent.Listener) { +func (e *EventSource) RegisterEventListener(lis event.Listener) { e.listeners = append(e.listeners, lis) } diff --git a/internal/etw/source_test.go b/internal/etw/source_test.go index 326431198..a3a73b9ea 100644 --- a/internal/etw/source_test.go +++ b/internal/etw/source_test.go @@ -21,11 +21,10 @@ import ( "context" "fmt" "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/handle" htypes "github.com/rabbitstack/fibratus/pkg/handle/types" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/rabbitstack/fibratus/pkg/ps" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" "github.com/rabbitstack/fibratus/pkg/symbolize" @@ -58,7 +57,7 @@ type MockListener struct { func (l *MockListener) CanEnqueue() bool { return true } -func (l *MockListener) ProcessEvent(e *kevent.Kevent) (bool, error) { +func (l *MockListener) ProcessEvent(e *event.Event) (bool, error) { l.gotEvent = true return true, nil } @@ -171,16 +170,16 @@ func TestEventSourceEnableFlagsDynamically(t *testing.T) { HasThreadEvents: false, HasVAMapEvents: true, HasAuditAPIEvents: true, - UsedEvents: []ktypes.Ktype{ - ktypes.CreateProcess, - ktypes.LoadImage, - ktypes.RegCreateKey, - ktypes.RegSetValue, - ktypes.CreateFile, - ktypes.RenameFile, - ktypes.MapViewFile, - ktypes.OpenProcess, - ktypes.ConnectTCPv4, + UsedEvents: []event.Type{ + event.CreateProcess, + event.LoadImage, + event.RegCreateKey, + event.RegSetValue, + event.CreateFile, + event.RenameFile, + event.MapViewFile, + event.OpenProcess, + event.ConnectTCPv4, }, } cfg := &config.Config{ @@ -217,10 +216,10 @@ func TestEventSourceEnableFlagsDynamically(t *testing.T) { // but VAMap is disabled in the config require.True(t, flags&etw.VaMap == 0) - require.False(t, cfg.Kstream.TestDropMask(ktypes.UnloadImage)) - require.True(t, cfg.Kstream.TestDropMask(ktypes.WriteFile)) - require.True(t, cfg.Kstream.TestDropMask(ktypes.UnmapViewFile)) - require.False(t, cfg.Kstream.TestDropMask(ktypes.OpenProcess)) + require.False(t, cfg.Kstream.TestDropMask(event.UnloadImage)) + require.True(t, cfg.Kstream.TestDropMask(event.WriteFile)) + require.True(t, cfg.Kstream.TestDropMask(event.UnmapViewFile)) + require.False(t, cfg.Kstream.TestDropMask(event.OpenProcess)) } func TestEventSourceEnableFlagsDynamicallyWithYaraEnabled(t *testing.T) { @@ -249,14 +248,14 @@ func TestEventSourceEnableFlagsDynamicallyWithYaraEnabled(t *testing.T) { HasFileEvents: false, HasThreadEvents: false, HasAuditAPIEvents: true, - UsedEvents: []ktypes.Ktype{ - ktypes.CreateProcess, - ktypes.LoadImage, - ktypes.RegCreateKey, - ktypes.RegSetValue, - ktypes.RenameFile, - ktypes.OpenProcess, - ktypes.ConnectTCPv4, + UsedEvents: []event.Type{ + event.CreateProcess, + event.LoadImage, + event.RegCreateKey, + event.RegSetValue, + event.RenameFile, + event.OpenProcess, + event.ConnectTCPv4, }, } cfg := &config.Config{ @@ -293,9 +292,9 @@ func TestEventSourceEnableFlagsDynamicallyWithYaraEnabled(t *testing.T) { // alloc scanning is enabled require.True(t, flags&etw.VirtualAlloc != 0) - require.False(t, cfg.Kstream.TestDropMask(ktypes.CreateFile)) - require.True(t, cfg.Kstream.TestDropMask(ktypes.MapViewFile)) - require.False(t, cfg.Kstream.TestDropMask(ktypes.VirtualAlloc)) + require.False(t, cfg.Kstream.TestDropMask(event.CreateFile)) + require.True(t, cfg.Kstream.TestDropMask(event.MapViewFile)) + require.False(t, cfg.Kstream.TestDropMask(event.VirtualAlloc)) } func TestEventSourceRundownEvents(t *testing.T) { @@ -325,7 +324,7 @@ func TestEventSourceRundownEvents(t *testing.T) { } cfg := &config.Config{ Kstream: kstreamConfig, - KcapFile: "fake.kcap", // simulate capture to receive state/rundown events + KcapFile: "fake.cap", // simulate capture to receive state/rundown events Filters: &config.Filters{}, } @@ -336,12 +335,12 @@ func TestEventSourceRundownEvents(t *testing.T) { require.NoError(t, evs.Open(cfg)) defer evs.Close() - rundownsByType := map[ktypes.Ktype]bool{ - ktypes.ProcessRundown: false, - ktypes.ThreadRundown: false, - ktypes.ImageRundown: false, - ktypes.FileRundown: false, - ktypes.RegKCBRundown: false, + rundownsByType := map[event.Type]bool{ + event.ProcessRundown: false, + event.ThreadRundown: false, + event.ImageRundown: false, + event.FileRundown: false, + event.RegKCBRundown: false, } rundownsByHash := make(map[uint64]uint8) timeout := time.After(time.Minute) @@ -374,7 +373,7 @@ func TestEventSourceRundownEvents(t *testing.T) { } func TestEventSourceAllEvents(t *testing.T) { - kevent.DropCurrentProc = false + event.DropCurrentProc = false var viewBase uintptr var freeAddress uintptr var dupHandleID windows.Handle @@ -382,7 +381,7 @@ func TestEventSourceAllEvents(t *testing.T) { var tests = []*struct { name string gen func() error - want func(e *kevent.Kevent) bool + want func(e *event.Event) bool completed bool }{ { @@ -411,26 +410,26 @@ func TestEventSourceAllEvents(t *testing.T) { defer windows.TerminateProcess(pi.Process, 0) return nil }, - func(e *kevent.Kevent) bool { + func(e *event.Event) bool { return e.IsCreateProcess() && e.CurrentPid() && - strings.EqualFold(e.GetParamAsString(kparams.ProcessName), "notepad.exe") + strings.EqualFold(e.GetParamAsString(params.ProcessName), "notepad.exe") }, false, }, { "terminate process", nil, - func(e *kevent.Kevent) bool { - return e.IsTerminateProcess() && strings.EqualFold(e.GetParamAsString(kparams.ProcessName), "notepad.exe") + func(e *event.Event) bool { + return e.IsTerminateProcess() && strings.EqualFold(e.GetParamAsString(params.ProcessName), "notepad.exe") }, false, }, { "load image", nil, - func(e *kevent.Kevent) bool { + func(e *event.Event) bool { img := filepath.Join(os.Getenv("windir"), "System32", "notepad.exe") - return e.IsLoadImage() && strings.EqualFold(img, e.GetParamAsString(kparams.ImagePath)) + return e.IsLoadImage() && strings.EqualFold(img, e.GetParamAsString(params.ImagePath)) }, false, }, @@ -444,9 +443,9 @@ func TestEventSourceAllEvents(t *testing.T) { defer f.Close() return nil }, - func(e *kevent.Kevent) bool { - return e.CurrentPid() && e.Type == ktypes.CreateFile && - strings.HasPrefix(filepath.Base(e.GetParamAsString(kparams.FilePath)), "fibratus-test") && + func(e *event.Event) bool { + return e.CurrentPid() && e.Type == event.CreateFile && + strings.HasPrefix(filepath.Base(e.GetParamAsString(params.FilePath)), "fibratus-test") && !e.IsOpenDisposition() }, false, @@ -474,8 +473,8 @@ func TestEventSourceAllEvents(t *testing.T) { }() return nil }, - func(e *kevent.Kevent) bool { - return e.CurrentPid() && (e.Type == ktypes.ConnectTCPv4 || e.Type == ktypes.ConnectTCPv6) + func(e *event.Event) bool { + return e.CurrentPid() && (e.Type == event.ConnectTCPv4 || e.Type == event.ConnectTCPv6) }, false, }, @@ -527,11 +526,11 @@ func TestEventSourceAllEvents(t *testing.T) { } return nil }, - func(e *kevent.Kevent) bool { - return e.CurrentPid() && e.Type == ktypes.MapViewFile && - e.GetParamAsString(kparams.MemProtect) == "EXECUTE_READWRITE|READONLY" && - e.GetParamAsString(kparams.FileViewSectionType) == "IMAGE" && - strings.Contains(e.GetParamAsString(kparams.FilePath), "_fixtures\\yara-test.dll") + func(e *event.Event) bool { + return e.CurrentPid() && e.Type == event.MapViewFile && + e.GetParamAsString(params.MemProtect) == "EXECUTE_READWRITE|READONLY" && + e.GetParamAsString(params.FileViewSectionType) == "IMAGE" && + strings.Contains(e.GetParamAsString(params.FilePath), "_fixtures\\yara-test.dll") }, false, }, @@ -575,10 +574,10 @@ func TestEventSourceAllEvents(t *testing.T) { } return sys.NtUnmapViewOfSection(windows.CurrentProcess(), viewBase) }, - func(e *kevent.Kevent) bool { - return e.CurrentPid() && e.Type == ktypes.UnmapViewFile && - e.GetParamAsString(kparams.MemProtect) == "READONLY" && - e.Kparams.MustGetUint64(kparams.FileViewBase) == uint64(viewBase) + func(e *event.Event) bool { + return e.CurrentPid() && e.Type == event.UnmapViewFile && + e.GetParamAsString(params.MemProtect) == "READONLY" && + e.Params.MustGetUint64(params.FileViewBase) == uint64(viewBase) }, false, }, @@ -594,9 +593,9 @@ func TestEventSourceAllEvents(t *testing.T) { }() return nil }, - func(e *kevent.Kevent) bool { - return e.CurrentPid() && e.Type == ktypes.VirtualAlloc && - e.GetParamAsString(kparams.MemAllocType) == "COMMIT|RESERVE" && e.GetParamAsString(kparams.MemProtectMask) == "RWX" + func(e *event.Event) bool { + return e.CurrentPid() && e.Type == event.VirtualAlloc && + e.GetParamAsString(params.MemAllocType) == "COMMIT|RESERVE" && e.GetParamAsString(params.MemProtectMask) == "RWX" }, false, }, @@ -610,9 +609,9 @@ func TestEventSourceAllEvents(t *testing.T) { } return windows.VirtualFree(freeAddress, 1024, windows.MEM_DECOMMIT) }, - func(e *kevent.Kevent) bool { - return e.CurrentPid() && e.Type == ktypes.VirtualFree && - e.GetParamAsString(kparams.MemAllocType) == "DECOMMIT" && e.Kparams.MustGetUint64(kparams.MemBaseAddress) == uint64(freeAddress) + func(e *event.Event) bool { + return e.CurrentPid() && e.Type == event.VirtualFree && + e.GetParamAsString(params.MemAllocType) == "DECOMMIT" && e.Params.MustGetUint64(params.MemBaseAddress) == uint64(freeAddress) }, false, }, @@ -660,10 +659,10 @@ func TestEventSourceAllEvents(t *testing.T) { defer windows.Close(dup) return nil }, - func(e *kevent.Kevent) bool { - return e.CurrentPid() && e.Type == ktypes.DuplicateHandle && - e.GetParamAsString(kparams.HandleObjectTypeID) == handle.Key && - windows.Handle(e.Kparams.MustGetUint32(kparams.HandleSourceID)) == dupHandleID + func(e *event.Event) bool { + return e.CurrentPid() && e.Type == event.DuplicateHandle && + e.GetParamAsString(params.HandleObjectTypeID) == handle.Key && + windows.Handle(e.Params.MustGetUint32(params.HandleSourceID)) == dupHandleID }, false, }, @@ -673,11 +672,11 @@ func TestEventSourceAllEvents(t *testing.T) { _, err := net.LookupHost("dns.google") return err }, - func(e *kevent.Kevent) bool { - return e.CurrentPid() && e.Type == ktypes.QueryDNS && e.IsDNS() && - e.Type.Subcategory() == ktypes.DNS && - e.GetParamAsString(kparams.DNSName) == "dns.google" && - e.GetParamAsString(kparams.DNSRR) == "A" + func(e *event.Event) bool { + return e.CurrentPid() && e.Type == event.QueryDNS && e.IsDNS() && + e.Type.Subcategory() == event.DNS && + e.GetParamAsString(params.DNSName) == "dns.google" && + e.GetParamAsString(params.DNSRR) == "A" }, false, }, @@ -687,13 +686,13 @@ func TestEventSourceAllEvents(t *testing.T) { _, err := net.LookupHost("dns.google") return err }, - func(e *kevent.Kevent) bool { - return e.CurrentPid() && e.Type == ktypes.ReplyDNS && e.IsDNS() && - e.Type.Subcategory() == ktypes.DNS && - e.GetParamAsString(kparams.DNSName) == "dns.google" && - e.GetParamAsString(kparams.DNSRR) == "AAAA" && - e.GetParamAsString(kparams.DNSRcode) == "NOERROR" && - e.GetParamAsString(kparams.DNSAnswers) != "" + func(e *event.Event) bool { + return e.CurrentPid() && e.Type == event.ReplyDNS && e.IsDNS() && + e.Type.Subcategory() == event.DNS && + e.GetParamAsString(params.DNSName) == "dns.google" && + e.GetParamAsString(params.DNSRR) == "AAAA" && + e.GetParamAsString(params.DNSRcode) == "NOERROR" && + e.GetParamAsString(params.DNSAnswers) != "" }, false, }, @@ -702,8 +701,8 @@ func TestEventSourceAllEvents(t *testing.T) { func() error { return nil }, - func(e *kevent.Kevent) bool { - return e.CurrentPid() && e.Type == ktypes.SetThreadContext && e.GetParamAsString(kparams.NTStatus) == "Success" + func(e *event.Event) bool { + return e.CurrentPid() && e.Type == event.SetThreadContext && e.GetParamAsString(params.NTStatus) == "Success" }, false, }, @@ -806,21 +805,21 @@ type NoopPsSnapshotter struct{} var fakeProc = &pstypes.PS{PID: 111111, Name: "fake.exe"} -func (s *NoopPsSnapshotter) Write(kevt *kevent.Kevent) error { return nil } -func (s *NoopPsSnapshotter) Remove(kevt *kevent.Kevent) error { return nil } +func (s *NoopPsSnapshotter) Write(evt *event.Event) error { return nil } +func (s *NoopPsSnapshotter) Remove(evt *event.Event) error { return nil } func (s *NoopPsSnapshotter) Find(pid uint32) (bool, *pstypes.PS) { return true, fakeProc } func (s *NoopPsSnapshotter) FindAndPut(pid uint32) *pstypes.PS { return fakeProc } func (s *NoopPsSnapshotter) Put(ps *pstypes.PS) {} func (s *NoopPsSnapshotter) Size() uint32 { return 1 } func (s *NoopPsSnapshotter) Close() error { return nil } func (s *NoopPsSnapshotter) GetSnapshot() []*pstypes.PS { return nil } -func (s *NoopPsSnapshotter) AddThread(kevt *kevent.Kevent) error { return nil } -func (s *NoopPsSnapshotter) AddModule(kevt *kevent.Kevent) error { return nil } +func (s *NoopPsSnapshotter) AddThread(evt *event.Event) error { return nil } +func (s *NoopPsSnapshotter) AddModule(evt *event.Event) error { return nil } func (s *NoopPsSnapshotter) FindModule(addr va.Address) (bool, *pstypes.Module) { return false, nil } func (s *NoopPsSnapshotter) RemoveThread(pid uint32, tid uint32) error { return nil } func (s *NoopPsSnapshotter) RemoveModule(pid uint32, addr va.Address) error { return nil } -func (s *NoopPsSnapshotter) WriteFromKcap(kevt *kevent.Kevent) error { return nil } -func (s *NoopPsSnapshotter) AddMmap(kevt *kevent.Kevent) error { return nil } +func (s *NoopPsSnapshotter) WriteFromKcap(evt *event.Event) error { return nil } +func (s *NoopPsSnapshotter) AddMmap(evt *event.Event) error { return nil } func (s *NoopPsSnapshotter) RemoveMmap(pid uint32, addr va.Address) error { return nil } func TestCallstackEnrichment(t *testing.T) { @@ -851,14 +850,14 @@ func TestCallstackEnrichment(t *testing.T) { } func testCallstackEnrichment(t *testing.T, hsnap handle.Snapshotter, psnap ps.Snapshotter) { - kevent.DropCurrentProc = false + event.DropCurrentProc = false var procHandle windows.Handle var tests = []*struct { name string gen func() error - want func(e *kevent.Kevent) bool + want func(e *event.Event) bool completed bool }{ { @@ -887,9 +886,9 @@ func testCallstackEnrichment(t *testing.T, hsnap handle.Snapshotter, psnap ps.Sn procHandle = pi.Process return nil }, - func(e *kevent.Kevent) bool { + func(e *event.Event) bool { if e.IsCreateProcess() && e.CurrentPid() && - strings.EqualFold(e.GetParamAsString(kparams.ProcessName), "notepad.exe") { + strings.EqualFold(e.GetParamAsString(params.ProcessName), "notepad.exe") { callstack := e.Callstack.String() log.Infof("create process event %s: %s", e.String(), callstack) return callstackContainsTestExe(callstack) && @@ -902,8 +901,8 @@ func testCallstackEnrichment(t *testing.T, hsnap handle.Snapshotter, psnap ps.Sn { "load image callstack", nil, - func(e *kevent.Kevent) bool { - if e.IsLoadImage() && filepath.Ext(e.GetParamAsString(kparams.FilePath)) == ".dll" { + func(e *event.Event) bool { + if e.IsLoadImage() && filepath.Ext(e.GetParamAsString(params.FilePath)) == ".dll" { callstack := e.Callstack.String() return strings.Contains(strings.ToLower(callstack), strings.ToLower("\\WINDOWS\\System32\\KERNELBASE.dll!LoadLibraryExW")) && strings.Contains(strings.ToLower(callstack), strings.ToLower("\\WINDOWS\\system32\\ntoskrnl.exe!NtMapViewOfSection")) @@ -915,7 +914,7 @@ func testCallstackEnrichment(t *testing.T, hsnap handle.Snapshotter, psnap ps.Sn { "create thread callstack", nil, - func(e *kevent.Kevent) bool { + func(e *event.Event) bool { if e.IsCreateThread() { callstack := e.Callstack.String() log.Infof("create thread event %s: %s", e.String(), callstack) @@ -929,7 +928,7 @@ func testCallstackEnrichment(t *testing.T, hsnap handle.Snapshotter, psnap ps.Sn { "terminate thread callstack", nil, - func(e *kevent.Kevent) bool { + func(e *event.Event) bool { if e.IsTerminateThread() { callstack := e.Callstack.String() log.Infof("terminate thread event %s: %s", e.String(), callstack) @@ -954,8 +953,8 @@ func testCallstackEnrichment(t *testing.T, hsnap handle.Snapshotter, psnap ps.Sn defer registry.DeleteKey(registry.CURRENT_USER, path) return nil }, - func(e *kevent.Kevent) bool { - if e.CurrentPid() && e.Type == ktypes.RegCreateKey && e.GetParamAsString(kparams.RegPath) == "HKEY_CURRENT_USER\\Volatile Environment\\CallstackTest" { + func(e *event.Event) bool { + if e.CurrentPid() && e.Type == event.RegCreateKey && e.GetParamAsString(params.RegPath) == "HKEY_CURRENT_USER\\Volatile Environment\\CallstackTest" { callstack := e.Callstack.String() log.Infof("create key event %s: %s", e.String(), callstack) return callstackContainsTestExe(callstack) && @@ -970,8 +969,8 @@ func testCallstackEnrichment(t *testing.T, hsnap handle.Snapshotter, psnap ps.Sn { "delete registry key callstack", nil, - func(e *kevent.Kevent) bool { - if e.CurrentPid() && e.Type == ktypes.RegDeleteKey { + func(e *event.Event) bool { + if e.CurrentPid() && e.Type == event.RegDeleteKey { callstack := e.Callstack.String() log.Infof("delete key event %s: %s", e.String(), callstack) return callstackContainsTestExe(callstack) && @@ -992,8 +991,8 @@ func testCallstackEnrichment(t *testing.T, hsnap handle.Snapshotter, psnap ps.Sn defer key.DeleteValue("FibratusCallstack") return key.SetStringValue("FibratusCallstack", "Callstack") }, - func(e *kevent.Kevent) bool { - if e.CurrentPid() && e.Type == ktypes.RegSetValue && strings.HasSuffix(e.GetParamAsString(kparams.RegPath), "FibratusCallstack") { + func(e *event.Event) bool { + if e.CurrentPid() && e.Type == event.RegSetValue && strings.HasSuffix(e.GetParamAsString(params.RegPath), "FibratusCallstack") { callstack := e.Callstack.String() log.Infof("set value event %s: %s", e.String(), callstack) return callstackContainsTestExe(callstack) && @@ -1008,8 +1007,8 @@ func testCallstackEnrichment(t *testing.T, hsnap handle.Snapshotter, psnap ps.Sn { "delete registry value callstack", nil, - func(e *kevent.Kevent) bool { - if e.CurrentPid() && e.Type == ktypes.RegDeleteValue { + func(e *event.Event) bool { + if e.CurrentPid() && e.Type == event.RegDeleteValue { callstack := e.Callstack.String() log.Infof("delete value event %s: %s", e.String(), callstack) return callstackContainsTestExe(callstack) && @@ -1022,8 +1021,8 @@ func testCallstackEnrichment(t *testing.T, hsnap handle.Snapshotter, psnap ps.Sn { "set thread context callstack", nil, - func(e *kevent.Kevent) bool { - return e.Type == ktypes.SetThreadContext && + func(e *event.Event) bool { + return e.Type == event.SetThreadContext && callstackContainsTestExe(e.Callstack.String()) && strings.Contains(strings.ToLower(e.Callstack.String()), strings.ToLower("\\WINDOWS\\System32\\KERNELBASE.dll!SetThreadContext")) }, @@ -1039,9 +1038,9 @@ func testCallstackEnrichment(t *testing.T, hsnap handle.Snapshotter, psnap ps.Sn defer f.Close() return nil }, - func(e *kevent.Kevent) bool { - if e.CurrentPid() && e.Type == ktypes.CreateFile && - strings.HasPrefix(filepath.Base(e.GetParamAsString(kparams.FilePath)), "fibratus-callstack") && + func(e *event.Event) bool { + if e.CurrentPid() && e.Type == event.CreateFile && + strings.HasPrefix(filepath.Base(e.GetParamAsString(params.FilePath)), "fibratus-callstack") && !e.IsOpenDisposition() { callstack := e.Callstack.String() log.Infof("create file event %s: %s", e.String(), callstack) @@ -1068,9 +1067,9 @@ func testCallstackEnrichment(t *testing.T, hsnap handle.Snapshotter, psnap ps.Sn defer windows.Close(h) return nil }, - func(e *kevent.Kevent) bool { - if e.CurrentPid() && e.Type == ktypes.CreateFile && - strings.HasPrefix(filepath.Base(e.GetParamAsString(kparams.FilePath)), "fibratus-file-transacted") && + func(e *event.Event) bool { + if e.CurrentPid() && e.Type == event.CreateFile && + strings.HasPrefix(filepath.Base(e.GetParamAsString(params.FilePath)), "fibratus-file-transacted") && !e.IsOpenDisposition() { callstack := e.Callstack.String() log.Infof("create transacted file event %s: %s", e.String(), callstack) @@ -1090,9 +1089,9 @@ func testCallstackEnrichment(t *testing.T, hsnap handle.Snapshotter, psnap ps.Sn } return nil }, - func(e *kevent.Kevent) bool { - if e.CurrentPid() && e.Type == ktypes.VirtualAlloc && - e.GetParamAsString(kparams.MemAllocType) == "COMMIT|RESERVE" { + func(e *event.Event) bool { + if e.CurrentPid() && e.Type == event.VirtualAlloc && + e.GetParamAsString(params.MemAllocType) == "COMMIT|RESERVE" { callstack := e.Callstack.String() log.Infof("virtual alloc event %s: %s", e.String(), callstack) return callstackContainsTestExe(callstack) && @@ -1115,9 +1114,9 @@ func testCallstackEnrichment(t *testing.T, hsnap handle.Snapshotter, psnap ps.Sn // to, _ := windows.UTF16PtrFromString(filepath.Join(os.TempDir(), "copied-file")) // return copyFile(from, to) // }, - // func(e *kevent.Kevent) bool { - // if e.CurrentPid() && e.Type == ktypes.CreateFile && - // strings.HasPrefix(filepath.Base(e.GetParamAsString(kparams.FileName)), "copied-file") && + // func(e *event.Event) bool { + // if e.CurrentPid() && e.Type == event.CreateFile && + // strings.HasPrefix(filepath.Base(e.GetParamAsString(params.FileName)), "copied-file") && // !e.IsOpenDisposition() { // callstack := e.Callstack.String() // log.Infof("copy file event %s: %s", e.String(), callstack) @@ -1138,9 +1137,9 @@ func testCallstackEnrichment(t *testing.T, hsnap handle.Snapshotter, psnap ps.Sn f.Close() return os.Remove(f.Name()) }, - func(e *kevent.Kevent) bool { - if e.CurrentPid() && e.Type == ktypes.DeleteFile && - strings.HasPrefix(filepath.Base(e.GetParamAsString(kparams.FilePath)), "fibratus-delete") { + func(e *event.Event) bool { + if e.CurrentPid() && e.Type == event.DeleteFile && + strings.HasPrefix(filepath.Base(e.GetParamAsString(params.FilePath)), "fibratus-delete") { callstack := e.Callstack.String() log.Infof("delete file event %s: %s", e.String(), callstack) return callstackContainsTestExe(callstack) && @@ -1163,9 +1162,9 @@ func testCallstackEnrichment(t *testing.T, hsnap handle.Snapshotter, psnap ps.Sn } return os.Remove(filepath.Join(os.TempDir(), "fibratus-ren")) }, - func(e *kevent.Kevent) bool { - if e.CurrentPid() && e.Type == ktypes.RenameFile && - strings.HasPrefix(filepath.Base(e.GetParamAsString(kparams.FilePath)), "fibratus-rename") { + func(e *event.Event) bool { + if e.CurrentPid() && e.Type == event.RenameFile && + strings.HasPrefix(filepath.Base(e.GetParamAsString(params.FilePath)), "fibratus-rename") { callstack := e.Callstack.String() log.Infof("rename file event %s: %s", e.String(), callstack) return callstackContainsTestExe(callstack) && @@ -1181,8 +1180,8 @@ func testCallstackEnrichment(t *testing.T, hsnap handle.Snapshotter, psnap ps.Sn _, err := windows.OpenProcess(windows.PROCESS_VM_READ, false, uint32(os.Getpid())) return err }, - func(e *kevent.Kevent) bool { - if e.CurrentPid() && e.Type == ktypes.OpenProcess { + func(e *event.Event) bool { + if e.CurrentPid() && e.Type == event.OpenProcess { callstack := e.Callstack.String() log.Infof("open process event %s: %s", e.String(), callstack) return callstackContainsTestExe(callstack) && @@ -1198,8 +1197,8 @@ func testCallstackEnrichment(t *testing.T, hsnap handle.Snapshotter, psnap ps.Sn _, err := windows.OpenThread(windows.THREAD_IMPERSONATE, false, windows.GetCurrentThreadId()) return err }, - func(e *kevent.Kevent) bool { - if e.CurrentPid() && e.Type == ktypes.OpenThread { + func(e *event.Event) bool { + if e.CurrentPid() && e.Type == event.OpenThread { callstack := e.Callstack.String() log.Infof("open thread event %s: %s", e.String(), callstack) return callstackContainsTestExe(callstack) && diff --git a/internal/etw/stackext.go b/internal/etw/stackext.go index 03c348c1a..e87cc53e8 100644 --- a/internal/etw/stackext.go +++ b/internal/etw/stackext.go @@ -20,7 +20,7 @@ package etw import ( "github.com/rabbitstack/fibratus/pkg/config" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/sys/etw" "golang.org/x/sys/windows" ) @@ -38,15 +38,15 @@ func NewStackExtensions(config config.KstreamConfig) *StackExtensions { } // AddStackTracing enables stack tracing for the specified event type. -func (s *StackExtensions) AddStackTracing(ktype ktypes.Ktype) { - if !s.config.TestDropMask(ktype) { - s.ids = append(s.ids, etw.NewClassicEventID(ktype.GUID(), ktype.HookID())) +func (s *StackExtensions) AddStackTracing(Type event.Type) { + if !s.config.TestDropMask(Type) { + s.ids = append(s.ids, etw.NewClassicEventID(Type.GUID(), Type.HookID())) } } // AddStackTracingWith enables stack tracing for the specified provider GUID and event hook id. func (s *StackExtensions) AddStackTracingWith(guid windows.GUID, hookID uint16) { - if !s.config.TestDropMask(ktypes.FromParts(guid, hookID)) { + if !s.config.TestDropMask(event.TypeFromParts(guid, hookID)) { s.ids = append(s.ids, etw.NewClassicEventID(guid, hookID)) } } @@ -60,13 +60,13 @@ func (s *StackExtensions) EventIds() []etw.ClassicEventID { return s.ids } // creating/terminating a thread or loading an image into // process address space. func (s *StackExtensions) EnableProcessCallstack() { - s.AddStackTracing(ktypes.CreateProcess) + s.AddStackTracing(event.CreateProcess) if s.config.EnableThreadKevents { - s.AddStackTracing(ktypes.CreateThread) - s.AddStackTracing(ktypes.TerminateThread) + s.AddStackTracing(event.CreateThread) + s.AddStackTracing(event.TerminateThread) } if s.config.EnableImageKevents { - s.AddStackTracingWith(ktypes.ProcessEventGUID, ktypes.LoadImage.HookID()) + s.AddStackTracingWith(event.ProcessEventGUID, event.LoadImage.HookID()) } } @@ -75,9 +75,9 @@ func (s *StackExtensions) EnableProcessCallstack() { // return addresses for file system activity. func (s *StackExtensions) EnableFileCallstack() { if s.config.EnableFileIOKevents { - s.AddStackTracing(ktypes.CreateFile) - s.AddStackTracing(ktypes.DeleteFile) - s.AddStackTracing(ktypes.RenameFile) + s.AddStackTracing(event.CreateFile) + s.AddStackTracing(event.DeleteFile) + s.AddStackTracing(event.RenameFile) } } @@ -86,10 +86,10 @@ func (s *StackExtensions) EnableFileCallstack() { // return addresses for registry operations. func (s *StackExtensions) EnableRegistryCallstack() { if s.config.EnableRegistryKevents { - s.AddStackTracing(ktypes.RegCreateKey) - s.AddStackTracing(ktypes.RegDeleteKey) - s.AddStackTracing(ktypes.RegSetValue) - s.AddStackTracing(ktypes.RegDeleteValue) + s.AddStackTracing(event.RegCreateKey) + s.AddStackTracing(event.RegDeleteKey) + s.AddStackTracing(event.RegSetValue) + s.AddStackTracing(event.RegDeleteValue) } } @@ -97,15 +97,15 @@ func (s *StackExtensions) EnableRegistryCallstack() { // events such as memory allocations. func (s *StackExtensions) EnableMemoryCallstack() { if s.config.EnableMemKevents { - s.AddStackTracing(ktypes.VirtualAlloc) + s.AddStackTracing(event.VirtualAlloc) } } // EnableThreadpoolCallstack enables stack tracing for thread pool events. func (s *StackExtensions) EnableThreadpoolCallstack() { if s.config.EnableThreadpoolEvents { - s.AddStackTracing(ktypes.SubmitThreadpoolWork) - s.AddStackTracing(ktypes.SubmitThreadpoolCallback) - s.AddStackTracing(ktypes.SetThreadpoolTimer) + s.AddStackTracing(event.SubmitThreadpoolWork) + s.AddStackTracing(event.SubmitThreadpoolCallback) + s.AddStackTracing(event.SetThreadpoolTimer) } } diff --git a/internal/etw/stackext_test.go b/internal/etw/stackext_test.go index 0f8457b86..a2ea5470c 100644 --- a/internal/etw/stackext_test.go +++ b/internal/etw/stackext_test.go @@ -20,7 +20,7 @@ package etw import ( "github.com/rabbitstack/fibratus/pkg/config" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/sys/etw" "github.com/stretchr/testify/assert" "testing" @@ -47,11 +47,11 @@ func TestStackExtensions(t *testing.T) { exts.EnableMemoryCallstack() assert.Len(t, exts.EventIds(), 7) - assert.Contains(t, exts.EventIds(), etw.ClassicEventID{GUID: ktypes.ProcessEventGUID, Type: uint8(ktypes.CreateProcess.HookID())}) - assert.Contains(t, exts.EventIds(), etw.ClassicEventID{GUID: ktypes.ThreadEventGUID, Type: uint8(ktypes.CreateThread.HookID())}) - assert.Contains(t, exts.EventIds(), etw.ClassicEventID{GUID: ktypes.ThreadEventGUID, Type: uint8(ktypes.TerminateThread.HookID())}) - assert.Contains(t, exts.EventIds(), etw.ClassicEventID{GUID: ktypes.FileEventGUID, Type: uint8(ktypes.CreateFile.HookID())}) - assert.Contains(t, exts.EventIds(), etw.ClassicEventID{GUID: ktypes.FileEventGUID, Type: uint8(ktypes.RenameFile.HookID())}) - assert.Contains(t, exts.EventIds(), etw.ClassicEventID{GUID: ktypes.FileEventGUID, Type: uint8(ktypes.DeleteFile.HookID())}) - assert.Contains(t, exts.EventIds(), etw.ClassicEventID{GUID: ktypes.MemEventGUID, Type: uint8(ktypes.VirtualAlloc.HookID())}) + assert.Contains(t, exts.EventIds(), etw.ClassicEventID{GUID: event.ProcessEventGUID, Type: uint8(event.CreateProcess.HookID())}) + assert.Contains(t, exts.EventIds(), etw.ClassicEventID{GUID: event.ThreadEventGUID, Type: uint8(event.CreateThread.HookID())}) + assert.Contains(t, exts.EventIds(), etw.ClassicEventID{GUID: event.ThreadEventGUID, Type: uint8(event.TerminateThread.HookID())}) + assert.Contains(t, exts.EventIds(), etw.ClassicEventID{GUID: event.FileEventGUID, Type: uint8(event.CreateFile.HookID())}) + assert.Contains(t, exts.EventIds(), etw.ClassicEventID{GUID: event.FileEventGUID, Type: uint8(event.RenameFile.HookID())}) + assert.Contains(t, exts.EventIds(), etw.ClassicEventID{GUID: event.FileEventGUID, Type: uint8(event.DeleteFile.HookID())}) + assert.Contains(t, exts.EventIds(), etw.ClassicEventID{GUID: event.MemEventGUID, Type: uint8(event.VirtualAlloc.HookID())}) } diff --git a/internal/etw/trace.go b/internal/etw/trace.go index 1f6fce354..7a85963c4 100644 --- a/internal/etw/trace.go +++ b/internal/etw/trace.go @@ -284,7 +284,7 @@ func (t *Trace) processEventCallback(ev *etw.EventRecord) uintptr { } if err := t.consumer.ProcessEvent(ev); err != nil { t.errs <- err - failedKevents.Add(err.Error(), 1) + eventsFailed.Add(err.Error(), 1) } return callbackNext } diff --git a/make.bat b/make.bat index c6dd53632..b17ae597d 100644 --- a/make.bat +++ b/make.bat @@ -29,7 +29,7 @@ set LDFLAGS="-s -w -X github.com/rabbitstack/fibratus/cmd/fibratus/app.version=% :: In case you want to avoid CGO overhead or don't need a specific feature, try tweaking the following compilation tags: :: -:: kcap: enables capture support +:: cap: enables capture support :: filament: enables running filaments and thus interacting with the CPython interpreter :: yara: enables YARA scanner via cgo bindings if NOT DEFINED TAGS ( diff --git a/pkg/aggregator/aggregator.go b/pkg/aggregator/aggregator.go index 991be1ca9..5993bba0c 100644 --- a/pkg/aggregator/aggregator.go +++ b/pkg/aggregator/aggregator.go @@ -25,7 +25,7 @@ import ( "github.com/rabbitstack/fibratus/pkg/aggregator/transformers" "github.com/rabbitstack/fibratus/pkg/alertsender" - "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/outputs" log "github.com/sirupsen/logrus" @@ -51,27 +51,27 @@ import ( ) var ( - // keventsDequeued counts the number of dequeued events - keventsDequeued = expvar.NewInt("kstream.kevents.dequeued") + // eventsDequeued counts the number of dequeued events + eventsDequeued = expvar.NewInt("aggregator.events.dequeued") // flushesCount computes the total count of aggregator flushes flushesCount = expvar.NewInt("aggregator.flushes.count") // batchEvents represents the overall number of processed batches batchEvents = expvar.NewInt("aggregator.batch.events") // transformerErrors is the count of errors occurred when applying transformers transformerErrors = expvar.NewMap("aggregator.transformer.errors") - // keventErrors is the number of event errors - keventErrors = expvar.NewInt("aggregator.kevent.errors") + // eventsErrors is the number of event errors + eventsErrors = expvar.NewInt("aggregator.event.errors") ) // BufferedAggregator collects events from the inbound channel and produces batches on regular intervals. The batches // are pushed to the work queue from which load-balanced configured workers consume the batches and publish to the outputs. type BufferedAggregator struct { - kevtsc <-chan *kevent.Kevent + evtsc <-chan *event.Event errsc <-chan error stop chan struct{} flusher *time.Ticker - // queue of inbound kernel events - kevts []*kevent.Kevent + // queue of inbound events + evts []*event.Event // work queue that forwarder passes to outputs wq queue submitter *submitter @@ -81,7 +81,7 @@ type BufferedAggregator struct { // NewBuffered creates a new instance of the event aggregator. func NewBuffered( - evts <-chan *kevent.Kevent, + evts <-chan *event.Event, errs <-chan error, aggConfig Config, outputConfig outputs.Config, @@ -93,12 +93,12 @@ func NewBuffered( flushInterval = time.Millisecond * 250 } agg := &BufferedAggregator{ - kevtsc: evts, - kevts: make([]*kevent.Kevent, 0), + evtsc: evts, + evts: make([]*event.Event, 0), errsc: errs, stop: make(chan struct{}, 1), flusher: time.NewTicker(flushInterval), - wq: make(chan *kevent.Batch), + wq: make(chan *event.Batch), c: aggConfig, } @@ -127,7 +127,7 @@ func (agg *BufferedAggregator) Stop() error { agg.stop <- struct{}{} // flush enqueued events - b := kevent.NewBatch(agg.kevts...) + b := event.NewBatch(agg.evts...) if b.Len() > 0 { done := make(chan struct{}, 1) go func() { @@ -163,10 +163,10 @@ func (agg *BufferedAggregator) run() { agg.flusher.Stop() return case <-agg.flusher.C: - if len(agg.kevts) == 0 { + if len(agg.evts) == 0 { continue } - b := kevent.NewBatch(agg.kevts...) + b := event.NewBatch(agg.evts...) l := b.Len() batchEvents.Add(l) // push the batch to the work queue @@ -175,8 +175,8 @@ func (agg *BufferedAggregator) run() { } flushesCount.Add(1) // clear the queue - agg.kevts = nil - case evt := <-agg.kevtsc: + agg.evts = nil + case evt := <-agg.evtsc: for _, transform := range agg.transforms { err := transform.Transform(evt) if err != nil { @@ -184,10 +184,10 @@ func (agg *BufferedAggregator) run() { } } // push the event to the queue - agg.kevts = append(agg.kevts, evt) - keventsDequeued.Add(1) + agg.evts = append(agg.evts, evt) + eventsDequeued.Add(1) case err := <-agg.errsc: - keventErrors.Add(1) + eventsErrors.Add(1) log.Errorf("event processing failure: %v", err) } } diff --git a/pkg/aggregator/aggregator_test.go b/pkg/aggregator/aggregator_test.go index 8e3568f6b..37ad1bbb6 100644 --- a/pkg/aggregator/aggregator_test.go +++ b/pkg/aggregator/aggregator_test.go @@ -19,9 +19,8 @@ package aggregator import ( - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/outputs" "github.com/rabbitstack/fibratus/pkg/outputs/console" "github.com/stretchr/testify/assert" @@ -32,7 +31,7 @@ import ( ) func TestNewBufferedAggregator(t *testing.T) { - keventsc := make(chan *kevent.Kevent, 20) + keventsc := make(chan *event.Event, 20) errsc := make(chan error, 1) agg, err := NewBuffered( keventsc, @@ -46,36 +45,36 @@ func TestNewBufferedAggregator(t *testing.T) { require.NotNil(t, agg) for i := 0; i < 4; i++ { - kevt := &kevent.Kevent{ - Type: ktypes.SendTCPv4, + evt := &event.Event{ + Type: event.SendTCPv4, Tid: 2484, PID: 859, - Kparams: kevent.Kparams{ - kparams.NetDport: {Name: kparams.NetDport, Type: kparams.Uint16, Value: uint16(443)}, - kparams.NetSport: {Name: kparams.NetSport, Type: kparams.Uint16, Value: uint16(43123)}, - kparams.NetSIP: {Name: kparams.NetSIP, Type: kparams.IPv4, Value: net.ParseIP("127.0.0.1")}, - kparams.NetDIP: {Name: kparams.NetDIP, Type: kparams.IPv4, Value: net.ParseIP("216.58.201.174")}, + Params: event.Params{ + params.NetDport: {Name: params.NetDport, Type: params.Uint16, Value: uint16(443)}, + params.NetSport: {Name: params.NetSport, Type: params.Uint16, Value: uint16(43123)}, + params.NetSIP: {Name: params.NetSIP, Type: params.IPv4, Value: net.ParseIP("127.0.0.1")}, + params.NetDIP: {Name: params.NetDIP, Type: params.IPv4, Value: net.ParseIP("216.58.201.174")}, }, } - keventsc <- kevt + keventsc <- evt } <-time.After(time.Millisecond * 275) assert.Equal(t, int64(4), batchEvents.Value()) for i := 0; i < 2; i++ { - kevt := &kevent.Kevent{ - Type: ktypes.SendTCPv4, + evt := &event.Event{ + Type: event.SendTCPv4, Tid: 2484, PID: 859, Seq: uint64(i), - Kparams: kevent.Kparams{ - kparams.NetDport: {Name: kparams.NetDport, Type: kparams.Uint16, Value: uint16(443)}, - kparams.NetSport: {Name: kparams.NetSport, Type: kparams.Uint16, Value: uint16(43123)}, - kparams.NetSIP: {Name: kparams.NetSIP, Type: kparams.IPv4, Value: net.ParseIP("127.0.0.1")}, - kparams.NetDIP: {Name: kparams.NetDIP, Type: kparams.IPv4, Value: net.ParseIP("216.58.201.174")}, + Params: event.Params{ + params.NetDport: {Name: params.NetDport, Type: params.Uint16, Value: uint16(443)}, + params.NetSport: {Name: params.NetSport, Type: params.Uint16, Value: uint16(43123)}, + params.NetSIP: {Name: params.NetSIP, Type: params.IPv4, Value: net.ParseIP("127.0.0.1")}, + params.NetDIP: {Name: params.NetDIP, Type: params.IPv4, Value: net.ParseIP("216.58.201.174")}, }, } - keventsc <- kevt + keventsc <- evt } <-time.After(time.Millisecond * 260) assert.Equal(t, int64(6), batchEvents.Value()) diff --git a/pkg/aggregator/submitter.go b/pkg/aggregator/submitter.go index 528f7c20d..2b1aadba4 100644 --- a/pkg/aggregator/submitter.go +++ b/pkg/aggregator/submitter.go @@ -19,12 +19,12 @@ package aggregator import ( - "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/outputs" ) // queue defines the type alias for the batch worker queue -type queue chan *kevent.Batch +type queue chan *event.Batch // submitter initializes a group of load balanced output producers. type submitter struct { diff --git a/pkg/aggregator/transformers/config.go b/pkg/aggregator/transformers/config.go index 41f9d60a3..b80270fc7 100644 --- a/pkg/aggregator/transformers/config.go +++ b/pkg/aggregator/transformers/config.go @@ -20,7 +20,7 @@ package transformers import "fmt" -// ErrInvalidConfig signals an invalid configuration input +// ErrInvalidConfig signals an invalid transformer configuration var ErrInvalidConfig = func(name Type) error { return fmt.Errorf("invalid config for %q transformer", name) } // Config acts as a container for the transformer configuration structures. diff --git a/pkg/aggregator/transformers/remove/config.go b/pkg/aggregator/transformers/remove/config.go index 1362a983f..daf7dfd1d 100644 --- a/pkg/aggregator/transformers/remove/config.go +++ b/pkg/aggregator/transformers/remove/config.go @@ -21,20 +21,20 @@ package remove import "github.com/spf13/pflag" const ( - kpars = "transformers.remove.kparams" + pars = "transformers.remove.params" enabled = "transformers.remove.enabled" ) // Config stores the configuration for the remove transformer. type Config struct { - // Kparams is the list of parameters that are dropped from the event. - Kparams []string `mapstructure:"kparams"` + // Params is the list of parameters that are dropped from the event. + Params []string `mapstructure:"params"` // Enabled indicates whether this transformer is enabled Enabled bool `mapstructure:"enabled"` } // AddFlags registers persistent flags. func AddFlags(flags *pflag.FlagSet) { - flags.StringSlice(kpars, []string{}, "A list of comma-separated parameters that will be removed from the event") + flags.StringSlice(pars, []string{}, "A list of comma-separated parameters that will be removed from the event") flags.Bool(enabled, false, "Indicates if remove transformer is enabled") } diff --git a/pkg/aggregator/transformers/remove/remove.go b/pkg/aggregator/transformers/remove/remove.go index 1b81c71af..0a8856349 100644 --- a/pkg/aggregator/transformers/remove/remove.go +++ b/pkg/aggregator/transformers/remove/remove.go @@ -21,12 +21,12 @@ package remove import ( "expvar" "github.com/rabbitstack/fibratus/pkg/aggregator/transformers" - "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/event" ) var removedCount = expvar.NewInt("transformers.removed.params") -// remove transformer deletes kparams that are given in the list. +// remove transformer deletes params that are given in the list. type remove struct { c Config } @@ -43,9 +43,9 @@ func initRemoveTransformer(config transformers.Config) (transformers.Transformer return &remove{c: cfg}, nil } -func (r remove) Transform(kevt *kevent.Kevent) error { - for _, kpar := range r.c.Kparams { - delete(kevt.Kparams, kpar) +func (r remove) Transform(evt *event.Event) error { + for _, par := range r.c.Params { + delete(evt.Params, par) removedCount.Add(1) } return nil diff --git a/pkg/aggregator/transformers/remove/remove_test.go b/pkg/aggregator/transformers/remove/remove_test.go index 75dfbb4ec..0e114be1a 100644 --- a/pkg/aggregator/transformers/remove/remove_test.go +++ b/pkg/aggregator/transformers/remove/remove_test.go @@ -20,9 +20,8 @@ package remove import ( "github.com/rabbitstack/fibratus/pkg/aggregator/transformers" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "net" @@ -30,24 +29,24 @@ import ( ) func TestTransform(t *testing.T) { - kevt := &kevent.Kevent{ - Type: ktypes.SendTCPv4, + evt := &event.Event{ + Type: event.SendTCPv4, Tid: 2484, PID: 859, - Kparams: kevent.Kparams{ - kparams.NetDport: {Name: kparams.NetDport, Type: kparams.Uint16, Value: uint16(443)}, - kparams.NetSport: {Name: kparams.NetSport, Type: kparams.Uint16, Value: uint16(43123)}, - kparams.NetSIP: {Name: kparams.NetSIP, Type: kparams.IPv4, Value: net.ParseIP("127.0.0.1")}, - kparams.NetDIP: {Name: kparams.NetDIP, Type: kparams.IPv4, Value: net.ParseIP("216.58.201.174")}, + Params: event.Params{ + params.NetDport: {Name: params.NetDport, Type: params.Uint16, Value: uint16(443)}, + params.NetSport: {Name: params.NetSport, Type: params.Uint16, Value: uint16(43123)}, + params.NetSIP: {Name: params.NetSIP, Type: params.IPv4, Value: net.ParseIP("127.0.0.1")}, + params.NetDIP: {Name: params.NetDIP, Type: params.IPv4, Value: net.ParseIP("216.58.201.174")}, }, } - assert.Len(t, kevt.Kparams, 4) + assert.Len(t, evt.Params, 4) - transf, err := transformers.Load(transformers.Config{Type: transformers.Remove, Transformer: Config{Kparams: []string{"dip", "sport", "foo"}}}) + transf, err := transformers.Load(transformers.Config{Type: transformers.Remove, Transformer: Config{Params: []string{"dip", "sport", "foo"}}}) require.NoError(t, err) - err = transf.Transform(kevt) + err = transf.Transform(evt) require.NoError(t, err) - assert.Len(t, kevt.Kparams, 2) + assert.Len(t, evt.Params, 2) } diff --git a/pkg/aggregator/transformers/rename/config.go b/pkg/aggregator/transformers/rename/config.go index b0b742dee..0464a75ec 100644 --- a/pkg/aggregator/transformers/rename/config.go +++ b/pkg/aggregator/transformers/rename/config.go @@ -32,8 +32,8 @@ type Rename struct { // Config stores the configuration of the rename transformer. type Config struct { - // Kparams is the list of parameters that will be renamed. - Kparams []Rename + // Params is the list of parameters that will be renamed. + Params []Rename // Enabled indicates whether this transformer is enabled. Enabled bool } diff --git a/pkg/aggregator/transformers/rename/rename.go b/pkg/aggregator/transformers/rename/rename.go index 7f89cb977..4ac17cc6b 100644 --- a/pkg/aggregator/transformers/rename/rename.go +++ b/pkg/aggregator/transformers/rename/rename.go @@ -20,10 +20,10 @@ package rename import ( "github.com/rabbitstack/fibratus/pkg/aggregator/transformers" - "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/event" ) -// rename as it name implies, it renames a sequence of kparams to their new names. +// rename as it name implies, it renames a sequence of params to their new names. type rename struct { c Config } @@ -40,15 +40,15 @@ func initRenameTransformer(config transformers.Config) (transformers.Transformer return &rename{c: cfg}, nil } -func (r rename) Transform(kevt *kevent.Kevent) error { - for _, par := range r.c.Kparams { - kpar, ok := kevt.Kparams[par.Old] +func (r rename) Transform(evt *event.Event) error { + for _, p := range r.c.Params { + par, ok := evt.Params[p.Old] if !ok { continue } - kevt.Kparams.Remove(par.Old) - kpar.Name = par.New - kevt.Kparams[par.New] = kpar + evt.Params.Remove(p.Old) + par.Name = p.New + evt.Params[p.New] = par } return nil } diff --git a/pkg/aggregator/transformers/rename/rename_test.go b/pkg/aggregator/transformers/rename/rename_test.go index e3e7ed736..8a90259f4 100644 --- a/pkg/aggregator/transformers/rename/rename_test.go +++ b/pkg/aggregator/transformers/rename/rename_test.go @@ -20,9 +20,8 @@ package rename import ( "github.com/rabbitstack/fibratus/pkg/aggregator/transformers" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "net" @@ -30,26 +29,26 @@ import ( ) func TestTransform(t *testing.T) { - kevt := &kevent.Kevent{ - Type: ktypes.SendTCPv4, + evt := &event.Event{ + Type: event.SendTCPv4, Tid: 2484, PID: 859, - Kparams: kevent.Kparams{ - kparams.NetDport: {Name: kparams.NetDport, Type: kparams.Uint16, Value: uint16(443)}, - kparams.NetSport: {Name: kparams.NetSport, Type: kparams.Uint16, Value: uint16(43123)}, - kparams.NetSIP: {Name: kparams.NetSIP, Type: kparams.IPv4, Value: net.ParseIP("127.0.0.1")}, - kparams.NetDIP: {Name: kparams.NetDIP, Type: kparams.IPv4, Value: net.ParseIP("216.58.201.174")}, + Params: event.Params{ + params.NetDport: {Name: params.NetDport, Type: params.Uint16, Value: uint16(443)}, + params.NetSport: {Name: params.NetSport, Type: params.Uint16, Value: uint16(43123)}, + params.NetSIP: {Name: params.NetSIP, Type: params.IPv4, Value: net.ParseIP("127.0.0.1")}, + params.NetDIP: {Name: params.NetDIP, Type: params.IPv4, Value: net.ParseIP("216.58.201.174")}, }, - Metadata: make(map[kevent.MetadataKey]any), + Metadata: make(map[event.MetadataKey]any), } - transf, err := transformers.Load(transformers.Config{Type: transformers.Rename, Transformer: Config{Kparams: []Rename{{Old: "dport", New: "dstport"}, {Old: "sip", New: "srcip"}}}}) + transf, err := transformers.Load(transformers.Config{Type: transformers.Rename, Transformer: Config{Params: []Rename{{Old: "dport", New: "dstport"}, {Old: "sip", New: "srcip"}}}}) require.NoError(t, err) - require.NoError(t, transf.Transform(kevt)) + require.NoError(t, transf.Transform(evt)) - assert.True(t, kevt.Kparams.Contains("dstport")) - assert.False(t, kevt.Kparams.Contains("dport")) - assert.True(t, kevt.Kparams.Contains("srcip")) - assert.False(t, kevt.Kparams.Contains("sip")) + assert.True(t, evt.Params.Contains("dstport")) + assert.False(t, evt.Params.Contains("dport")) + assert.True(t, evt.Params.Contains("srcip")) + assert.False(t, evt.Params.Contains("sip")) } diff --git a/pkg/aggregator/transformers/replace/config.go b/pkg/aggregator/transformers/replace/config.go index ea8c65cfd..a96f0e64a 100644 --- a/pkg/aggregator/transformers/replace/config.go +++ b/pkg/aggregator/transformers/replace/config.go @@ -26,17 +26,17 @@ const ( // Config stores the configuration for the replace transformer type Config struct { - // Replacements describes a list of replacements that are applied on the kparam. + // Replacements describes a list of replacements that are applied on the Param. Replacements []Replacement `mapstructure:"replacements"` // Enabled indicates whether this transformer is enabled Enabled bool `mapstructure:"enabled"` } -// Replacement defines the string replacement config for a specific kparam. +// Replacement defines the string replacement config for a specific Param. type Replacement struct { - Kpar string `mapstructure:"kparam"` - Old string `mapstructure:"old"` - New string `mapstructure:"new"` + Param string `mapstructure:"param"` + Old string `mapstructure:"old"` + New string `mapstructure:"new"` } // AddFlags registers persistent flags. diff --git a/pkg/aggregator/transformers/replace/replace.go b/pkg/aggregator/transformers/replace/replace.go index 2a449ff33..4889b331a 100644 --- a/pkg/aggregator/transformers/replace/replace.go +++ b/pkg/aggregator/transformers/replace/replace.go @@ -21,13 +21,13 @@ package replace import ( "expvar" "github.com/rabbitstack/fibratus/pkg/aggregator/transformers" - "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/event" "strings" ) var replaceCount = expvar.NewInt("transformers.replaced.params") -// replace applies string substitutions in kpar values. +// replace applies string substitutions in par values. type replace struct { c Config } @@ -44,17 +44,17 @@ func initReplaceTransformer(config transformers.Config) (transformers.Transforme return &replace{c: cfg}, nil } -func (r replace) Transform(kevt *kevent.Kevent) error { +func (r replace) Transform(evt *event.Event) error { for _, repl := range r.c.Replacements { - kpar := kevt.Kparams.Find(repl.Kpar) - if kpar == nil { + par := evt.Params.Find(repl.Param) + if par == nil { continue } - _, ok := kpar.Value.(string) + _, ok := par.Value.(string) if !ok { continue } - kpar.Value = strings.ReplaceAll(kevt.GetParamAsString(kpar.Name), repl.Old, repl.New) + par.Value = strings.ReplaceAll(evt.GetParamAsString(par.Name), repl.Old, repl.New) replaceCount.Add(1) } return nil diff --git a/pkg/aggregator/transformers/replace/replace_test.go b/pkg/aggregator/transformers/replace/replace_test.go index d2ed0dc45..8a48c99e5 100644 --- a/pkg/aggregator/transformers/replace/replace_test.go +++ b/pkg/aggregator/transformers/replace/replace_test.go @@ -20,31 +20,30 @@ package replace import ( "github.com/rabbitstack/fibratus/pkg/aggregator/transformers" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "testing" ) func TestTransform(t *testing.T) { - kevt := &kevent.Kevent{ - Type: ktypes.RegCreateKey, + evt := &event.Event{ + Type: event.RegCreateKey, Tid: 2484, PID: 859, - Kparams: kevent.Kparams{ - kparams.RegPath: {Name: kparams.RegPath, Type: kparams.UnicodeString, Value: `HKEY_LOCAL_MACHINE\SYSTEM\Setup\Pid`}, - kparams.RegKeyHandle: {Name: kparams.RegKeyHandle, Type: kparams.Address, Value: uint64(18446666033449935464)}, + Params: event.Params{ + params.RegPath: {Name: params.RegPath, Type: params.UnicodeString, Value: `HKEY_LOCAL_MACHINE\SYSTEM\Setup\Pid`}, + params.RegKeyHandle: {Name: params.RegKeyHandle, Type: params.Address, Value: uint64(18446666033449935464)}, }, } - transf, err := transformers.Load(transformers.Config{Type: transformers.Replace, Transformer: Config{Replacements: []Replacement{{Kpar: "key_path", Old: "HKEY_LOCAL_MACHINE", New: "HKLM"}}}}) + transf, err := transformers.Load(transformers.Config{Type: transformers.Replace, Transformer: Config{Replacements: []Replacement{{Param: "key_path", Old: "HKEY_LOCAL_MACHINE", New: "HKLM"}}}}) require.NoError(t, err) - require.NoError(t, transf.Transform(kevt)) + require.NoError(t, transf.Transform(evt)) - keyName, _ := kevt.Kparams.GetString(kparams.RegPath) + keyName, _ := evt.Params.GetString(params.RegPath) assert.Equal(t, `HKLM\SYSTEM\Setup\Pid`, keyName) } diff --git a/pkg/aggregator/transformers/tags/tags.go b/pkg/aggregator/transformers/tags/tags.go index d1909f5e1..e479dd748 100644 --- a/pkg/aggregator/transformers/tags/tags.go +++ b/pkg/aggregator/transformers/tags/tags.go @@ -23,7 +23,7 @@ import ( "strings" "github.com/rabbitstack/fibratus/pkg/aggregator/transformers" - "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/event" ) // tags transformer appends tags to the event's metadata. It is capable of adding literal values as well as @@ -61,9 +61,9 @@ func initTagsTransformer(config transformers.Config) (transformers.Transformer, return &tags{tags: ktags}, nil } -func (t tags) Transform(kevt *kevent.Kevent) error { +func (t tags) Transform(evt *event.Event) error { for k, v := range t.tags { - kevt.AddMeta(kevent.MetadataKey(k), v) + evt.AddMeta(event.MetadataKey(k), v) } return nil } diff --git a/pkg/aggregator/transformers/tags/tags_test.go b/pkg/aggregator/transformers/tags/tags_test.go index 8651a9371..4d3a84cf4 100644 --- a/pkg/aggregator/transformers/tags/tags_test.go +++ b/pkg/aggregator/transformers/tags/tags_test.go @@ -24,35 +24,34 @@ import ( "testing" "github.com/rabbitstack/fibratus/pkg/aggregator/transformers" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestTransform(t *testing.T) { - kevt := &kevent.Kevent{ - Type: ktypes.SendTCPv4, + evt := &event.Event{ + Type: event.SendTCPv4, Tid: 2484, PID: 859, - Kparams: kevent.Kparams{ - kparams.NetDport: {Name: kparams.NetDport, Type: kparams.Uint16, Value: uint16(443)}, - kparams.NetSport: {Name: kparams.NetSport, Type: kparams.Uint16, Value: uint16(43123)}, - kparams.NetSIP: {Name: kparams.NetSIP, Type: kparams.IPv4, Value: net.ParseIP("127.0.0.1")}, - kparams.NetDIP: {Name: kparams.NetDIP, Type: kparams.IPv4, Value: net.ParseIP("216.58.201.174")}, + Params: event.Params{ + params.NetDport: {Name: params.NetDport, Type: params.Uint16, Value: uint16(443)}, + params.NetSport: {Name: params.NetSport, Type: params.Uint16, Value: uint16(43123)}, + params.NetSIP: {Name: params.NetSIP, Type: params.IPv4, Value: net.ParseIP("127.0.0.1")}, + params.NetDIP: {Name: params.NetDIP, Type: params.IPv4, Value: net.ParseIP("216.58.201.174")}, }, - Metadata: make(map[kevent.MetadataKey]any), + Metadata: make(map[event.MetadataKey]any), } require.NoError(t, os.Setenv("NODENAME", "archbunny")) transf, err := transformers.Load(transformers.Config{Type: transformers.Tags, Transformer: Config{Tags: []Tag{{Key: "env", Value: "staging"}, {Key: "zone", Value: "dmz"}, {Key: "node", Value: "%NODENAME%"}}}}) require.NoError(t, err) - require.NoError(t, transf.Transform(kevt)) + require.NoError(t, transf.Transform(evt)) - require.Len(t, kevt.Metadata, 3) + require.Len(t, evt.Metadata, 3) - assert.Equal(t, "staging", kevt.Metadata["env"]) - assert.Equal(t, "dmz", kevt.Metadata["zone"]) - assert.Equal(t, "archbunny", kevt.Metadata["node"]) + assert.Equal(t, "staging", evt.Metadata["env"]) + assert.Equal(t, "dmz", evt.Metadata["zone"]) + assert.Equal(t, "archbunny", evt.Metadata["node"]) } diff --git a/pkg/aggregator/transformers/transformer.go b/pkg/aggregator/transformers/transformer.go index 6242f2855..e08841637 100644 --- a/pkg/aggregator/transformers/transformer.go +++ b/pkg/aggregator/transformers/transformer.go @@ -20,7 +20,7 @@ package transformers import ( "fmt" - "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/event" ) var transformers = map[Type]Factory{} @@ -34,11 +34,11 @@ type Type uint8 const ( // Remove represents the remove transformer type. This transformer deletes the given list of parameters from the event. Remove Type = iota - // Rename represents the rename transformer type. It renames a sequence of kparam from old to new names. + // Rename represents the rename transformer type. It renames a sequence of Param from old to new names. Rename - // Replace represents the replace tranformer type. It applies string replacements on specific kparams. + // Replace represents the replace transformer type. It applies string replacements on specific params. Replace - // Trim represents the trim transformer type that that removes suffix/prefix from string kparams. + // Trim represents the trim transformer type that that removes suffix/prefix from string params. Trim // Tags represents the tags transformer type. This transformer appends tags to the event's metadata. Tags @@ -95,5 +95,5 @@ func Load(config Config) (Transformer, error) { // Transformer is the minimal interface all transformers have to satisfy. type Transformer interface { - Transform(*kevent.Kevent) error + Transform(*event.Event) error } diff --git a/pkg/aggregator/transformers/trim/config.go b/pkg/aggregator/transformers/trim/config.go index 60f56c6a4..de05166d4 100644 --- a/pkg/aggregator/transformers/trim/config.go +++ b/pkg/aggregator/transformers/trim/config.go @@ -26,15 +26,15 @@ const ( // Trim defines the trim configuration for a single event parameter. type Trim struct { - Name string `mapstructure:"kparam"` + Name string `mapstructure:"Param"` Trim string `mapstructure:"trim"` } // Config stores the configuration for the trim transformer. type Config struct { - // Prefixes contains the mapping between distinct kparam names and the prefixes that will get trimmed from their values. + // Prefixes contains the mapping between distinct Param names and the prefixes that will get trimmed from their values. Prefixes []Trim `mapstructure:"prefixes"` - // Suffixes contains the mapping between distinct kparam names and the suffixes that will get trimmed from their values. + // Suffixes contains the mapping between distinct Param names and the suffixes that will get trimmed from their values. Suffixes []Trim `mapstructure:"suffixes"` // Enabled determines whether trim transformer is enabled or disabled. Enabled bool `mapstructure:"enabled"` diff --git a/pkg/aggregator/transformers/trim/trim.go b/pkg/aggregator/transformers/trim/trim.go index abce5754d..bbcaa99a7 100644 --- a/pkg/aggregator/transformers/trim/trim.go +++ b/pkg/aggregator/transformers/trim/trim.go @@ -20,11 +20,11 @@ package trim import ( "github.com/rabbitstack/fibratus/pkg/aggregator/transformers" - "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/event" "strings" ) -// trim transformer trims suffixes/prefixes from kpar values. +// trim transformer trims suffixes/prefixes from par values. type trim struct { c Config } @@ -41,29 +41,29 @@ func initTrimTransformer(config transformers.Config) (transformers.Transformer, return &trim{c: cfg}, nil } -func (r trim) Transform(kevt *kevent.Kevent) error { - for _, kpar := range kevt.Kparams { +func (r trim) Transform(evt *event.Event) error { + for _, par := range evt.Params { // trim prefixes - for _, par := range r.c.Prefixes { - if kpar.Name != par.Name { + for _, pre := range r.c.Prefixes { + if par.Name != pre.Name { continue } - _, ok := kpar.Value.(string) + _, ok := par.Value.(string) if !ok { continue } - kpar.Value = strings.TrimPrefix(kevt.GetParamAsString(kpar.Name), par.Trim) + par.Value = strings.TrimPrefix(evt.GetParamAsString(par.Name), pre.Trim) } // trim suffixes - for _, par := range r.c.Suffixes { - if kpar.Name != par.Name { + for _, suf := range r.c.Suffixes { + if par.Name != suf.Name { continue } - _, ok := kpar.Value.(string) + _, ok := par.Value.(string) if !ok { continue } - kpar.Value = strings.TrimSuffix(kevt.GetParamAsString(kpar.Name), par.Trim) + par.Value = strings.TrimSuffix(evt.GetParamAsString(par.Name), suf.Trim) } } return nil diff --git a/pkg/aggregator/transformers/trim/trim_test.go b/pkg/aggregator/transformers/trim/trim_test.go index dc8e78b00..43270822b 100644 --- a/pkg/aggregator/transformers/trim/trim_test.go +++ b/pkg/aggregator/transformers/trim/trim_test.go @@ -20,9 +20,8 @@ package trim import ( "github.com/rabbitstack/fibratus/pkg/aggregator/transformers" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "testing" @@ -30,36 +29,36 @@ import ( ) func TestTransform(t *testing.T) { - kevt := &kevent.Kevent{ - Type: ktypes.CreateFile, + evt := &event.Event{ + Type: event.CreateFile, Tid: 2484, PID: 859, CPU: 1, Seq: 2, Name: "CreateFile", - Category: ktypes.File, + Category: event.File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", - Kparams: kevent.Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "overwriteif"}, - kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, - kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, - kparams.KstackLimit: {Name: kparams.KstackLimit, Type: kparams.Address, Value: uint64(18884888488889)}, - kparams.StartTime: {Name: kparams.StartTime, Type: kparams.Time, Value: time.Now()}, - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(1204)}, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.AnsiString, Value: "overwriteif"}, + params.BasePrio: {Name: params.BasePrio, Type: params.Int8, Value: int8(2)}, + params.PagePrio: {Name: params.PagePrio, Type: params.Uint8, Value: uint8(2)}, + params.KstackLimit: {Name: params.KstackLimit, Type: params.Address, Value: uint64(18884888488889)}, + params.StartTime: {Name: params.StartTime, Type: params.Time, Value: time.Now()}, + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1204)}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barz"}, } transf, err := transformers.Load(transformers.Config{Type: transformers.Trim, Transformer: Config{Prefixes: []Trim{{Name: "file_path", Trim: "\\Device"}}, Suffixes: []Trim{{Name: "create_disposition", Trim: "if"}}}}) require.NoError(t, err) - require.NoError(t, transf.Transform(kevt)) - filename, _ := kevt.Kparams.GetString(kparams.FilePath) - dispo, _ := kevt.Kparams.GetString(kparams.FileOperation) + require.NoError(t, transf.Transform(evt)) + filename, _ := evt.Params.GetString(params.FilePath) + dispo, _ := evt.Params.GetString(params.FileOperation) assert.Equal(t, "\\HarddiskVolume2\\Windows\\system32\\user32.dll", filename) assert.Equal(t, "overwrite", dispo) diff --git a/pkg/aggregator/worker_test.go b/pkg/aggregator/worker_test.go index 20eb708e5..66d493619 100644 --- a/pkg/aggregator/worker_test.go +++ b/pkg/aggregator/worker_test.go @@ -19,7 +19,7 @@ package aggregator import ( - "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/stretchr/testify/assert" "net/http" "net/http/httptest" @@ -51,7 +51,7 @@ func (c *httpClient) Connect() error { func (c *httpClient) Close() error { return nil } -func (c *httpClient) Publish(b *kevent.Batch) error { +func (c *httpClient) Publish(b *event.Batch) error { //nolint:noctx res, err := http.Post(c.url+"/publish", "application/json", nil) if err != nil { @@ -68,9 +68,9 @@ func (c *httpClient) Publish(b *kevent.Batch) error { } func TestRunWorker(t *testing.T) { - q := make(chan *kevent.Batch, 2) - q <- &kevent.Batch{} - q <- &kevent.Batch{} + q := make(chan *event.Batch, 2) + q <- &event.Batch{} + q <- &event.Batch{} mux := http.NewServeMux() mux.HandleFunc("/publish", func(w http.ResponseWriter, r *http.Request) { @@ -91,9 +91,9 @@ func TestRunWorker(t *testing.T) { } func TestConnectClientBackoff(t *testing.T) { - q := make(chan *kevent.Batch, 2) - q <- &kevent.Batch{} - q <- &kevent.Batch{} + q := make(chan *event.Batch, 2) + q <- &event.Batch{} + q <- &event.Batch{} fail := true diff --git a/pkg/alertsender/alert.go b/pkg/alertsender/alert.go index 6f07284d3..9c3243717 100644 --- a/pkg/alertsender/alert.go +++ b/pkg/alertsender/alert.go @@ -21,7 +21,7 @@ package alertsender import ( "bytes" "fmt" - "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/yuin/goldmark" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/renderer/html" @@ -95,7 +95,7 @@ type Alert struct { // Severity determines the severity of this alert. Severity Severity // Events contains a list of events that trigger the alert. - Events []*kevent.Kevent + Events []*event.Event } // String returns the alert string representation. If verbose @@ -146,6 +146,6 @@ func NewAlert(title, text string, tags []string, severity Severity) Alert { } // NewAlertWithEvents builds a new alert with associated events. -func NewAlertWithEvents(title, text string, tags []string, severity Severity, evts []*kevent.Kevent) Alert { +func NewAlertWithEvents(title, text string, tags []string, severity Severity, evts []*event.Event) Alert { return Alert{Title: title, Text: text, Tags: tags, Severity: severity, Events: evts} } diff --git a/pkg/alertsender/alert_test.go b/pkg/alertsender/alert_test.go index 21d04b16e..697596269 100644 --- a/pkg/alertsender/alert_test.go +++ b/pkg/alertsender/alert_test.go @@ -19,9 +19,8 @@ package alertsender import ( - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" "github.com/stretchr/testify/require" "testing" @@ -44,12 +43,12 @@ func TestAlertString(t *testing.T) { "Credential discovery via VaultCmd.exe\n\nSuspicious vault enumeration via VaultCmd tool", }, { - NewAlertWithEvents("Credential discovery via VaultCmd.exe", "Suspicious vault enumeration via VaultCmd tool", nil, Normal, []*kevent.Kevent{{ - Type: ktypes.CreateProcess, - Category: ktypes.Process, - Kparams: kevent.Kparams{ - kparams.Cmdline: {Name: kparams.Cmdline, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\svchost-fake.exe -k RPCSS"}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.AnsiString, Value: "svchost-fake.exe"}}, + NewAlertWithEvents("Credential discovery via VaultCmd.exe", "Suspicious vault enumeration via VaultCmd tool", nil, Normal, []*event.Event{{ + Type: event.CreateProcess, + Category: event.Process, + Params: event.Params{ + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\svchost-fake.exe -k RPCSS"}, + params.ProcessName: {Name: params.ProcessName, Type: params.AnsiString, Value: "svchost-fake.exe"}}, Name: "CreateProcess", PID: 1023, PS: &pstypes.PS{ @@ -65,12 +64,12 @@ func TestAlertString(t *testing.T) { "Credential discovery via VaultCmd.exe\n\nSuspicious vault enumeration via VaultCmd tool\n\nSeverity: low\n\nSystem event involved in this alert:\n\n\tEvent #1:\n\n\t\tSeq: 0\n\t\tPid: 1023\n\t\tTid: 0\n\t\tName: CreateProcess\n\t\tCategory: process\n\t\tHost: \n\t\tTimestamp: 0001-01-01 00:00:00 +0000 UTC\n\t\tParameters: cmdline➜ C:\\Windows\\system32\\svchost-fake.exe -k RPCSS, name➜ svchost-fake.exe\n \n\t\tPid: 0\n\t\tPpid: 345\n\t\tName: svchost.exe\n\t\tCmdline: C:\\Windows\\System32\\svchost.exe\n\t\tExe: \n\t\tCwd: \n\t\tSID: S-1-5-18\n\t\tUsername: SYSTEM\n\t\tDomain: NT AUTHORITY\n\t\tArgs: []\n\t\tSession ID: 0\n\t\tAncestors: \n\t\n", }, { - NewAlertWithEvents("Credential discovery via VaultCmd.exe", "", nil, Normal, []*kevent.Kevent{{ - Type: ktypes.CreateProcess, - Category: ktypes.Process, - Kparams: kevent.Kparams{ - kparams.Cmdline: {Name: kparams.Cmdline, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\svchost-fake.exe -k RPCSS"}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.AnsiString, Value: "svchost-fake.exe"}}, + NewAlertWithEvents("Credential discovery via VaultCmd.exe", "", nil, Normal, []*event.Event{{ + Type: event.CreateProcess, + Category: event.Process, + Params: event.Params{ + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\svchost-fake.exe -k RPCSS"}, + params.ProcessName: {Name: params.ProcessName, Type: params.AnsiString, Value: "svchost-fake.exe"}}, Name: "CreateProcess", PID: 1023, PS: &pstypes.PS{ diff --git a/pkg/alertsender/eventlog/eventlog_test.go b/pkg/alertsender/eventlog/eventlog_test.go index 894073910..3567dd8f6 100644 --- a/pkg/alertsender/eventlog/eventlog_test.go +++ b/pkg/alertsender/eventlog/eventlog_test.go @@ -20,10 +20,9 @@ package eventlog import ( "github.com/rabbitstack/fibratus/pkg/alertsender" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" htypes "github.com/rabbitstack/fibratus/pkg/handle/types" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" pex "github.com/rabbitstack/fibratus/pkg/pe" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" "github.com/rabbitstack/fibratus/pkg/util/va" @@ -39,25 +38,25 @@ func TestEventlogSender(t *testing.T) { require.NotNil(t, s) require.NoError(t, s.Send(alertsender.Alert{ - Events: []*kevent.Kevent{ + Events: []*event.Event{ { - Type: ktypes.CreateFile, + Type: event.CreateFile, Tid: 2484, PID: 859, CPU: 1, Seq: 2, Name: "CreateFile", Timestamp: time.Now(), - Category: ktypes.File, + Category: event.File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", - Kparams: kevent.Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.Enum, Value: uint32(1)}, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.Enum, Value: uint32(1)}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, PS: &pstypes.PS{ PID: 2436, Ppid: 6304, @@ -136,22 +135,22 @@ func TestEventlogSender(t *testing.T) { }, }, { - Type: ktypes.CreateProcess, + Type: event.CreateProcess, Tid: 2184, PID: 1022, CPU: 2, Seq: 3, Name: "CreateProcess", Timestamp: time.Now(), - Category: ktypes.File, + Category: event.File, Host: "archrabbit", Description: "Creates a new process", - Kparams: kevent.Kparams{ - kparams.Cmdline: {Name: kparams.Cmdline, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\svchost.exe -k RPCSS"}, - kparams.Exe: {Name: kparams.Exe, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\svchost.exe"}, - kparams.UserSID: {Name: kparams.UserSID, Type: kparams.UnicodeString, Value: "admin\\SYSTEM"}, + Params: event.Params{ + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\svchost.exe -k RPCSS"}, + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\svchost.exe"}, + params.UserSID: {Name: params.UserSID, Type: params.UnicodeString, Value: "admin\\SYSTEM"}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, PS: &pstypes.PS{ PID: 2436, Ppid: 6304, diff --git a/pkg/alertsender/mail/renderer_test.go b/pkg/alertsender/mail/renderer_test.go index 32033f009..751244347 100644 --- a/pkg/alertsender/mail/renderer_test.go +++ b/pkg/alertsender/mail/renderer_test.go @@ -21,10 +21,9 @@ package mail import ( "github.com/antchfx/htmlquery" "github.com/rabbitstack/fibratus/pkg/alertsender" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" htypes "github.com/rabbitstack/fibratus/pkg/handle/types" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" pex "github.com/rabbitstack/fibratus/pkg/pe" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" "github.com/rabbitstack/fibratus/pkg/util/va" @@ -50,25 +49,25 @@ func TestRenderHTMLTemplate(t *testing.T) { "subtechnique.name": "Windows Credential Manager", "subtechnique.ref": "https://attack.mitre.org/techniques/T1555/004/", }, - Events: []*kevent.Kevent{ + Events: []*event.Event{ { - Type: ktypes.CreateFile, + Type: event.CreateFile, Tid: 2484, PID: 859, CPU: 1, Seq: 2, Name: "CreateFile", Timestamp: time.Now(), - Category: ktypes.File, + Category: event.File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", - Kparams: kevent.Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.Enum, Value: uint32(1)}, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.Enum, Value: uint32(1)}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, PS: &pstypes.PS{ PID: 2436, Ppid: 6304, @@ -147,22 +146,22 @@ func TestRenderHTMLTemplate(t *testing.T) { }, }, { - Type: ktypes.CreateProcess, + Type: event.CreateProcess, Tid: 2184, PID: 1022, CPU: 2, Seq: 3, Name: "CreateProcess", Timestamp: time.Now(), - Category: ktypes.File, + Category: event.File, Host: "archrabbit", Description: "Creates a new process", - Kparams: kevent.Kparams{ - kparams.Cmdline: {Name: kparams.Cmdline, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\svchost.exe -k RPCSS"}, - kparams.Exe: {Name: kparams.Exe, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\svchost.exe"}, - kparams.UserSID: {Name: kparams.UserSID, Type: kparams.UnicodeString, Value: "admin\\SYSTEM"}, + Params: event.Params{ + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\svchost.exe -k RPCSS"}, + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\svchost.exe"}, + params.UserSID: {Name: params.UserSID, Type: params.UnicodeString, Value: "admin\\SYSTEM"}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, PS: &pstypes.PS{ PID: 2436, Ppid: 6304, diff --git a/pkg/alertsender/mail/template.go b/pkg/alertsender/mail/template.go index dd3163060..f8345d89f 100644 --- a/pkg/alertsender/mail/template.go +++ b/pkg/alertsender/mail/template.go @@ -132,7 +132,7 @@ var htmlTemplate = ` - {{- range $key, $par := .Kparams }} + {{- range $key, $par := .Params }}
{{ regexReplaceAll "_" $key " " | title }} diff --git a/pkg/alertsender/systray/systray.go b/pkg/alertsender/systray/systray.go index cab1bfd7c..f0353ba7d 100644 --- a/pkg/alertsender/systray/systray.go +++ b/pkg/alertsender/systray/systray.go @@ -25,7 +25,7 @@ import ( "github.com/Microsoft/go-winio" "github.com/cenkalti/backoff/v4" "github.com/rabbitstack/fibratus/pkg/alertsender" - "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/sys" log "github.com/sirupsen/logrus" "golang.org/x/sys/windows" @@ -198,7 +198,7 @@ func makeSender(config alertsender.Config) (alertsender.Sender, error) { func (s *systray) Send(alert alertsender.Alert) error { // remove all events to avoid decoding errors on systray server end - alert.Events = make([]*kevent.Kevent, 0) + alert.Events = make([]*event.Event, 0) return s.send(&Msg{Type: Balloon, Data: alert}) } diff --git a/pkg/kcap/_fixtures/cap.kcap b/pkg/cap/_fixtures/cap.cap similarity index 100% rename from pkg/kcap/_fixtures/cap.kcap rename to pkg/cap/_fixtures/cap.cap diff --git a/pkg/kcap/_fixtures/cap1.kcap b/pkg/cap/_fixtures/cap1.cap similarity index 100% rename from pkg/kcap/_fixtures/cap1.kcap rename to pkg/cap/_fixtures/cap1.cap diff --git a/pkg/kcap/_fixtures/cap2.kcap b/pkg/cap/_fixtures/cap2.cap similarity index 100% rename from pkg/kcap/_fixtures/cap2.kcap rename to pkg/cap/_fixtures/cap2.cap diff --git a/pkg/kcap/config.go b/pkg/cap/config.go similarity index 98% rename from pkg/kcap/config.go rename to pkg/cap/config.go index bf63f3655..f73c0a8d5 100644 --- a/pkg/kcap/config.go +++ b/pkg/cap/config.go @@ -16,7 +16,7 @@ * limitations under the License. */ -package kcap +package cap import "time" diff --git a/pkg/kcap/header.go b/pkg/cap/header.go similarity index 70% rename from pkg/kcap/header.go rename to pkg/cap/header.go index 169ff25de..c1720164c 100644 --- a/pkg/kcap/header.go +++ b/pkg/cap/header.go @@ -1,5 +1,5 @@ -//go:build kcap -// +build kcap +//go:build cap +// +build cap /* * Copyright 2019-2020 by Nedim Sabic Sabic @@ -19,24 +19,24 @@ * limitations under the License. */ -package kcap +package cap import ( - "github.com/rabbitstack/fibratus/pkg/kcap/section" - kcapver "github.com/rabbitstack/fibratus/pkg/kcap/version" + "github.com/rabbitstack/fibratus/pkg/cap/section" + kcapver "github.com/rabbitstack/fibratus/pkg/cap/version" ) -// magic has two purposes. It is used to identify kcap files. The magic is stored within the first 8 bytes of the file. +// magic has two purposes. It is used to identify cap files. The magic is stored within the first 8 bytes of the file. // The reader ensures the magic number matches this constant. Besides identifying the capture file, it serves as an -// input for initializing the byte order on the machine where kcap file is read. This implies capture can be taken on a +// input for initializing the byte order on the machine where cap file is read. This implies capture can be taken on a // machine with different endianness from the one capture is replayed. const magic = 0x6669627261747573 -// major represents the major digit of the kcap file format. Incrementing the major digit makes older kcap readers not +// major represents the major digit of the cap file format. Incrementing the major digit makes older cap readers not // capable to replay the capture file const major = uint8(2) -// minor represents the minor digit of the kcap file format +// minor represents the minor digit of the cap file format const minor = uint8(0) // flags denotes extra flags for the purpose of the header description diff --git a/pkg/kcap/reader.go b/pkg/cap/reader.go similarity index 59% rename from pkg/kcap/reader.go rename to pkg/cap/reader.go index f4d0c513f..9f52ec153 100644 --- a/pkg/kcap/reader.go +++ b/pkg/cap/reader.go @@ -1,5 +1,5 @@ -//go:build kcap -// +build kcap +//go:build cap +// +build cap /* * Copyright 2020-2021 by Nedim Sabic Sabic @@ -19,30 +19,30 @@ * limitations under the License. */ -package kcap +package cap import ( "errors" "expvar" "fmt" - "github.com/rabbitstack/fibratus/pkg/kcap/section" + "github.com/rabbitstack/fibratus/pkg/cap/section" ) var ( - // ErrKcapMagicMismatch signals invalid kcap binary format - ErrKcapMagicMismatch = errors.New("invalid kcap file magic number") - // ErrMajorVer signals incompatible kcap version + // ErrCapMagicMismatch signals invalid capture binary format + ErrCapMagicMismatch = errors.New("invalid capture file magic number") + // ErrMajorVer signals incompatible cap version ErrMajorVer = func(maj, min byte) error { - return fmt.Errorf("incompatible kcap version format. Required version %d.%d but %d.%d found", major, minor, maj, min) + return fmt.Errorf("incompatible cap version format. Required version %d.%d but %d.%d found", major, minor, maj, min) } // ErrReadVersion is thrown when version digit errors occur ErrReadVersion = func(s string, err error) error { return fmt.Errorf("couldn't read %s version digit: %v", s, err) } // ErrReadSection is thrown when section read errors occur ErrReadSection = func(s section.Type, err error) error { return fmt.Errorf("couldn't read %s section: %v", s, err) } - kcapReadKevents = expvar.NewInt("kcap.read.kevents") - kcapReadBytes = expvar.NewInt("kcap.read.bytes") - kcapKeventUnmarshalErrors = expvar.NewInt("kcap.kevent.unmarshal.errors") - kcapHandleUnmarshalErrors = expvar.NewInt("kcap.reader.handle.unmarshal.errors") - kcapDroppedByFilter = expvar.NewInt("kcap.reader.dropped.by.filter") + capReadKevents = expvar.NewInt("cap.read.events") + capReadBytes = expvar.NewInt("cap.read.bytes") + capKeventUnmarshalErrors = expvar.NewInt("cap.event.unmarshal.errors") + capHandleUnmarshalErrors = expvar.NewInt("cap.reader.handle.unmarshal.errors") + capDroppedByFilter = expvar.NewInt("cap.reader.dropped.by.filter") ) diff --git a/pkg/kcap/reader_unsupported.go b/pkg/cap/reader_unsupported.go similarity index 84% rename from pkg/kcap/reader_unsupported.go rename to pkg/cap/reader_unsupported.go index c6ed5f86d..b344bd68f 100644 --- a/pkg/kcap/reader_unsupported.go +++ b/pkg/cap/reader_unsupported.go @@ -1,5 +1,5 @@ -//go:build !kcap -// +build !kcap +//go:build !cap +// +build !cap /* * Copyright 2019-2020 by Nedim Sabic Sabic @@ -19,14 +19,14 @@ * limitations under the License. */ -package kcap +package cap import ( "github.com/rabbitstack/fibratus/pkg/config" - kerrors "github.com/rabbitstack/fibratus/pkg/errors" + "github.com/rabbitstack/fibratus/pkg/errors" ) // NewReader returns unsupported reader. func NewReader(filename string, config *config.Config) (Reader, error) { - return nil, kerrors.ErrFeatureUnsupported("kcap") + return nil, errors.ErrFeatureUnsupported("cap") } diff --git a/pkg/kcap/reader_windows.go b/pkg/cap/reader_windows.go similarity index 69% rename from pkg/kcap/reader_windows.go rename to pkg/cap/reader_windows.go index 049f2cc63..be00cb3f7 100644 --- a/pkg/kcap/reader_windows.go +++ b/pkg/cap/reader_windows.go @@ -1,5 +1,5 @@ -//go:build kcap -// +build kcap +//go:build cap +// +build cap /* * Copyright 2019-2020 by Nedim Sabic Sabic @@ -19,19 +19,18 @@ * limitations under the License. */ -package kcap +package cap import ( "context" "fmt" + "github.com/rabbitstack/fibratus/pkg/cap/section" "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/filter" "github.com/rabbitstack/fibratus/pkg/handle" htypes "github.com/rabbitstack/fibratus/pkg/handle/types" - "github.com/rabbitstack/fibratus/pkg/kcap/section" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/rabbitstack/fibratus/pkg/ps" "github.com/rabbitstack/fibratus/pkg/util/bytes" log "github.com/sirupsen/logrus" @@ -52,10 +51,10 @@ type reader struct { mu sync.Mutex // guards the underlying zstd byte buffer } -// NewReader builds a new instance of the kcap reader. +// NewReader builds a new instance of the cap reader. func NewReader(filename string, config *config.Config) (Reader, error) { if filepath.Ext(filename) == "" { - filename += ".kcap" + filename += ".cap" } f, err := os.Open(filename) if err != nil { @@ -68,7 +67,7 @@ func NewReader(filename string, config *config.Config) (Reader, error) { mag := make([]byte, 8) if n, err := zr.Read(mag); err != nil || n != 8 { - return nil, ErrKcapMagicMismatch + return nil, ErrCapMagicMismatch } bytes.InitNativeEndian(mag) // from now on all byte reads will use the endianness of the magic number. @@ -76,7 +75,7 @@ func NewReader(filename string, config *config.Config) (Reader, error) { // on a machine with a different endianness from the machine where // actual capture is being read. if bytes.ReadUint64(mag) != magic { - return nil, ErrKcapMagicMismatch + return nil, ErrCapMagicMismatch } maj := make([]byte, 1) @@ -95,7 +94,7 @@ func NewReader(filename string, config *config.Config) (Reader, error) { // read the flags bit vector but do nothing with it at the moment flags := make([]byte, 8) if n, err := zr.Read(flags); err != nil || n != 8 { - return nil, fmt.Errorf("fail to read kcap flags: %v", err) + return nil, fmt.Errorf("fail to read cap flags: %v", err) } return &reader{f: f, zr: zr, config: config}, nil @@ -103,9 +102,9 @@ func NewReader(filename string, config *config.Config) (Reader, error) { func (r *reader) SetFilter(f filter.Filter) { r.filter = f } -func (r *reader) Read(ctx context.Context) (chan *kevent.Kevent, chan error) { +func (r *reader) Read(ctx context.Context) (chan *event.Event, chan error) { errsc := make(chan error, 100) - keventsc := make(chan *kevent.Kevent, 2000) + eventsc := make(chan *event.Event, 2000) go func() { r.mu.Lock() defer r.mu.Unlock() @@ -134,23 +133,23 @@ func (r *reader) Read(ctx context.Context) (chan *kevent.Kevent, chan error) { } break } - kevt, err := kevent.NewFromKcap(buf, sec.Version()) + evt, err := event.NewFromCapture(buf, sec.Version()) if err != nil { - errsc <- fmt.Errorf("fail to unmarshal kevent: %v", err) - kcapKeventUnmarshalErrors.Add(1) + errsc <- fmt.Errorf("fail to unmarshal event: %v", err) + capKeventUnmarshalErrors.Add(1) continue } - kcapReadBytes.Add(int64(len(buf))) + capReadBytes.Add(int64(len(buf))) // update the state of the ps/handle snapshotters - if err := r.updateSnapshotters(kevt); err != nil { + if err := r.updateSnapshotters(evt); err != nil { log.Warn(err) } // push the event to the chanel - r.read(kevt, keventsc) + r.read(evt, eventsc) } }() - return keventsc, errsc + return eventsc, errsc } func (r *reader) Close() error { @@ -165,56 +164,56 @@ func (r *reader) Close() error { return nil } -func (r *reader) read(kevt *kevent.Kevent, keventsc chan *kevent.Kevent) { - if kevt.Type.OnlyState() { +func (r *reader) read(evt *event.Event, eventsc chan *event.Event) { + if evt.Type.OnlyState() { return } - if r.filter != nil && !r.filter.Run(kevt) { - kcapDroppedByFilter.Add(1) + if r.filter != nil && !r.filter.Run(evt) { + capDroppedByFilter.Add(1) return } - keventsc <- kevt - kcapReadKevents.Add(1) + eventsc <- evt + capReadKevents.Add(1) } -func (r *reader) updateSnapshotters(kevt *kevent.Kevent) error { - switch kevt.Type { - case ktypes.TerminateProcess: - if err := r.psnapshotter.Remove(kevt); err != nil { +func (r *reader) updateSnapshotters(evt *event.Event) error { + switch evt.Type { + case event.TerminateProcess: + if err := r.psnapshotter.Remove(evt); err != nil { return err } - case ktypes.TerminateThread: - pid := kevt.Kparams.MustGetPid() - tid := kevt.Kparams.MustGetTid() + case event.TerminateThread: + pid := evt.Params.MustGetPid() + tid := evt.Params.MustGetTid() if err := r.psnapshotter.RemoveThread(pid, tid); err != nil { return err } - case ktypes.UnloadImage: - pid := kevt.Kparams.MustGetPid() - addr := kevt.Kparams.TryGetAddress(kparams.ImageBase) + case event.UnloadImage: + pid := evt.Params.MustGetPid() + addr := evt.Params.TryGetAddress(params.ImageBase) if err := r.psnapshotter.RemoveModule(pid, addr); err != nil { return err } - case ktypes.CreateProcess, - ktypes.ProcessRundown, - ktypes.LoadImage, - ktypes.ImageRundown, - ktypes.CreateThread, - ktypes.ThreadRundown: - if err := r.psnapshotter.WriteFromKcap(kevt); err != nil { + case event.CreateProcess, + event.ProcessRundown, + event.LoadImage, + event.ImageRundown, + event.CreateThread, + event.ThreadRundown: + if err := r.psnapshotter.WriteFromCapture(evt); err != nil { return err } - case ktypes.CreateHandle: - if err := r.hsnapshotter.Write(kevt); err != nil { + case event.CreateHandle: + if err := r.hsnapshotter.Write(evt); err != nil { return err } - case ktypes.CloseHandle: - if err := r.hsnapshotter.Remove(kevt); err != nil { + case event.CloseHandle: + if err := r.hsnapshotter.Remove(evt); err != nil { return err } } - if kevt.PS == nil { - _, kevt.PS = r.psnapshotter.Find(kevt.PID) + if evt.PS == nil { + _, evt.PS = r.psnapshotter.Find(evt.PID) } return nil } @@ -224,7 +223,7 @@ func (r *reader) RecoverSnapshotters() (handle.Snapshotter, ps.Snapshotter, erro if err != nil { return nil, nil, err } - r.psnapshotter = ps.NewSnapshotterFromKcap(hsnap, r.config) + r.psnapshotter = ps.NewSnapshotterFromCapture(hsnap, r.config) return hsnap, r.psnapshotter, nil } @@ -250,7 +249,7 @@ func (r *reader) recoverHandleSnapshotter() (handle.Snapshotter, error) { var err error handles[i], err = htypes.NewFromKcap(b) if err != nil { - kcapHandleUnmarshalErrors.Add(1) + capHandleUnmarshalErrors.Add(1) } } r.hsnapshotter = handle.NewFromKcap(handles) diff --git a/pkg/kcap/reader_windows_test.go b/pkg/cap/reader_windows_test.go similarity index 72% rename from pkg/kcap/reader_windows_test.go rename to pkg/cap/reader_windows_test.go index 539188121..cc69b7739 100644 --- a/pkg/kcap/reader_windows_test.go +++ b/pkg/cap/reader_windows_test.go @@ -19,7 +19,7 @@ * limitations under the License. */ -package kcap +package cap import ( "context" @@ -30,13 +30,13 @@ import ( ) func TestReadIncompatibleFormat(t *testing.T) { - r, err := NewReader("_fixtures/cap1.kcap", &config.Config{}) + r, err := NewReader("_fixtures/cap1.cap", &config.Config{}) require.Nil(t, r) - require.EqualErrorf(t, err, fmt.Sprintf("incompatible kcap version format. Required version %d.%d but 1.0 found", major, minor), "incompatible kcap version format. Required version %d.%d but 1.0 found", major, minor) + require.EqualErrorf(t, err, fmt.Sprintf("incompatible cap version format. Required version %d.%d but 1.0 found", major, minor), "incompatible cap version format. Required version %d.%d but 1.0 found", major, minor) } func TestRead(t *testing.T) { - r, err := NewReader("_fixtures/cap2.kcap", &config.Config{}) + r, err := NewReader("_fixtures/cap2.cap", &config.Config{}) if err != nil { t.Fatal(err) } @@ -46,13 +46,13 @@ func TestRead(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) - kevtsc, errs := r.Read(ctx) + evtsc, errs := r.Read(ctx) i := 0 for { select { - case kevt := <-kevtsc: - require.NotNil(t, kevt) - require.True(t, kevt.Seq > 0) + case evt := <-evtsc: + require.NotNil(t, evt) + require.True(t, evt.Seq > 0) i++ if i == 90 { cancel() diff --git a/pkg/kcap/section/section.go b/pkg/cap/section/section.go similarity index 83% rename from pkg/kcap/section/section.go rename to pkg/cap/section/section.go index d602125be..ca64bca5b 100644 --- a/pkg/kcap/section/section.go +++ b/pkg/cap/section/section.go @@ -20,20 +20,20 @@ package section import ( "fmt" - kcapver "github.com/rabbitstack/fibratus/pkg/kcap/version" + capver "github.com/rabbitstack/fibratus/pkg/cap/version" "github.com/rabbitstack/fibratus/pkg/util/bytes" ) // Section represents the header describing the type, length and the version of each section. type Section [10]byte -// String returns the string representation of the kcap section. +// String returns the string representation of the cap section. func (s Section) String() string { return fmt.Sprintf("type: %s, version: %d, len: %d, size: %d", s.Type(), s.Version(), s.Len(), s.Size()) } // New builds a new section block with the specified type, version, optional length and size. -func New(typ Type, ver kcapver.Version, l, size uint32) Section { +func New(typ Type, ver capver.Version, l, size uint32) Section { var s Section s[0] = uint8(typ) s[1] = uint8(ver) @@ -52,8 +52,8 @@ func Read(b []byte) Section { // Type returns the type of this section. func (s Section) Type() Type { return Type(s[0]) } -// Version returns the version of the captured section block. -func (s Section) Version() kcapver.Version { return kcapver.Version(s[1]) } +// Version returns the version of the capture section block. +func (s Section) Version() capver.Version { return capver.Version(s[1]) } // Len returns the length of the section. func (s Section) Len() uint32 { return bytes.ReadUint32(s[2:6]) } diff --git a/pkg/kcap/section/section_windows.go b/pkg/cap/section/section_windows.go similarity index 89% rename from pkg/kcap/section/section_windows.go rename to pkg/cap/section/section_windows.go index ebcb4f2a2..1930cfab9 100644 --- a/pkg/kcap/section/section_windows.go +++ b/pkg/cap/section/section_windows.go @@ -18,7 +18,7 @@ package section -// Type describes the type of a section +// Type describes the type of the capture section type Type uint8 const ( @@ -26,8 +26,8 @@ const ( Process Type = iota + 1 // Handle is the handle header type Handle - // Kevt is the kernel event header type - Kevt + // Event is the event header type + Event // PE is the Portable Executable header type PE ) @@ -39,8 +39,8 @@ func (s Type) String() string { return "process" case Handle: return "handle" - case Kevt: - return "kevent" + case Event: + return "event" case PE: return "pe" default: diff --git a/pkg/kcap/section/section_windows_test.go b/pkg/cap/section/section_windows_test.go similarity index 94% rename from pkg/kcap/section/section_windows_test.go rename to pkg/cap/section/section_windows_test.go index e464708c1..41fa28def 100644 --- a/pkg/kcap/section/section_windows_test.go +++ b/pkg/cap/section/section_windows_test.go @@ -19,7 +19,7 @@ package section import ( - kcapver "github.com/rabbitstack/fibratus/pkg/kcap/version" + kcapver "github.com/rabbitstack/fibratus/pkg/cap/version" "github.com/stretchr/testify/assert" "testing" ) diff --git a/pkg/kcap/types_linux.go b/pkg/cap/types_linux.go similarity index 64% rename from pkg/kcap/types_linux.go rename to pkg/cap/types_linux.go index 52f0ef6d0..73f1d4226 100644 --- a/pkg/kcap/types_linux.go +++ b/pkg/cap/types_linux.go @@ -16,23 +16,23 @@ * limitations under the License. */ -package kcap +package cap type Writer interface { - // Write accepts two channels. The event channel receives events pushed by the kstream consumer. When the event + // Write accepts two channels. The event channel receives events pushed by the event source. When the event // is peeked from the channel, it is serialized and written to the underlying byte buffer. - Write(chan *kevent.Kevent, chan error) chan error + Write(chan *Event.Event, chan error) chan error // Close disposes all resources allocated by the writer. Close() error } -// Reader offers the mechanism for recovering the state of the kcapture and replaying all captured events. +// Reader offers the mechanism for recovering the state of the capture and replaying all captured events. type Reader interface { - // Read returns two channels. The event channel is poplated with event instances pulled from the kcap. If - // any error occurs during kcap processing, it is pushed to the error channel. - Read(ctx context.Context) (chan *kevent.Kevent, chan error) + // Read returns two channels. The event channel is populated with event instances pulled from the cap. If + // any error occurs during cap processing, it is pushed to the error channel. + Read(ctx context.Context) (chan *Event.Event, chan error) // Close shutdowns the reader gracefully. Close() error - // SetFilter sets the filter that's is applied to each event coming out of the kcap. + // SetFilter sets the filter that is applied to each event coming out of the capture. SetFilter(f filter.Filter) } diff --git a/pkg/kcap/types_windows.go b/pkg/cap/types_windows.go similarity index 71% rename from pkg/kcap/types_windows.go rename to pkg/cap/types_windows.go index fa30a0b17..2461ae7c9 100644 --- a/pkg/kcap/types_windows.go +++ b/pkg/cap/types_windows.go @@ -16,17 +16,17 @@ * limitations under the License. */ -package kcap +package cap import ( "context" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/filter" "github.com/rabbitstack/fibratus/pkg/handle" - "github.com/rabbitstack/fibratus/pkg/kevent" "github.com/rabbitstack/fibratus/pkg/ps" ) -// Writer is the minimal interface that all kcap writers need to satisfy. The Windows kcap +// Writer is the minimal interface that all cap writers need to satisfy. The Windows cap // file format has the layout as depicted in the following diagram: // // +-+-+-+-+-+-+-+-++-+-+-+-+-+-+-+-++-+-+-+ @@ -34,30 +34,30 @@ import ( // |---------------------------------------- // | Handle Section | Handles | // ----------------------------------------- -// | Kevt Section | Kevt ..................| +// | evt Section | evt ..................| // | ......................................| // | ......................................| // | ......................................| -// | ........ Kevt Section n Kevt n EOF | +// | ........ evt Section n evt n EOF | // +-+-+-+-+-+-+-+-++-+-+-+-+-+-+-+-++-+-+-+ type Writer interface { // Write accepts two channels. The event channel receives events pushed by the event consumer. // When the event is peeked from the channel, it is serialized and written to the underlying // byte buffer. - Write(<-chan *kevent.Kevent, <-chan error) chan error + Write(<-chan *event.Event, <-chan error) chan error // Close disposes all resources allocated by the writer. Close() error } -// Reader offers the mechanism for recovering the state of the kcapture and replaying all captured events. +// Reader offers the mechanism for recovering the state of the capture and replaying all captured events. type Reader interface { - // Read returns two channels. The event channel is poplated with event instances pulled from the kcap. If - // any error occurs during kcap processing, it is pushed to the error channel. - Read(ctx context.Context) (chan *kevent.Kevent, chan error) + // Read returns two channels. The event channel is populated with event instances pulled from the cap. If + // any error occurs during capture processing, it is pushed to the error channel. + Read(ctx context.Context) (chan *event.Event, chan error) // Close shutdowns the reader gracefully. Close() error - // RecoverSnapshotters recovers the statate of the snapshotters from the kcap. + // RecoverSnapshotters recovers the state of the snapshotters from the cap. RecoverSnapshotters() (handle.Snapshotter, ps.Snapshotter, error) - // SetFilter sets the filter applied to each event coming out of the kcap. + // SetFilter sets the filter applied to each event coming out of the cap. SetFilter(f filter.Filter) } diff --git a/pkg/kcap/version/version_windows.go b/pkg/cap/version/version_windows.go similarity index 90% rename from pkg/kcap/version/version_windows.go rename to pkg/cap/version/version_windows.go index e4da2de99..d2172bbe9 100644 --- a/pkg/kcap/version/version_windows.go +++ b/pkg/cap/version/version_windows.go @@ -22,10 +22,10 @@ package version type Version uint16 const ( - // KevtSecV1 is the v1 of the event section - KevtSecV1 Version = iota + 1 - // KevtSecV2 is the v2 of the event section - KevtSecV2 + // EvtSecV1 is the v1 of the event section + EvtSecV1 Version = iota + 1 + // EvtSecV2 is the v2 of the event section + EvtSecV2 ) const ( diff --git a/pkg/kcap/writer.go b/pkg/cap/writer.go similarity index 67% rename from pkg/kcap/writer.go rename to pkg/cap/writer.go index 53891b1ac..9d1ac0844 100644 --- a/pkg/kcap/writer.go +++ b/pkg/cap/writer.go @@ -1,5 +1,5 @@ -//go:build kcap -// +build kcap +//go:build cap +// +build cap /* * Copyright 2020-2021 by Nedim Sabic Sabic @@ -19,12 +19,12 @@ * limitations under the License. */ -package kcap +package cap import ( "expvar" "fmt" - "github.com/rabbitstack/fibratus/pkg/kcap/section" + "github.com/rabbitstack/fibratus/pkg/cap/section" "math" ) @@ -32,15 +32,15 @@ var ( // ErrWriteMagic signals magic write errors ErrWriteMagic = func(err error) error { return fmt.Errorf("couldn't write magic number: %v", err) } // ErrWriteVersion signals version write errors - ErrWriteVersion = func(v string, err error) error { return fmt.Errorf("couldn't write %s kcap digit: %v", v, err) } + ErrWriteVersion = func(v string, err error) error { return fmt.Errorf("couldn't write %s cap digit: %v", v, err) } // ErrWriteSection signals section write errors - ErrWriteSection = func(s section.Type, err error) error { return fmt.Errorf("couldn't write %s kcap section: %v", s, err) } + ErrWriteSection = func(s section.Type, err error) error { return fmt.Errorf("couldn't write %s cap section: %v", s, err) } - handleWriteErrors = expvar.NewInt("kcap.handle.write.errors") - kevtWriteErrors = expvar.NewInt("kcap.kevt.write.errors") - flusherErrors = expvar.NewMap("kcap.flusher.errors") - overflowKevents = expvar.NewInt("kcap.overflow.kevents") - kstreamConsumerErrors = expvar.NewInt("kcap.kstream.consumer.errors") + handleWriteErrors = expvar.NewInt("cap.handle.write.errors") + evtWriteErrors = expvar.NewInt("cap.evt.write.errors") + flusherErrors = expvar.NewMap("cap.flusher.errors") + overflowKevents = expvar.NewInt("cap.overflow.kevents") + eventSourceErrors = expvar.NewInt("cap.eventsource.errors") ) const maxKevtSize = math.MaxUint32 diff --git a/pkg/kcap/writer_unsupported.go b/pkg/cap/writer_unsupported.go similarity index 90% rename from pkg/kcap/writer_unsupported.go rename to pkg/cap/writer_unsupported.go index 3fb7cea24..c6c85cfba 100644 --- a/pkg/kcap/writer_unsupported.go +++ b/pkg/cap/writer_unsupported.go @@ -1,5 +1,5 @@ -//go:build !kcap -// +build !kcap +//go:build !cap +// +build !cap /* * Copyright 2019-2020 by Nedim Sabic Sabic @@ -19,7 +19,7 @@ * limitations under the License. */ -package kcap +package cap import ( kerrors "github.com/rabbitstack/fibratus/pkg/errors" @@ -29,5 +29,5 @@ import ( // NewWriter returns unsupported writer. func NewWriter(filename string, psnap ps.Snapshotter, hsnap handle.Snapshotter) (Writer, error) { - return nil, kerrors.ErrFeatureUnsupported("kcap") + return nil, kerrors.ErrFeatureUnsupported("cap") } diff --git a/pkg/kcap/writer_windows.go b/pkg/cap/writer_windows.go similarity index 83% rename from pkg/kcap/writer_windows.go rename to pkg/cap/writer_windows.go index 9dac3cb8a..08845273c 100644 --- a/pkg/kcap/writer_windows.go +++ b/pkg/cap/writer_windows.go @@ -1,5 +1,5 @@ -//go:build kcap -// +build kcap +//go:build cap +// +build cap /* * Copyright 2019-2020 by Nedim Sabic Sabic @@ -19,7 +19,7 @@ * limitations under the License. */ -package kcap +package cap import ( "fmt" @@ -31,10 +31,10 @@ import ( "github.com/dustin/go-humanize" "github.com/jedib0t/go-pretty/v6/table" + "github.com/rabbitstack/fibratus/pkg/cap/section" + capver "github.com/rabbitstack/fibratus/pkg/cap/version" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/handle" - "github.com/rabbitstack/fibratus/pkg/kcap/section" - kcapver "github.com/rabbitstack/fibratus/pkg/kcap/version" - "github.com/rabbitstack/fibratus/pkg/kevent" "github.com/rabbitstack/fibratus/pkg/ps" "github.com/rabbitstack/fibratus/pkg/util/bytes" zstd "github.com/valyala/gozstd" @@ -42,21 +42,21 @@ import ( type stats struct { kcapFile string - kevtsWritten uint64 + evtsWritten uint64 bytesWritten uint64 handlesWritten uint64 procsWritten uint64 } -func (s *stats) incKevts(kevt *kevent.Kevent) { - if !kevt.Type.OnlyState() { - atomic.AddUint64(&s.kevtsWritten, 1) +func (s *stats) incKevts(evt *event.Event) { + if !evt.Type.OnlyState() { + atomic.AddUint64(&s.evtsWritten, 1) } } func (s *stats) incBytes(bytes uint64) { atomic.AddUint64(&s.bytesWritten, bytes) } func (s *stats) incHandles() { atomic.AddUint64(&s.handlesWritten, 1) } -func (s *stats) incProcs(kevt *kevent.Kevent) { - if kevt.IsCreateProcess() || kevt.IsProcessRundown() { +func (s *stats) incProcs(evt *event.Event) { + if evt.IsCreateProcess() || evt.IsProcessRundown() { atomic.AddUint64(&s.procsWritten, 1) } } @@ -70,7 +70,7 @@ func (s *stats) printStats() { t.AppendRow(table.Row{"File", filepath.Base(s.kcapFile)}) t.AppendSeparator() - t.AppendRow(table.Row{"Events written", atomic.LoadUint64(&s.kevtsWritten)}) + t.AppendRow(table.Row{"Events written", atomic.LoadUint64(&s.evtsWritten)}) t.AppendRow(table.Row{"Bytes written", atomic.LoadUint64(&s.bytesWritten)}) t.AppendRow(table.Row{"Processes written", atomic.LoadUint64(&s.procsWritten)}) t.AppendRow(table.Row{"Handles written", atomic.LoadUint64(&s.handlesWritten)}) @@ -101,17 +101,17 @@ type writer struct { released atomic.Bool } -// NewWriter constructs a new instance of the kcap writer. +// NewWriter constructs a new instance of the cap writer. func NewWriter(filename string, psnap ps.Snapshotter, hsnap handle.Snapshotter) (Writer, error) { if filepath.Ext(filename) == "" { - filename += ".kcap" + filename += ".cap" } f, err := os.Create(filename) if err != nil { return nil, err } zw := zstd.NewWriter(f) - // start by writing the kcap header that is composed + // start by writing the cap header that is composed // of magic number, major/minor digits and the optional // flags bit vector. The flags bit vector is reserved // for the future uses. @@ -157,7 +157,7 @@ func NewWriter(filename string, psnap ps.Snapshotter, hsnap handle.Snapshotter) func (w *writer) writeSnapshots() error { handles := w.hsnap.GetSnapshot() // write handle section and the data blocks - err := w.ws(section.Handle, kcapver.HandleSecV1, uint32(len(handles)), 0) + err := w.ws(section.Handle, capver.HandleSecV1, uint32(len(handles)), 0) if err != nil { return err } @@ -183,13 +183,13 @@ func (w *writer) writeSnapshots() error { return w.zw.Flush() } -func (w *writer) Write(kevtsc <-chan *kevent.Kevent, errs <-chan error) chan error { +func (w *writer) Write(evtsc <-chan *event.Event, errs <-chan error) chan error { errsc := make(chan error, 100) go func() { for { select { - case kevt := <-kevtsc: - b := kevt.MarshalRaw() + case evt := <-evtsc: + b := evt.MarshalRaw() l := len(b) if l == 0 { continue @@ -201,12 +201,12 @@ func (w *writer) Write(kevtsc <-chan *kevent.Kevent, errs <-chan error) chan err continue } // update stats - w.stats.incKevts(kevt) + w.stats.incKevts(evt) w.stats.incBytes(uint64(l)) - w.stats.incProcs(kevt) + w.stats.incProcs(evt) case err := <-errs: errsc <- err - kstreamConsumerErrors.Add(1) + eventSourceErrors.Add(1) case <-w.stop: return } @@ -223,12 +223,12 @@ func (w *writer) write(b []byte) error { overflowKevents.Add(1) return fmt.Errorf("event size overflow by %d bytes", l-maxKevtSize) } - if err := w.ws(section.Kevt, kcapver.KevtSecV2, 0, uint32(l)); err != nil { - kevtWriteErrors.Add(1) + if err := w.ws(section.Event, capver.EvtSecV2, 0, uint32(l)); err != nil { + evtWriteErrors.Add(1) return err } if _, err := w.zw.Write(b); err != nil { - kevtWriteErrors.Add(1) + evtWriteErrors.Add(1) return err } return nil diff --git a/pkg/kcap/writer_windows_test.go b/pkg/cap/writer_windows_test.go similarity index 83% rename from pkg/kcap/writer_windows_test.go rename to pkg/cap/writer_windows_test.go index 582569ebb..6a8d16618 100644 --- a/pkg/kcap/writer_windows_test.go +++ b/pkg/cap/writer_windows_test.go @@ -1,5 +1,5 @@ -//go:build kcap -// +build kcap +//go:build cap +// +build cap /* * Copyright 2019-2020 by Nedim Sabic Sabic @@ -19,16 +19,15 @@ * limitations under the License. */ -package kcap +package cap import ( "github.com/rabbitstack/fibratus/internal/etw" "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/handle" htypes "github.com/rabbitstack/fibratus/pkg/handle/types" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/rabbitstack/fibratus/pkg/ps" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" log "github.com/sirupsen/logrus" @@ -58,32 +57,32 @@ func TestWrite(t *testing.T) { hsnap.On("GetSnapshot").Return(handles) - w, err := NewWriter("_fixtures/cap.kcap", psnap, hsnap) + w, err := NewWriter("_fixtures/cap.cap", psnap, hsnap) require.NoError(t, err) require.NotNil(t, w) - kevtsc := make(chan *kevent.Kevent, 100) + evtsc := make(chan *event.Event, 100) errs := make(chan error, 10) for i := 0; i < 100; i++ { - kevt := &kevent.Kevent{ - Type: ktypes.CreateFile, + evt := &event.Event{ + Type: event.CreateFile, Tid: 2484, PID: 859, CPU: uint8(i / 2), Seq: uint64(i + 1), Name: "CreateFile", Timestamp: time.Now(), - Category: ktypes.File, + Category: event.File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", - Kparams: kevent.Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.AnsiString, Value: "open"}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barz"}, PS: &pstypes.PS{ PID: 2436, Ppid: 6304, @@ -143,12 +142,12 @@ func TestWrite(t *testing.T) { }, } if i%2 == 0 { - kevt.PS.Handles = append(kevt.PS.Handles, htypes.Handle{}) + evt.PS.Handles = append(evt.PS.Handles, htypes.Handle{}) } - kevtsc <- kevt + evtsc <- evt } - werrs := w.Write(kevtsc, errs) + werrs := w.Write(evtsc, errs) quit := make(chan struct{}, 1) time.AfterFunc(time.Second*5, func() { quit <- struct{}{} @@ -158,12 +157,12 @@ func TestWrite(t *testing.T) { t.Fatal(err) case <-quit: w.Close() - require.True(t, w.(*writer).stats.kevtsWritten > 0) + require.True(t, w.(*writer).stats.evtsWritten > 0) return } } -func TestLiveKcap(t *testing.T) { +func TestLiveCapture(t *testing.T) { t.SkipNow() cfg := &config.Config{ Kstream: config.KstreamConfig{ @@ -174,7 +173,7 @@ func TestLiveKcap(t *testing.T) { EnableThreadKevents: true, EnableHandleKevents: true, }, - KcapFile: "../../test.kcap", + KcapFile: "../../test.cap", Filters: &config.Filters{}, InitHandleSnapshot: true, } @@ -193,7 +192,7 @@ func TestLiveKcap(t *testing.T) { t.Fatal(err) } - // bootstrap kcap writer with inbound event channel + // bootstrap cap writer with inbound event channel writer, err := NewWriter(cfg.KcapFile, psnap, hsnap) if err != nil { t.Fatal(err) diff --git a/pkg/config/_fixtures/fibratus.json b/pkg/config/_fixtures/fibratus.json index 56bd02b44..cba8b29de 100644 --- a/pkg/config/_fixtures/fibratus.json +++ b/pkg/config/_fixtures/fibratus.json @@ -30,7 +30,7 @@ "init-snapshot": true }, - "kevent": { + "Event": { }, diff --git a/pkg/config/_fixtures/fibratus.yml b/pkg/config/_fixtures/fibratus.yml index b5dafd9dd..9c8c9b9ac 100644 --- a/pkg/config/_fixtures/fibratus.yml +++ b/pkg/config/_fixtures/fibratus.yml @@ -119,9 +119,9 @@ handle: kcap: file: "" -# =============================== Kevent =============================================== +# =============================== Event =============================================== -kevent: +Event: serialize-threads: false serialize-images: false serialize-handles: false @@ -197,11 +197,11 @@ pe: transformers: remove: enabled: true - kparams: + params: - disposition rename: enabled: true - kparams: + params: - old: "a" new: "b" - old: "c" @@ -209,7 +209,7 @@ transformers: replace: enabled: false replacements: - - kparam: key_name + - Param: key_name old: HKEY_CURRENT_USER new: HCU tags: @@ -220,10 +220,10 @@ transformers: trim: enabled: false prefixes: - - kparam: key_name + - Param: key_name trim: CurrentControlSet suffixes: - - kparam: file_name + - Param: file_name trim: .exe # =============================== YARA ================================================= diff --git a/pkg/config/_fixtures/filters/default-with-template.yml b/pkg/config/_fixtures/filters/default-with-template.yml index 04b6b474f..96daef67a 100644 --- a/pkg/config/_fixtures/filters/default-with-template.yml +++ b/pkg/config/_fixtures/filters/default-with-template.yml @@ -3,7 +3,7 @@ id: d6385b50-532a-4464-929a-044f21443dd3 version: 1.0.0 enabled: true description: this rule matches all network signals -condition: kevt.category = 'net' +condition: evt.category = 'net' severity: low output: > {{ upper "all network events" }} diff --git a/pkg/config/_fixtures/filters/default.yml b/pkg/config/_fixtures/filters/default.yml index 6232f1bc2..58c988c68 100644 --- a/pkg/config/_fixtures/filters/default.yml +++ b/pkg/config/_fixtures/filters/default.yml @@ -3,7 +3,7 @@ id: 313933e7-8eb9-45d9-81af-0305fee70e29 version: 1.0.0 enabled: true description: this rule matches all network signals -condition: kevt.category = 'net' +condition: evt.category = 'net' severity: low output: > `%ps.exe` attempted to reach out to `%net.sip` IP address diff --git a/pkg/config/_fixtures/filters/default1.yml b/pkg/config/_fixtures/filters/default1.yml index c24d5a7a7..3a9645a24 100644 --- a/pkg/config/_fixtures/filters/default1.yml +++ b/pkg/config/_fixtures/filters/default1.yml @@ -1,5 +1,5 @@ name: suspicious network {{ upper "activity" }} id: 1a06b6e0-a3f4-44a0-a1f0-89028273761b version: 1.1.0 -condition: kevt.category = 'net' and ps.name in ('at.exe', 'java.exe') +condition: evt.category = 'net' and ps.name in ('at.exe', 'java.exe') min-engine-version: 2.0.0 diff --git a/pkg/config/_fixtures/transformers.yml b/pkg/config/_fixtures/transformers.yml index 9785d071e..37a39665b 100644 --- a/pkg/config/_fixtures/transformers.yml +++ b/pkg/config/_fixtures/transformers.yml @@ -19,11 +19,11 @@ transformers.tags: transformers.remove: enabled: true - kparams: + params: - key_handle transformers.rename: enabled: true - kparams: + params: - old: key_handle new: KeyHandle \ No newline at end of file diff --git a/pkg/config/config_windows.go b/pkg/config/config_windows.go index 2829bb2c8..0921bdcc6 100644 --- a/pkg/config/config_windows.go +++ b/pkg/config/config_windows.go @@ -33,7 +33,7 @@ import ( removet "github.com/rabbitstack/fibratus/pkg/aggregator/transformers/remove" replacet "github.com/rabbitstack/fibratus/pkg/aggregator/transformers/replace" tagst "github.com/rabbitstack/fibratus/pkg/aggregator/transformers/tags" - "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/outputs/amqp" "github.com/rabbitstack/fibratus/pkg/outputs/elasticsearch" "github.com/rabbitstack/fibratus/pkg/util/log" @@ -62,7 +62,7 @@ import ( ) const ( - kcapFile = "kcap.file" + kcapFile = "cap.file" configFile = "config-file" debugPrivilege = "debug-privilege" initHandleSnapshot = "handle.init-snapshot" @@ -71,11 +71,11 @@ const ( symbolizeKernelAddresses = "symbolize-kernel-addresses" forwardMode = "forward" - serializeThreads = "kevent.serialize-threads" - serializeImages = "kevent.serialize-images" - serializeHandles = "kevent.serialize-handles" - serializePE = "kevent.serialize-pe" - serializeEnvs = "kevent.serialize-envs" + serializeThreads = "event.serialize-threads" + serializeImages = "event.serialize-images" + serializeHandles = "event.serialize-handles" + serializePE = "event.serialize-pe" + serializeEnvs = "event.serialize-envs" ) // Config stores configuration options for fine-tuning the behaviour of Fibratus. @@ -286,11 +286,11 @@ func (c *Config) Init() error { c.ForwardMode = c.viper.GetBool(forwardMode) c.KcapFile = c.viper.GetString(kcapFile) - kevent.SerializeThreads = c.viper.GetBool(serializeThreads) - kevent.SerializeImages = c.viper.GetBool(serializeImages) - kevent.SerializeHandles = c.viper.GetBool(serializeHandles) - kevent.SerializePE = c.viper.GetBool(serializePE) - kevent.SerializeEnvs = c.viper.GetBool(serializeEnvs) + event.SerializeThreads = c.viper.GetBool(serializeThreads) + event.SerializeImages = c.viper.GetBool(serializeImages) + event.SerializeHandles = c.viper.GetBool(serializeHandles) + event.SerializePE = c.viper.GetBool(serializePE) + event.SerializeEnvs = c.viper.GetBool(serializeEnvs) if c.opts.run || c.opts.replay { if err := c.tryLoadOutput(); err != nil { @@ -383,10 +383,10 @@ func (c *Config) addFlags() { c.flags.Bool(matchAll, true, "Indicates if the match all strategy is enabled for the rule engine. If the match all strategy is enabled, a single event can trigger multiple rules") } if c.opts.capture { - c.flags.StringP(kcapFile, "o", "", "The path of the output kcap file") + c.flags.StringP(kcapFile, "o", "", "The path of the output cap file") } if c.opts.replay { - c.flags.StringP(kcapFile, "k", "", "The path of the input kcap file") + c.flags.StringP(kcapFile, "k", "", "The path of the input cap file") } if c.opts.run || c.opts.replay || c.opts.list || c.opts.validate { c.flags.String(filamentPath, filepath.Join(os.Getenv("PROGRAMFILES"), "fibratus", "filaments"), "Denotes the directory where filaments are located") diff --git a/pkg/config/config_windows_test.go b/pkg/config/config_windows_test.go index 1ef4a108c..8510c7b01 100644 --- a/pkg/config/config_windows_test.go +++ b/pkg/config/config_windows_test.go @@ -89,8 +89,8 @@ func TestNewFromYamlFile(t *testing.T) { switch tr.Type { case transformers.Rename: rconfig := tr.Transformer.(rename.Config) - assert.Len(t, rconfig.Kparams, 2) - r1 := rconfig.Kparams[0] + assert.Len(t, rconfig.Params, 2) + r1 := rconfig.Params[0] assert.Equal(t, "b", r1.New) } } diff --git a/pkg/config/filters.go b/pkg/config/filters.go index 5c9cac2d6..c89fd0a11 100644 --- a/pkg/config/filters.go +++ b/pkg/config/filters.go @@ -22,8 +22,7 @@ import ( "bytes" "fmt" "github.com/Masterminds/sprig/v3" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/util/convert" "github.com/rabbitstack/fibratus/pkg/util/multierror" log "github.com/sirupsen/logrus" @@ -161,7 +160,7 @@ type ActionContext struct { // Events contains a single element simple rules // or a list of ordered matched events for sequence // policies - Events []*kevent.Kevent + Events []*event.Event // Filter represents the filter that matched the event Filter *FilterConfig } @@ -173,7 +172,7 @@ func (ctx *ActionContext) UniquePids() []uint32 { pids := make(map[uint32]struct{}) for _, e := range ctx.Events { if e.IsCreateProcess() { - pids[e.Kparams.MustGetPid()] = struct{}{} + pids[e.Params.MustGetPid()] = struct{}{} } else { pids[e.PID] = struct{}{} } @@ -199,13 +198,13 @@ type RulesCompileResult struct { HasDNSEvents bool HasAuditAPIEvents bool HasThreadpoolEvents bool - UsedEvents []ktypes.Ktype + UsedEvents []event.Type NumberRules int } -func (r RulesCompileResult) ContainsEvent(ktype ktypes.Ktype) bool { +func (r RulesCompileResult) ContainsEvent(Type event.Type) bool { for _, ktyp := range r.UsedEvents { - if ktyp == ktype { + if ktyp == Type { return true } } diff --git a/pkg/config/filters_test.go b/pkg/config/filters_test.go index 30a5cf2e2..0e036b71e 100644 --- a/pkg/config/filters_test.go +++ b/pkg/config/filters_test.go @@ -51,7 +51,7 @@ func TestLoadRulesFromPaths(t *testing.T) { assert.Equal(t, "1.0.0", f1.Version) assert.True(t, *f1.Enabled) assert.Contains(t, f1.Tags, "TE") - assert.Equal(t, "kevt.category = 'net'", f1.Condition) + assert.Equal(t, "evt.category = 'net'", f1.Condition) assert.Equal(t, "this rule matches all network signals", f1.Description) assert.Equal(t, "low", f1.Severity) assert.Equal(t, "`%ps.exe` attempted to reach out to `%net.sip` IP address\n", f1.Output) @@ -73,7 +73,7 @@ func TestLoadRulesFromPaths(t *testing.T) { f2 := filters.filters[1] assert.False(t, f2.IsDisabled()) assert.Equal(t, "suspicious network ACTIVITY", f2.Name) - assert.Equal(t, "kevt.category = 'net' and ps.name in ('at.exe', 'java.exe')", f2.Condition) + assert.Equal(t, "evt.category = 'net' and ps.name in ('at.exe', 'java.exe')", f2.Condition) } func TestLoadRulesFromPathsWithTemplate(t *testing.T) { @@ -135,7 +135,7 @@ func TestLoadGroupsFromURLs(t *testing.T) { assert.Equal(t, "only network category", f1.Name) assert.True(t, *f1.Enabled) assert.Contains(t, f1.Tags, "TE") - assert.Equal(t, "kevt.category = 'net'", f1.Condition) + assert.Equal(t, "evt.category = 'net'", f1.Condition) assert.Equal(t, "this rule matches all network signals", f1.Description) assert.Equal(t, "low", f1.Severity) assert.Equal(t, "`%ps.exe` attempted to reach out to `%net.sip` IP address\n", f1.Output) diff --git a/pkg/config/kstream.go b/pkg/config/kstream.go index f9fd33019..e0b09a78f 100644 --- a/pkg/config/kstream.go +++ b/pkg/config/kstream.go @@ -22,11 +22,11 @@ package config import ( + "github.com/rabbitstack/fibratus/pkg/event" "golang.org/x/sys/windows" "runtime" "time" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" "github.com/spf13/viper" ) @@ -63,17 +63,17 @@ var ( // KstreamConfig stores different configuration options for fine-tuning kstream consumer/controller settings. type KstreamConfig struct { - // EnableThreadKevents indicates if thread kernel events are collected by the ETW provider. + // EnableThreadKevents indicates if thread events are collected by the ETW provider. EnableThreadKevents bool `json:"enable-thread" yaml:"enable-thread"` - // EnableRegistryKevents indicates if registry kernel events are collected by the ETW provider. + // EnableRegistryKevents indicates if registry events are collected by the ETW provider. EnableRegistryKevents bool `json:"enable-registry" yaml:"enable-registry"` // EnableNetKevents determines whether network (TCP/UDP) events are collected by the ETW provider. EnableNetKevents bool `json:"enable-net" yaml:"enable-net"` - // EnableFileIOKevents indicates if file I/O kernel events are collected by the ETW provider. + // EnableFileIOKevents indicates if file I/O events are collected by the ETW provider. EnableFileIOKevents bool `json:"enable-fileio" yaml:"enable-fileio"` // EnableVAMapKevents indicates if VA map/unmap events are collected by the ETW provider. EnableVAMapKevents bool `json:"enable-vamap" yaml:"enable-vamap"` - // EnableImageKevents indicates if image kernel events are collected by the ETW provider. + // EnableImageKevents indicates if image events are collected by the ETW provider. EnableImageKevents bool `json:"enable-image" yaml:"enable-image"` // EnableHandleKevents indicates whether handle creation/disposal events are enabled. EnableHandleKevents bool `json:"enable-handle" yaml:"enable-handle"` @@ -102,7 +102,7 @@ type KstreamConfig struct { // ExcludedImages are process image names that will be rejected if they generate a kernel event. ExcludedImages []string `json:"blacklist.images" yaml:"blacklist.images"` - dropMasks ktypes.EventsetMasks + dropMasks event.EventsetMasks excludedImages map[string]bool } @@ -130,8 +130,8 @@ func (c *KstreamConfig) initFromViper(v *viper.Viper) { c.excludedImages = make(map[string]bool) for _, name := range c.ExcludedKevents { - if ktype := ktypes.KeventNameToKtype(name); ktype != ktypes.UnknownKtype { - c.dropMasks.Set(ktype) + if typ := event.NameToType(name); typ != event.UnknownType { + c.dropMasks.Set(typ) } } for _, name := range c.ExcludedImages { @@ -143,9 +143,9 @@ func (c *KstreamConfig) initFromViper(v *viper.Viper) { func (c *KstreamConfig) Init() { c.excludedImages = make(map[string]bool) for _, name := range c.ExcludedKevents { - for _, ktype := range ktypes.KeventNameToKtypes(name) { - if ktype != ktypes.UnknownKtype { - c.dropMasks.Set(ktype) + for _, typ := range event.NameToTypes(name) { + if typ != event.UnknownType { + c.dropMasks.Set(typ) } } } @@ -157,14 +157,14 @@ func (c *KstreamConfig) Init() { // SetDropMask inserts the event mask in the bitset to // instruct the given event type should be dropped from // the event stream. -func (c *KstreamConfig) SetDropMask(ktype ktypes.Ktype) { - c.dropMasks.Set(ktype) +func (c *KstreamConfig) SetDropMask(Type event.Type) { + c.dropMasks.Set(Type) } // TestDropMask checks if the specified event type has // the drop mask in the bitset. -func (c *KstreamConfig) TestDropMask(ktype ktypes.Ktype) bool { - return c.dropMasks.Test(ktype.GUID(), ktype.HookID()) +func (c *KstreamConfig) TestDropMask(Type event.Type) bool { + return c.dropMasks.Test(Type.GUID(), Type.HookID()) } // ExcludeKevent determines whether the supplied provider GUID diff --git a/pkg/config/kstream_test.go b/pkg/config/kstream_test.go index 56b633fae..7a3d6f258 100644 --- a/pkg/config/kstream_test.go +++ b/pkg/config/kstream_test.go @@ -22,12 +22,11 @@ package config import ( + "github.com/rabbitstack/fibratus/pkg/event" "testing" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -56,8 +55,8 @@ func TestKstreamConfig(t *testing.T) { assert.False(t, c.Kstream.EnableImageKevents) assert.False(t, c.Kstream.EnableFileIOKevents) - assert.True(t, c.Kstream.ExcludeKevent(ktypes.CloseHandle.GUID(), ktypes.CloseHandle.HookID())) - assert.False(t, c.Kstream.ExcludeKevent(ktypes.CreateProcess.GUID(), ktypes.CreateProcess.HookID())) + assert.True(t, c.Kstream.ExcludeKevent(event.CloseHandle.GUID(), event.CloseHandle.HookID())) + assert.False(t, c.Kstream.ExcludeKevent(event.CreateProcess.GUID(), event.CreateProcess.HookID())) assert.True(t, c.Kstream.ExcludeImage(&pstypes.PS{Name: "svchost.exe"})) assert.False(t, c.Kstream.ExcludeImage(&pstypes.PS{Name: "explorer.exe"})) diff --git a/pkg/config/schema_windows.go b/pkg/config/schema_windows.go index 1a1bb214c..631e76665 100644 --- a/pkg/config/schema_windows.go +++ b/pkg/config/schema_windows.go @@ -127,7 +127,7 @@ var schema = ` }, "additionalProperties": false }, - "kcap": { + "cap": { "type": "object", "properties": { "file": {"type": "string"} @@ -166,7 +166,7 @@ var schema = ` }, "additionalProperties": false }, - "kevent": { + "event": { "type": "object", "properties": { "serialize-threads": {"type": "boolean"}, @@ -339,13 +339,13 @@ var schema = ` "type": "object", "properties": { "enabled": {"type": "boolean"}, - "kparams": {"type": "array", "items": [{"type": "string"}]} + "params": {"type": "array", "items": [{"type": "string"}]} }, "if": { "properties": {"enabled": { "const": true }} }, "then": { - "properties": {"kparams": {"type": "array", "minItems": 1, "items": [{"type": "string"}]}} + "properties": {"params": {"type": "array", "minItems": 1, "items": [{"type": "string"}]}} }, "additionalProperties": false }, @@ -353,7 +353,7 @@ var schema = ` "type": "object", "properties": { "enabled": {"type": "boolean"}, - "kparams": {"type": "array", "items": [ + "params": {"type": "array", "items": [ { "type": "object", "properties": { @@ -368,7 +368,7 @@ var schema = ` "properties": {"enabled": { "const": true }} }, "then": { - "properties": {"kparams": {"minItems": 1}} + "properties": {"params": {"minItems": 1}} }, "additionalProperties": false }, @@ -380,7 +380,7 @@ var schema = ` { "type": "object", "properties": { - "kparam": {"type": "string", "minLength": 1}, + "Param": {"type": "string", "minLength": 1}, "old": {"type": "string", "minLength": 1}, "new": {"type": "string"} }, @@ -427,7 +427,7 @@ var schema = ` { "type": "object", "properties": { - "kparam": {"type": "string", "minLength": 1}, + "Param": {"type": "string", "minLength": 1}, "trim": {"type": "string", "minLength": 1} }, "additionalProperties": false @@ -437,7 +437,7 @@ var schema = ` { "type": "object", "properties": { - "kparam": {"type": "string", "minLength": 1}, + "Param": {"type": "string", "minLength": 1}, "trim": {"type": "string", "minLength": 1} }, "additionalProperties": false diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go index 83a210480..812c79c55 100644 --- a/pkg/errors/errors.go +++ b/pkg/errors/errors.go @@ -34,20 +34,20 @@ var ( } ) -// ErrKparamNotFound is the error is thrown when a parameter is not present in the list of parameters -type ErrKparamNotFound struct { +// ErrParamNotFound is the error is thrown when a parameter is not present in the list of parameters +type ErrParamNotFound struct { Name string } // Error returns the error message. -func (e ErrKparamNotFound) Error() string { +func (e ErrParamNotFound) Error() string { return "couldn't find '" + e.Name + "' in event parameters" } -// IsKparamNotFound returns true if the error is KparamNotFound. -func IsKparamNotFound(err error) bool { +// IsParamNotFound returns true if the error is ErrParamNotFound. +func IsParamNotFound(err error) bool { switch err.(type) { - case *ErrKparamNotFound: + case *ErrParamNotFound: return true default: return false diff --git a/pkg/errors/errors_windows.go b/pkg/errors/errors_windows.go index 9ce976d3b..ba5cd0fac 100644 --- a/pkg/errors/errors_windows.go +++ b/pkg/errors/errors_windows.go @@ -23,7 +23,7 @@ import ( ) var ( - // ErrTraceAccessDenied is returned when user doesn't have enough privileges to start kernel trace + // ErrTraceAccessDenied is returned when user doesn't have enough privileges to start the trace ErrTraceAccessDenied = errors.New("not enough privileges to start the trace. Only users with administrative privileges or users in the Performance Log Users group can start kernel traces") // ErrTraceInvalidParameter signals invalid values for trace session ErrTraceInvalidParameter = errors.New("trace has invalid values") @@ -35,14 +35,14 @@ var ( ErrTraceDiskFull = errors.New("not enough disk space for writing to log file") // ErrInvalidTrace signals invalid trace handle ErrInvalidTrace = errors.New("invalid trace handle") - // ErrRestartTrace signals an error that is thrown when currently running kernel trace cannot be restarted + // ErrRestartTrace signals an error that is thrown when currently running trace cannot be restarted ErrRestartTrace = errors.New("couldn't restart an already running trace") - // ErrTraceAlreadyRunning identifies kernel trace already running errors + // ErrTraceAlreadyRunning identifies trace already running errors ErrTraceAlreadyRunning = errors.New("trace is already running") // ErrEventCallbackException signals that an exception has occurred in the event processing function ErrEventCallbackException = errors.New("an exception occurred in the event callback function") - // ErrKsessionNotRunning is thrown when kernel session from which consumer is trying to collect events is not running - ErrKsessionNotRunning = errors.New("session from which you are trying to consume events in real time is not running") - // ErrTraceCancelled is thrown when in-progress kernel event trace is cancelled + // ErrSessionNotRunning is thrown when session from which consumer is trying to collect events is not running + ErrSessionNotRunning = errors.New("session from which you are trying to consume events in real time is not running") + // ErrTraceCancelled is thrown when in-progress event trace is cancelled ErrTraceCancelled = errors.New("event trace has been cancelled") ) diff --git a/pkg/kevent/_fixtures/handles.json b/pkg/event/_fixtures/handles.json similarity index 100% rename from pkg/kevent/_fixtures/handles.json rename to pkg/event/_fixtures/handles.json diff --git a/pkg/kevent/batch.go b/pkg/event/batch.go similarity index 86% rename from pkg/kevent/batch.go rename to pkg/event/batch.go index fbd86d470..facd8a0d9 100644 --- a/pkg/kevent/batch.go +++ b/pkg/event/batch.go @@ -16,15 +16,15 @@ * limitations under the License. */ -package kevent +package event -// Batch contains a sequence of kernel events. +// Batch contains a group of events. type Batch struct { - Events []*Kevent + Events []*Event } // NewBatch produces a new batch from the group of events. -func NewBatch(evts ...*Kevent) *Batch { +func NewBatch(evts ...*Event) *Batch { return &Batch{Events: evts} } @@ -35,12 +35,12 @@ func (b *Batch) Len() int64 { return int64(len(b.Events)) } func (b *Batch) MarshalJSON() []byte { buf := make([]byte, 0) buf = append(buf, '[') - for i, kevt := range b.Events { + for i, evt := range b.Events { writeMore := true if i == len(b.Events)-1 { writeMore = false } - buf = append(buf, kevt.MarshalJSON()...) + buf = append(buf, evt.MarshalJSON()...) buf = append(buf, '\n') if writeMore { buf = append(buf, ',') diff --git a/pkg/kevent/batch_test.go b/pkg/event/batch_test.go similarity index 78% rename from pkg/kevent/batch_test.go rename to pkg/event/batch_test.go index 41960b6e4..e53039d1b 100644 --- a/pkg/kevent/batch_test.go +++ b/pkg/event/batch_test.go @@ -16,14 +16,13 @@ * limitations under the License. */ -package kevent +package event import ( "encoding/json" "github.com/magiconair/properties/assert" + "github.com/rabbitstack/fibratus/pkg/event/params" htypes "github.com/rabbitstack/fibratus/pkg/handle/types" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" "github.com/rabbitstack/fibratus/pkg/util/va" "github.com/stretchr/testify/require" @@ -33,24 +32,24 @@ import ( ) func TestBatchMarshalJSON(t *testing.T) { - kevt := &Kevent{ - Type: ktypes.CreateFile, + evt := &Event{ + Type: CreateFile, Tid: 2484, PID: 859, CPU: 1, Seq: 2, Name: "CreateFile", Timestamp: time.Now(), - Category: ktypes.File, + Category: File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", - Kparams: Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, - kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, - kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + Params: Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.AnsiString, Value: "open"}, + params.BasePrio: {Name: params.BasePrio, Type: params.Int8, Value: int8(2)}, + params.PagePrio: {Name: params.PagePrio, Type: params.Uint8, Value: uint8(2)}, }, Metadata: map[MetadataKey]any{"foo": "bar", "fooz": "baarz"}, PS: &pstypes.PS{ @@ -101,24 +100,24 @@ func TestBatchMarshalJSON(t *testing.T) { }, } - kevt1 := &Kevent{ - Type: ktypes.CreateFile, + evt1 := &Event{ + Type: CreateFile, Tid: 2484, PID: 459, CPU: 1, Seq: 2, Name: "CreateFile", Timestamp: time.Now(), - Category: ktypes.File, + Category: File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", - Kparams: Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, - kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, - kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + Params: Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.AnsiString, Value: "open"}, + params.BasePrio: {Name: params.BasePrio, Type: params.Int8, Value: int8(2)}, + params.PagePrio: {Name: params.PagePrio, Type: params.Uint8, Value: uint8(2)}, }, Metadata: map[MetadataKey]any{"foo": "bar", "fooz": "baarz"}, PS: &pstypes.PS{ @@ -169,24 +168,24 @@ func TestBatchMarshalJSON(t *testing.T) { }, } - kevt2 := &Kevent{ - Type: ktypes.CreateFile, + evt2 := &Event{ + Type: CreateFile, Tid: 2484, PID: 829, CPU: 1, Seq: 2, Name: "CreateFile", Timestamp: time.Now(), - Category: ktypes.File, + Category: File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", - Kparams: Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, - kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, - kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + Params: Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.AnsiString, Value: "open"}, + params.BasePrio: {Name: params.BasePrio, Type: params.Int8, Value: int8(2)}, + params.PagePrio: {Name: params.PagePrio, Type: params.Uint8, Value: uint8(2)}, }, Metadata: map[MetadataKey]any{"foo": "bar", "fooz": "baarz"}, PS: &pstypes.PS{ @@ -237,17 +236,17 @@ func TestBatchMarshalJSON(t *testing.T) { }, } - b := NewBatch(kevt, kevt1, kevt2) + b := NewBatch(evt, evt1, evt2) require.Equal(t, int64(3), b.Len()) buf := b.MarshalJSON() - var kevts []*Kevent + var evts []*Event - err := json.Unmarshal(buf, &kevts) + err := json.Unmarshal(buf, &evts) require.NoError(t, err) - require.Len(t, kevts, 3) + require.Len(t, evts, 3) - assert.Equal(t, uint32(859), kevts[0].PID) - assert.Equal(t, uint32(459), kevts[1].PID) - assert.Equal(t, uint32(829), kevts[2].PID) + assert.Equal(t, uint32(859), evts[0].PID) + assert.Equal(t, uint32(459), evts[1].PID) + assert.Equal(t, uint32(829), evts[2].PID) } diff --git a/pkg/kevent/ktypes/category.go b/pkg/event/category.go similarity index 96% rename from pkg/kevent/ktypes/category.go rename to pkg/event/category.go index 08f0c5d52..d9d45e966 100644 --- a/pkg/kevent/ktypes/category.go +++ b/pkg/event/category.go @@ -16,7 +16,7 @@ * limitations under the License. */ -package ktypes +package event import ( "github.com/rabbitstack/fibratus/pkg/util/hashers" @@ -29,7 +29,7 @@ type Category string type Subcategory string const ( - // Registry is the category for registry related kernel events + // Registry is the category for registry related events Registry Category = "registry" // File is the category for file system events File Category = "file" diff --git a/pkg/kevent/doc.go b/pkg/event/doc.go similarity index 81% rename from pkg/kevent/doc.go rename to pkg/event/doc.go index 8b601e5c4..655006b76 100644 --- a/pkg/kevent/doc.go +++ b/pkg/event/doc.go @@ -16,6 +16,6 @@ * limitations under the License. */ -// Package kevent defines the fundamental data structures that underpin the state of every kernel event pushed from the -// consumer. -package kevent +// Package event defines the fundamental data structures that underpin the state of the event pushed from the +// event source. +package event diff --git a/pkg/kevent/enum.go b/pkg/event/enum.go similarity index 99% rename from pkg/kevent/enum.go rename to pkg/event/enum.go index a256cded5..6d2f71c52 100644 --- a/pkg/kevent/enum.go +++ b/pkg/event/enum.go @@ -16,7 +16,7 @@ * limitations under the License. */ -package kevent +package event import ( "github.com/rabbitstack/fibratus/pkg/util/va" diff --git a/pkg/kevent/kevent.go b/pkg/event/event.go similarity index 79% rename from pkg/kevent/kevent.go rename to pkg/event/event.go index 6b358788b..0383f0c76 100644 --- a/pkg/kevent/kevent.go +++ b/pkg/event/event.go @@ -16,14 +16,13 @@ * limitations under the License. */ -package kevent +package event import ( "fmt" "github.com/rabbitstack/fibratus/pkg/callstack" - kcapver "github.com/rabbitstack/fibratus/pkg/kcap/version" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + capver "github.com/rabbitstack/fibratus/pkg/cap/version" + "github.com/rabbitstack/fibratus/pkg/event/params" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" "strings" "sync" @@ -62,32 +61,32 @@ func (md Metadata) String() string { return strings.TrimSuffix(sb.String(), ", ") } -// Kevent encapsulates event's state and provides a set of methods for +// Event encapsulates event's state and provides a set of methods for // accessing and manipulating event parameters, process state, and other // metadata. -type Kevent struct { +type Event struct { // Seq is monotonically incremented kernel event sequence. Seq uint64 `json:"seq"` // PID is the identifier of the process that generated the event. PID uint32 `json:"pid"` // Tid is the thread identifier of the thread that generated the event. Tid uint32 `json:"tid"` - // Type is the internal representation of the kernel event. This field should be ignored by serializers. - Type ktypes.Ktype `json:"-"` + // Type is the internal representation of the event. This field should be ignored by serializers. + Type Type `json:"-"` // CPU designates the processor logical core where the event was originated. CPU uint8 `json:"cpu"` // Name is the human friendly name of the kernel event. Name string `json:"name"` // Category designates the category to which this event pertains. - Category ktypes.Category `json:"category"` + Category Category `json:"category"` // Description is the short explanation that describes the purpose of the event. Description string `json:"description"` // Host is the machine name that reported the generated event. Host string `json:"host"` // Timestamp represents the temporal occurrence of the event. Timestamp time.Time `json:"timestamp"` - // Kparams stores the collection of kernel event parameters. - Kparams Kparams `json:"params"` + // Params stores the collection of event parameters. + Params Params `json:"-"` // Metadata represents any tags that are meaningful to this event. Metadata Metadata `json:"metadata"` // mmux guards the metadata map @@ -104,7 +103,7 @@ type Kevent struct { } // String returns event's string representation. -func (e *Kevent) String() string { +func (e *Event) String() string { e.mmux.RLock() defer e.mmux.RUnlock() if e.PS != nil { @@ -119,7 +118,7 @@ func (e *Kevent) String() string { Description: %s Host: %s Timestamp: %s - Kparams: %s + Params: %s Metadata: %s %s `, @@ -133,7 +132,7 @@ func (e *Kevent) String() string { e.Description, e.Host, e.Timestamp, - e.Kparams, + e.Params, e.Metadata, e.PS, ) @@ -149,7 +148,7 @@ func (e *Kevent) String() string { Description: %s Host: %s Timestamp: %s - Kparams: %s + Params: %s Metadata: %s `, e.Seq, @@ -162,14 +161,14 @@ func (e *Kevent) String() string { e.Description, e.Host, e.Timestamp, - e.Kparams, + e.Params, e.Metadata, ) } // StringShort returns event's string representation // by removing some irrelevant event/process fields. -func (e *Kevent) StringShort() string { +func (e *Event) StringShort() string { e.mmux.RLock() defer e.mmux.RUnlock() if e.PS != nil { @@ -191,7 +190,7 @@ func (e *Kevent) StringShort() string { e.Category, e.Host, e.Timestamp, - e.Kparams, + e.Params, e.PS.StringShort(), ) } @@ -212,23 +211,23 @@ func (e *Kevent) StringShort() string { e.Category, e.Host, e.Timestamp, - e.Kparams, + e.Params, ) } // Empty return a pristine event instance. -func Empty() *Kevent { - return &Kevent{ - Kparams: map[string]*Kparam{}, +func Empty() *Event { + return &Event{ + Params: map[string]*Param{}, Metadata: make(map[MetadataKey]any), PS: &pstypes.PS{}, } } -// NewFromKcap recovers the event instance from the capture byte buffer. -func NewFromKcap(buf []byte, ver kcapver.Version) (*Kevent, error) { - e := &Kevent{ - Kparams: make(Kparams), +// NewFromCapture recovers the event instance from the capture byte buffer. +func NewFromCapture(buf []byte, ver capver.Version) (*Event, error) { + e := &Event{ + Params: make(Params), Metadata: make(map[MetadataKey]any), } if err := e.UnmarshalRaw(buf, ver); err != nil { @@ -238,21 +237,21 @@ func NewFromKcap(buf []byte, ver kcapver.Version) (*Kevent, error) { } // AddMeta appends a key/value pair to event's metadata. -func (e *Kevent) AddMeta(k MetadataKey, v any) { +func (e *Event) AddMeta(k MetadataKey, v any) { e.mmux.Lock() defer e.mmux.Unlock() e.Metadata[k] = v } // RemoveMeta removes the event metadata index by given key. -func (e *Kevent) RemoveMeta(k MetadataKey) { +func (e *Event) RemoveMeta(k MetadataKey) { e.mmux.Lock() defer e.mmux.Unlock() delete(e.Metadata, k) } // GetMetaAsString returns the metadata as a string value. -func (e *Kevent) GetMetaAsString(k MetadataKey) string { +func (e *Event) GetMetaAsString(k MetadataKey) string { e.mmux.RLock() defer e.mmux.RUnlock() if v, ok := e.Metadata[k]; ok { @@ -264,25 +263,25 @@ func (e *Kevent) GetMetaAsString(k MetadataKey) string { } // ContainsMeta returns true if the metadata contains the specified key. -func (e *Kevent) ContainsMeta(k MetadataKey) bool { +func (e *Event) ContainsMeta(k MetadataKey) bool { e.mmux.RLock() defer e.mmux.RUnlock() return e.Metadata[k] != nil } // AppendParam adds a new parameter to this event. -func (e *Kevent) AppendParam(name string, typ kparams.Type, value kparams.Value, opts ...ParamOption) { - e.Kparams.Append(name, typ, value, opts...) +func (e *Event) AppendParam(name string, typ params.Type, value params.Value, opts ...ParamOption) { + e.Params.Append(name, typ, value, opts...) } // AppendEnum adds the enum parameter to this event. -func (e *Kevent) AppendEnum(name string, value uint32, enum ParamEnum) { - e.AppendParam(name, kparams.Enum, value, WithEnum(enum)) +func (e *Event) AppendEnum(name string, value uint32, enum ParamEnum) { + e.AppendParam(name, params.Enum, value, WithEnum(enum)) } // AppendFlags adds the flags parameter to this event. -func (e *Kevent) AppendFlags(name string, value uint32, flags ParamFlags) { - e.AppendParam(name, kparams.Flags, value, WithFlags(flags)) +func (e *Event) AppendFlags(name string, value uint32, flags ParamFlags) { + e.AppendParam(name, params.Flags, value, WithFlags(flags)) } // GetParamAsString returns the specified parameter value as string. @@ -291,8 +290,8 @@ func (e *Kevent) AppendFlags(name string, value uint32, flags ParamFlags) { // to the error message. // Returns an empty string if the given parameter name is not found // in event parameters. -func (e *Kevent) GetParamAsString(name string) string { - par, err := e.Kparams.Get(name) +func (e *Event) GetParamAsString(name string) string { + par, err := e.Params.Get(name) if err != nil { return "" } @@ -300,12 +299,12 @@ func (e *Kevent) GetParamAsString(name string) string { } // GetFlagsAsSlice returns parameter flags as a slice of bitmask string values. -func (e *Kevent) GetFlagsAsSlice(name string) []string { +func (e *Event) GetFlagsAsSlice(name string) []string { return strings.Split(e.GetParamAsString(name), "|") } // SequenceLink returns the sequence link value from event metadata. -func (e *Kevent) SequenceLink() any { +func (e *Event) SequenceLink() any { e.mmux.RLock() defer e.mmux.RUnlock() return e.Metadata[RuleSequenceLink] diff --git a/pkg/kevent/kevent_windows.go b/pkg/event/event_windows.go similarity index 53% rename from pkg/kevent/kevent_windows.go rename to pkg/event/event_windows.go index 4c800520a..6269605ea 100644 --- a/pkg/kevent/kevent_windows.go +++ b/pkg/event/event_windows.go @@ -16,13 +16,12 @@ * limitations under the License. */ -package kevent +package event import ( "encoding/binary" "fmt" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/sys" "github.com/rabbitstack/fibratus/pkg/sys/etw" "github.com/rabbitstack/fibratus/pkg/util/filetime" @@ -48,23 +47,23 @@ var ( // New constructs a fresh event instance with basic fields and parameters // from the raw ETW event record. -func New(seq uint64, ktype ktypes.Ktype, evt *etw.EventRecord) *Kevent { +func New(seq uint64, typ Type, evt *etw.EventRecord) *Event { var ( pid = evt.Header.ProcessID tid = evt.Header.ThreadID cpu = *(*uint8)(unsafe.Pointer(&evt.BufferContext.ProcessorIndex[0])) ts = filetime.ToEpoch(evt.Header.Timestamp) ) - e := &Kevent{ + e := &Event{ Seq: seq, PID: pid, Tid: tid, CPU: cpu, - Type: ktype, - Category: ktype.Category(), - Name: ktype.String(), - Kparams: make(map[string]*Kparam), - Description: ktype.Description(), + Type: typ, + Category: typ.Category(), + Name: typ.String(), + Params: make(map[string]*Param), + Description: typ.Description(), Timestamp: ts, Metadata: make(map[MetadataKey]any), Host: hostname.Get(), @@ -74,24 +73,24 @@ func New(seq uint64, ktype ktypes.Ktype, evt *etw.EventRecord) *Kevent { return e } -func (e *Kevent) adjustPID() { +func (e *Event) adjustPID() { switch e.Category { - case ktypes.Image: + case Image: // sometimes the pid present in event header is invalid // but, we can get the valid one from the event parameters if e.InvalidPid() { - e.PID, _ = e.Kparams.GetPid() + e.PID, _ = e.Params.GetPid() } - case ktypes.File: + case File: if !e.IsMapViewFile() && !e.IsUnmapViewFile() { // take thread id from the event parameters - e.Tid, _ = e.Kparams.GetTid() + e.Tid, _ = e.Params.GetTid() } switch { - case e.InvalidPid() && e.Type == ktypes.MapFileRundown: + case e.InvalidPid() && e.Type == MapFileRundown: // a valid pid for map rundown events // is located in the event parameters - e.PID = e.Kparams.MustGetPid() + e.PID = e.Params.MustGetPid() case e.InvalidPid(): // on some Windows versions the value of // the PID is invalid in the event header @@ -105,26 +104,26 @@ func (e *Kevent) adjustPID() { }() e.PID = sys.GetProcessIdOfThread(thread) } - case ktypes.Process: + case Process: // process start events may be logged in the context of the parent or child process. // As a result, the ProcessId member of EVENT_TRACE_HEADER may not correspond to the // process being created, so we set the event pid to be the one of the parent process if e.IsCreateProcess() { - e.PID, _ = e.Kparams.GetPpid() + e.PID, _ = e.Params.GetPpid() } - case ktypes.Net: + case Net: if !e.IsDNS() { - e.PID, _ = e.Kparams.GetPid() + e.PID, _ = e.Params.GetPid() } - case ktypes.Handle: - if e.Type == ktypes.DuplicateHandle { - e.PID, _ = e.Kparams.GetUint32(kparams.TargetProcessID) - e.Kparams.Remove(kparams.TargetProcessID) + case Handle: + if e.Type == DuplicateHandle { + e.PID, _ = e.Params.GetUint32(params.TargetProcessID) + e.Params.Remove(params.TargetProcessID) } - case ktypes.Thread: - if e.Type == ktypes.StackWalk { - e.PID, _ = e.Kparams.GetPid() - e.Tid, _ = e.Kparams.GetTid() + case Thread: + if e.Type == StackWalk { + e.PID, _ = e.Params.GetPid() + e.Tid, _ = e.Params.GetTid() } } } @@ -136,7 +135,7 @@ func (e *Kevent) adjustPID() { // we're not storing them into the capture file, it can be dropped // 2. Rundowns events are dropped if they haven't been processed already // 3. If the event is generated by Fibratus process, we can safely ignore it -func (e *Kevent) IsDropped(capture bool) bool { +func (e *Event) IsDropped(capture bool) bool { if e.IsState() && !capture { return true } @@ -154,43 +153,43 @@ func IsCurrentProcDropped(pid uint32) bool { return DropCurrentProc && pid == cu // store and reference delayed events in the event // backlog state. The delayed event is indexed by // the sequence identifier. -func (e *Kevent) DelayKey() uint64 { +func (e *Event) DelayKey() uint64 { switch e.Type { - case ktypes.CreateHandle, ktypes.CloseHandle: - return e.Kparams.MustGetUint64(kparams.HandleObject) + case CreateHandle, CloseHandle: + return e.Params.MustGetUint64(params.HandleObject) } return 0 } // IsNetworkTCP determines whether the event pertains to network TCP events. -func (e *Kevent) IsNetworkTCP() bool { - return e.Category == ktypes.Net && !e.IsNetworkUDP() +func (e *Event) IsNetworkTCP() bool { + return e.Category == Net && !e.IsNetworkUDP() } // IsNetworkUDP determines whether the event pertains to network UDP events. -func (e *Kevent) IsNetworkUDP() bool { - return e.Type == ktypes.RecvUDPv4 || e.Type == ktypes.RecvUDPv6 || e.Type == ktypes.SendUDPv4 || e.Type == ktypes.SendUDPv6 +func (e *Event) IsNetworkUDP() bool { + return e.Type == RecvUDPv4 || e.Type == RecvUDPv6 || e.Type == SendUDPv4 || e.Type == SendUDPv6 } // IsDNS determines whether the event is a DNS question/answer. -func (e *Kevent) IsDNS() bool { - return e.Type.Subcategory() == ktypes.DNS +func (e *Event) IsDNS() bool { + return e.Type.Subcategory() == DNS } // IsRundown determines if this is a rundown events. -func (e *Kevent) IsRundown() bool { - return e.Type == ktypes.ProcessRundown || e.Type == ktypes.ThreadRundown || e.Type == ktypes.ImageRundown || - e.Type == ktypes.FileRundown || e.Type == ktypes.RegKCBRundown +func (e *Event) IsRundown() bool { + return e.Type == ProcessRundown || e.Type == ThreadRundown || e.Type == ImageRundown || + e.Type == FileRundown || e.Type == RegKCBRundown } // IsSuccess checks if the event contains the status parameter // and in such case, returns true if the operation completed // successfully, i.e. the system code is equal to ERROR_SUCCESS. -func (e *Kevent) IsSuccess() bool { - if !e.Kparams.Contains(kparams.NTStatus) { +func (e *Event) IsSuccess() bool { + if !e.Params.Contains(params.NTStatus) { return true } - return e.GetParamAsString(kparams.NTStatus) == ntstatus.Success + return e.GetParamAsString(params.NTStatus) == ntstatus.Success } // IsRundownProcessed checks if the rundown events was processed @@ -201,7 +200,7 @@ func (e *Kevent) IsSuccess() bool { // function which causes duplicate rundown events. // For more pointers check `kstream/controller_windows.go` // and the `etw.SetTraceInformation` API function. -func (e *Kevent) IsRundownProcessed() bool { +func (e *Event) IsRundownProcessed() bool { mu.Lock() defer mu.Unlock() key := e.RundownKey() @@ -213,96 +212,96 @@ func (e *Kevent) IsRundownProcessed() bool { return false } -func (e *Kevent) IsCreateFile() bool { return e.Type == ktypes.CreateFile } -func (e *Kevent) IsCreateProcess() bool { return e.Type == ktypes.CreateProcess } -func (e *Kevent) IsCreateThread() bool { return e.Type == ktypes.CreateThread } -func (e *Kevent) IsCloseFile() bool { return e.Type == ktypes.CloseFile } -func (e *Kevent) IsCreateHandle() bool { return e.Type == ktypes.CreateHandle } -func (e *Kevent) IsCloseHandle() bool { return e.Type == ktypes.CloseHandle } -func (e *Kevent) IsDeleteFile() bool { return e.Type == ktypes.DeleteFile } -func (e *Kevent) IsEnumDirectory() bool { return e.Type == ktypes.EnumDirectory } -func (e *Kevent) IsTerminateProcess() bool { return e.Type == ktypes.TerminateProcess } -func (e *Kevent) IsTerminateThread() bool { return e.Type == ktypes.TerminateThread } -func (e *Kevent) IsUnloadImage() bool { return e.Type == ktypes.UnloadImage } -func (e *Kevent) IsLoadImage() bool { return e.Type == ktypes.LoadImage } -func (e *Kevent) IsImageRundown() bool { return e.Type == ktypes.ImageRundown } -func (e *Kevent) IsFileOpEnd() bool { return e.Type == ktypes.FileOpEnd } -func (e *Kevent) IsRegSetValue() bool { return e.Type == ktypes.RegSetValue } -func (e *Kevent) IsProcessRundown() bool { return e.Type == ktypes.ProcessRundown } -func (e *Kevent) IsVirtualAlloc() bool { return e.Type == ktypes.VirtualAlloc } -func (e *Kevent) IsMapViewFile() bool { return e.Type == ktypes.MapViewFile } -func (e *Kevent) IsUnmapViewFile() bool { return e.Type == ktypes.UnmapViewFile } -func (e *Kevent) IsStackWalk() bool { return e.Type == ktypes.StackWalk } +func (e *Event) IsCreateFile() bool { return e.Type == CreateFile } +func (e *Event) IsCreateProcess() bool { return e.Type == CreateProcess } +func (e *Event) IsCreateThread() bool { return e.Type == CreateThread } +func (e *Event) IsCloseFile() bool { return e.Type == CloseFile } +func (e *Event) IsCreateHandle() bool { return e.Type == CreateHandle } +func (e *Event) IsCloseHandle() bool { return e.Type == CloseHandle } +func (e *Event) IsDeleteFile() bool { return e.Type == DeleteFile } +func (e *Event) IsEnumDirectory() bool { return e.Type == EnumDirectory } +func (e *Event) IsTerminateProcess() bool { return e.Type == TerminateProcess } +func (e *Event) IsTerminateThread() bool { return e.Type == TerminateThread } +func (e *Event) IsUnloadImage() bool { return e.Type == UnloadImage } +func (e *Event) IsLoadImage() bool { return e.Type == LoadImage } +func (e *Event) IsImageRundown() bool { return e.Type == ImageRundown } +func (e *Event) IsFileOpEnd() bool { return e.Type == FileOpEnd } +func (e *Event) IsRegSetValue() bool { return e.Type == RegSetValue } +func (e *Event) IsProcessRundown() bool { return e.Type == ProcessRundown } +func (e *Event) IsVirtualAlloc() bool { return e.Type == VirtualAlloc } +func (e *Event) IsMapViewFile() bool { return e.Type == MapViewFile } +func (e *Event) IsUnmapViewFile() bool { return e.Type == UnmapViewFile } +func (e *Event) IsStackWalk() bool { return e.Type == StackWalk } // InvalidPid indicates if the process generating the event is invalid. -func (e *Kevent) InvalidPid() bool { return e.PID == sys.InvalidProcessID } +func (e *Event) InvalidPid() bool { return e.PID == sys.InvalidProcessID } // CurrentPid indicates if Fibratus is the process generating the event. -func (e *Kevent) CurrentPid() bool { return e.PID == currentPid } +func (e *Event) CurrentPid() bool { return e.PID == currentPid } // IsSystemPid indicates if the process generating the event is the System process. -func (e *Kevent) IsSystemPid() bool { return e.PID == 4 } +func (e *Event) IsSystemPid() bool { return e.PID == 4 } // IsState indicates if this event is only used for state management. -func (e *Kevent) IsState() bool { return e.Type.OnlyState() } +func (e *Event) IsState() bool { return e.Type.OnlyState() } // IsCreateDisposition determines if the file disposition leads to creating a new file. -func (e *Kevent) IsCreateDisposition() bool { - return e.IsCreateFile() && e.Kparams.MustGetUint32(kparams.FileOperation) == windows.FILE_CREATE +func (e *Event) IsCreateDisposition() bool { + return e.IsCreateFile() && e.Params.MustGetUint32(params.FileOperation) == windows.FILE_CREATE } // IsOpenDisposition determines if the file disposition leads to opening a file object. -func (e *Kevent) IsOpenDisposition() bool { - return e.IsCreateFile() && e.Kparams.MustGetUint32(kparams.FileOperation) == windows.FILE_OPEN +func (e *Event) IsOpenDisposition() bool { + return e.IsCreateFile() && e.Params.MustGetUint32(params.FileOperation) == windows.FILE_OPEN } // StackID returns the integer that is used to identify the callstack present in the StackWalk event. -func (e *Kevent) StackID() uint64 { return uint64(e.PID + e.Tid) } +func (e *Event) StackID() uint64 { return uint64(e.PID + e.Tid) } // RundownKey calculates the rundown event hash. The hash is // used to determine if the rundown event was already processed. -func (e *Kevent) RundownKey() uint64 { +func (e *Event) RundownKey() uint64 { switch e.Type { - case ktypes.ProcessRundown: + case ProcessRundown: b := make([]byte, 4) - pid, _ := e.Kparams.GetPid() + pid, _ := e.Params.GetPid() binary.LittleEndian.PutUint32(b, pid) return hashers.FnvUint64(b) - case ktypes.ThreadRundown: + case ThreadRundown: b := make([]byte, 8) - pid, _ := e.Kparams.GetPid() - tid, _ := e.Kparams.GetTid() + pid, _ := e.Params.GetPid() + tid, _ := e.Params.GetTid() binary.LittleEndian.PutUint32(b, pid) binary.LittleEndian.PutUint32(b, tid) return hashers.FnvUint64(b) - case ktypes.ImageRundown: - pid, _ := e.Kparams.GetPid() - mod, _ := e.Kparams.GetString(kparams.ImagePath) + case ImageRundown: + pid, _ := e.Params.GetPid() + mod, _ := e.Params.GetString(params.ImagePath) b := make([]byte, 4+len(mod)) binary.LittleEndian.PutUint32(b, pid) b = append(b, mod...) return hashers.FnvUint64(b) - case ktypes.FileRundown: + case FileRundown: b := make([]byte, 8) - fileObject, _ := e.Kparams.GetUint64(kparams.FileObject) + fileObject, _ := e.Params.GetUint64(params.FileObject) binary.LittleEndian.PutUint64(b, fileObject) return hashers.FnvUint64(b) - case ktypes.MapFileRundown: + case MapFileRundown: b := make([]byte, 12) - fileKey, _ := e.Kparams.GetUint64(kparams.FileKey) + fileKey, _ := e.Params.GetUint64(params.FileKey) binary.LittleEndian.PutUint32(b, e.PID) binary.LittleEndian.PutUint64(b, fileKey) return hashers.FnvUint64(b) - case ktypes.RegKCBRundown: - key, _ := e.Kparams.GetString(kparams.RegPath) + case RegKCBRundown: + key, _ := e.Params.GetString(params.RegPath) b := make([]byte, 4+len(key)) binary.LittleEndian.PutUint32(b, e.PID) @@ -316,76 +315,76 @@ func (e *Kevent) RundownKey() uint64 { // that can be employed to determine if the event // from the given process and source has been processed // in the rule sequences. -func (e *Kevent) PartialKey() uint64 { +func (e *Event) PartialKey() uint64 { switch e.Type { - case ktypes.WriteFile, ktypes.ReadFile: - return e.Kparams.MustGetUint64(kparams.FileObject) + uint64(e.PID) - case ktypes.MapViewFile, ktypes.UnmapViewFile: - return e.Kparams.MustGetUint64(kparams.FileViewBase) + uint64(e.PID) - case ktypes.CreateFile: - file, _ := e.Kparams.GetString(kparams.FilePath) + case WriteFile, ReadFile: + return e.Params.MustGetUint64(params.FileObject) + uint64(e.PID) + case MapViewFile, UnmapViewFile: + return e.Params.MustGetUint64(params.FileViewBase) + uint64(e.PID) + case CreateFile: + file, _ := e.Params.GetString(params.FilePath) b := make([]byte, 4+len(file)) binary.LittleEndian.PutUint32(b, e.PID) b = append(b, []byte(file)...) return hashers.FnvUint64(b) - case ktypes.OpenProcess: - pid := e.Kparams.MustGetUint32(kparams.ProcessID) - access := e.Kparams.MustGetUint32(kparams.DesiredAccess) + case OpenProcess: + pid := e.Params.MustGetUint32(params.ProcessID) + access := e.Params.MustGetUint32(params.DesiredAccess) return uint64(pid + access + e.PID) - case ktypes.OpenThread: - tid := e.Kparams.MustGetUint32(kparams.ThreadID) - access := e.Kparams.MustGetUint32(kparams.DesiredAccess) + case OpenThread: + tid := e.Params.MustGetUint32(params.ThreadID) + access := e.Params.MustGetUint32(params.DesiredAccess) return uint64(tid + access + e.PID) - case ktypes.AcceptTCPv4, ktypes.RecvTCPv4, ktypes.RecvUDPv4: + case AcceptTCPv4, RecvTCPv4, RecvUDPv4: b := make([]byte, 10) - ip, _ := e.Kparams.GetIP(kparams.NetSIP) - port, _ := e.Kparams.GetUint16(kparams.NetSport) + ip, _ := e.Params.GetIP(params.NetSIP) + port, _ := e.Params.GetUint16(params.NetSport) binary.LittleEndian.PutUint32(b, e.PID) binary.LittleEndian.PutUint32(b, binary.BigEndian.Uint32(ip.To4())) binary.LittleEndian.PutUint16(b, port) return hashers.FnvUint64(b) - case ktypes.AcceptTCPv6, ktypes.RecvTCPv6, ktypes.RecvUDPv6: + case AcceptTCPv6, RecvTCPv6, RecvUDPv6: b := make([]byte, 22) - ip, _ := e.Kparams.GetIP(kparams.NetSIP) - port, _ := e.Kparams.GetUint16(kparams.NetSport) + ip, _ := e.Params.GetIP(params.NetSIP) + port, _ := e.Params.GetUint16(params.NetSport) binary.LittleEndian.PutUint32(b, e.PID) binary.LittleEndian.PutUint64(b, binary.BigEndian.Uint64(ip.To16()[0:8])) binary.LittleEndian.PutUint64(b, binary.BigEndian.Uint64(ip.To16()[8:16])) binary.LittleEndian.PutUint16(b, port) return hashers.FnvUint64(b) - case ktypes.ConnectTCPv4, ktypes.SendTCPv4, ktypes.SendUDPv4: + case ConnectTCPv4, SendTCPv4, SendUDPv4: b := make([]byte, 10) - ip, _ := e.Kparams.GetIP(kparams.NetDIP) - port, _ := e.Kparams.GetUint16(kparams.NetDport) + ip, _ := e.Params.GetIP(params.NetDIP) + port, _ := e.Params.GetUint16(params.NetDport) binary.LittleEndian.PutUint32(b, e.PID) binary.LittleEndian.PutUint32(b, binary.BigEndian.Uint32(ip.To4())) binary.LittleEndian.PutUint16(b, port) return hashers.FnvUint64(b) - case ktypes.ConnectTCPv6, ktypes.SendTCPv6, ktypes.SendUDPv6: + case ConnectTCPv6, SendTCPv6, SendUDPv6: b := make([]byte, 22) - ip, _ := e.Kparams.GetIP(kparams.NetDIP) - port, _ := e.Kparams.GetUint16(kparams.NetDport) + ip, _ := e.Params.GetIP(params.NetDIP) + port, _ := e.Params.GetUint16(params.NetDport) binary.LittleEndian.PutUint32(b, e.PID) binary.LittleEndian.PutUint64(b, binary.BigEndian.Uint64(ip.To16()[0:8])) binary.LittleEndian.PutUint64(b, binary.BigEndian.Uint64(ip.To16()[8:16])) binary.LittleEndian.PutUint16(b, port) return hashers.FnvUint64(b) - case ktypes.RegOpenKey, ktypes.RegQueryKey, ktypes.RegQueryValue, - ktypes.RegDeleteKey, ktypes.RegDeleteValue, ktypes.RegSetValue, - ktypes.RegCloseKey: - key, _ := e.Kparams.GetString(kparams.RegPath) + case RegOpenKey, RegQueryKey, RegQueryValue, + RegDeleteKey, RegDeleteValue, RegSetValue, + RegCloseKey: + key, _ := e.Params.GetString(params.RegPath) b := make([]byte, 4+len(key)) binary.LittleEndian.PutUint32(b, e.PID) b = append(b, key...) return hashers.FnvUint64(b) - case ktypes.VirtualAlloc, ktypes.VirtualFree: - return e.Kparams.MustGetUint64(kparams.MemBaseAddress) + uint64(e.PID) - case ktypes.DuplicateHandle: - pid := e.Kparams.MustGetUint32(kparams.ProcessID) - object := e.Kparams.MustGetUint64(kparams.HandleObject) + case VirtualAlloc, VirtualFree: + return e.Params.MustGetUint64(params.MemBaseAddress) + uint64(e.PID) + case DuplicateHandle: + pid := e.Params.MustGetUint32(params.ProcessID) + object := e.Params.MustGetUint64(params.HandleObject) return object + uint64(pid+e.PID) - case ktypes.QueryDNS, ktypes.ReplyDNS: - n, _ := e.Kparams.GetString(kparams.DNSName) + case QueryDNS, ReplyDNS: + n, _ := e.Params.GetString(params.DNSName) b := make([]byte, 4+len(n)) binary.LittleEndian.PutUint32(b, e.PID) b = append(b, n...) @@ -395,182 +394,182 @@ func (e *Kevent) PartialKey() uint64 { } // BacklogKey represents the key used to index the events in the backlog store. -func (e *Kevent) BacklogKey() uint64 { +func (e *Event) BacklogKey() uint64 { switch e.Type { - case ktypes.CreateHandle, ktypes.CloseHandle: - return e.Kparams.MustGetUint64(kparams.HandleObject) + case CreateHandle, CloseHandle: + return e.Params.MustGetUint64(params.HandleObject) } return 0 } // CopyState adds parameters, tags, or process state from the provided event. -func (e *Kevent) CopyState(evt *Kevent) { +func (e *Event) CopyState(evt *Event) { switch evt.Type { - case ktypes.CloseHandle: - if evt.Kparams.Contains(kparams.ImagePath) { - e.Kparams.Append(kparams.ImagePath, kparams.UnicodeString, evt.GetParamAsString(kparams.ImagePath)) + case CloseHandle: + if evt.Params.Contains(params.ImagePath) { + e.Params.Append(params.ImagePath, params.UnicodeString, evt.GetParamAsString(params.ImagePath)) } - _ = e.Kparams.SetValue(kparams.HandleObjectName, evt.GetParamAsString(kparams.HandleObjectName)) + _ = e.Params.SetValue(params.HandleObjectName, evt.GetParamAsString(params.HandleObjectName)) } } // Summary returns a brief summary of this event. Various important substrings // in the summary text are highlighted by surrounding them inside HTML tags. -func (e *Kevent) Summary() string { +func (e *Event) Summary() string { switch e.Type { - case ktypes.CreateProcess: - exe := e.Kparams.MustGetString(kparams.Exe) - sid := e.GetParamAsString(kparams.Username) + case CreateProcess: + exe := e.Params.MustGetString(params.Exe) + sid := e.GetParamAsString(params.Username) return printSummary(e, fmt.Sprintf("spawned %s process as %s user", exe, sid)) - case ktypes.TerminateProcess: - exe := e.Kparams.MustGetString(kparams.Exe) - sid := e.GetParamAsString(kparams.Username) + case TerminateProcess: + exe := e.Params.MustGetString(params.Exe) + sid := e.GetParamAsString(params.Username) return printSummary(e, fmt.Sprintf("terminated %s process as %s user", exe, sid)) - case ktypes.OpenProcess: - access := e.GetParamAsString(kparams.DesiredAccess) - exe, _ := e.Kparams.GetString(kparams.Exe) + case OpenProcess: + access := e.GetParamAsString(params.DesiredAccess) + exe, _ := e.Params.GetString(params.Exe) return printSummary(e, fmt.Sprintf("opened %s process object with %s access right(s)", exe, access)) - case ktypes.CreateThread: - tid, _ := e.Kparams.GetTid() - addr := e.GetParamAsString(kparams.StartAddress) + case CreateThread: + tid, _ := e.Params.GetTid() + addr := e.GetParamAsString(params.StartAddress) return printSummary(e, fmt.Sprintf("spawned a new thread with %d id at %s address", tid, addr)) - case ktypes.TerminateThread: - tid, _ := e.Kparams.GetTid() - addr := e.GetParamAsString(kparams.StartAddress) + case TerminateThread: + tid, _ := e.Params.GetTid() + addr := e.GetParamAsString(params.StartAddress) return printSummary(e, fmt.Sprintf("terminated a thread with %d id at %s address", tid, addr)) - case ktypes.OpenThread: - access := e.GetParamAsString(kparams.DesiredAccess) - exe, _ := e.Kparams.GetString(kparams.Exe) + case OpenThread: + access := e.GetParamAsString(params.DesiredAccess) + exe, _ := e.Params.GetString(params.Exe) return printSummary(e, fmt.Sprintf("opened %s process' thread object with %s access right(s)", exe, access)) - case ktypes.LoadImage: - filename := e.GetParamAsString(kparams.FilePath) + case LoadImage: + filename := e.GetParamAsString(params.FilePath) return printSummary(e, fmt.Sprintf("loaded %s module", filename)) - case ktypes.UnloadImage: - filename := e.GetParamAsString(kparams.FilePath) + case UnloadImage: + filename := e.GetParamAsString(params.FilePath) return printSummary(e, fmt.Sprintf("unloaded %s module", filename)) - case ktypes.CreateFile: - op := e.GetParamAsString(kparams.FileOperation) - filename := e.GetParamAsString(kparams.FilePath) + case CreateFile: + op := e.GetParamAsString(params.FileOperation) + filename := e.GetParamAsString(params.FilePath) return printSummary(e, fmt.Sprintf("%sed a file %s", strings.ToLower(op), filename)) - case ktypes.ReadFile: - filename := e.GetParamAsString(kparams.FilePath) - size, _ := e.Kparams.GetUint32(kparams.FileIoSize) + case ReadFile: + filename := e.GetParamAsString(params.FilePath) + size, _ := e.Params.GetUint32(params.FileIoSize) return printSummary(e, fmt.Sprintf("read %d bytes from %s file", size, filename)) - case ktypes.WriteFile: - filename := e.GetParamAsString(kparams.FilePath) - size, _ := e.Kparams.GetUint32(kparams.FileIoSize) + case WriteFile: + filename := e.GetParamAsString(params.FilePath) + size, _ := e.Params.GetUint32(params.FileIoSize) return printSummary(e, fmt.Sprintf("wrote %d bytes to %s file", size, filename)) - case ktypes.SetFileInformation: - filename := e.GetParamAsString(kparams.FilePath) - class := e.GetParamAsString(kparams.FileInfoClass) + case SetFileInformation: + filename := e.GetParamAsString(params.FilePath) + class := e.GetParamAsString(params.FileInfoClass) return printSummary(e, fmt.Sprintf("set %s information class on %s file", class, filename)) - case ktypes.DeleteFile: - filename := e.GetParamAsString(kparams.FilePath) + case DeleteFile: + filename := e.GetParamAsString(params.FilePath) return printSummary(e, fmt.Sprintf("deleted %s file", filename)) - case ktypes.RenameFile: - filename := e.GetParamAsString(kparams.FilePath) + case RenameFile: + filename := e.GetParamAsString(params.FilePath) return printSummary(e, fmt.Sprintf("renamed %s file", filename)) - case ktypes.CloseFile: - filename := e.GetParamAsString(kparams.FilePath) + case CloseFile: + filename := e.GetParamAsString(params.FilePath) return printSummary(e, fmt.Sprintf("closed %s file", filename)) - case ktypes.EnumDirectory: - filename := e.GetParamAsString(kparams.FilePath) + case EnumDirectory: + filename := e.GetParamAsString(params.FilePath) return printSummary(e, fmt.Sprintf("enumerated %s directory", filename)) - case ktypes.RegCreateKey: - key := e.GetParamAsString(kparams.RegPath) + case RegCreateKey: + key := e.GetParamAsString(params.RegPath) return printSummary(e, fmt.Sprintf("created %s key", key)) - case ktypes.RegOpenKey: - key := e.GetParamAsString(kparams.RegPath) + case RegOpenKey: + key := e.GetParamAsString(params.RegPath) return printSummary(e, fmt.Sprintf("opened %s key", key)) - case ktypes.RegDeleteKey: - key := e.GetParamAsString(kparams.RegPath) + case RegDeleteKey: + key := e.GetParamAsString(params.RegPath) return printSummary(e, fmt.Sprintf("deleted %s key", key)) - case ktypes.RegQueryKey: - key := e.GetParamAsString(kparams.RegPath) + case RegQueryKey: + key := e.GetParamAsString(params.RegPath) return printSummary(e, fmt.Sprintf("queried %s key", key)) - case ktypes.RegSetValue: - key := e.GetParamAsString(kparams.RegPath) - val, err := e.Kparams.GetString(kparams.RegValue) + case RegSetValue: + key := e.GetParamAsString(params.RegPath) + val, err := e.Params.GetString(params.RegValue) if err != nil { return printSummary(e, fmt.Sprintf("set %s value", key)) } return printSummary(e, fmt.Sprintf("set %s payload in %s value", val, key)) - case ktypes.RegDeleteValue: - key := e.GetParamAsString(kparams.RegPath) + case RegDeleteValue: + key := e.GetParamAsString(params.RegPath) return printSummary(e, fmt.Sprintf("deleted %s value", key)) - case ktypes.RegQueryValue: - key := e.GetParamAsString(kparams.RegPath) + case RegQueryValue: + key := e.GetParamAsString(params.RegPath) return printSummary(e, fmt.Sprintf("queried %s value", key)) - case ktypes.AcceptTCPv4, ktypes.AcceptTCPv6: - ip, _ := e.Kparams.GetIP(kparams.NetSIP) - port, _ := e.Kparams.GetUint16(kparams.NetSport) + case AcceptTCPv4, AcceptTCPv6: + ip, _ := e.Params.GetIP(params.NetSIP) + port, _ := e.Params.GetUint16(params.NetSport) return printSummary(e, fmt.Sprintf("accepted connection from %v and %d port", ip, port)) - case ktypes.ConnectTCPv4, ktypes.ConnectTCPv6: - ip, _ := e.Kparams.GetIP(kparams.NetDIP) - port, _ := e.Kparams.GetUint16(kparams.NetDport) + case ConnectTCPv4, ConnectTCPv6: + ip, _ := e.Params.GetIP(params.NetDIP) + port, _ := e.Params.GetUint16(params.NetDport) return printSummary(e, fmt.Sprintf("connected to %v and %d port", ip, port)) - case ktypes.SendTCPv4, ktypes.SendTCPv6, ktypes.SendUDPv4, ktypes.SendUDPv6: - ip, _ := e.Kparams.GetIP(kparams.NetDIP) - port, _ := e.Kparams.GetUint16(kparams.NetDport) - size, _ := e.Kparams.GetUint32(kparams.NetSize) + case SendTCPv4, SendTCPv6, SendUDPv4, SendUDPv6: + ip, _ := e.Params.GetIP(params.NetDIP) + port, _ := e.Params.GetUint16(params.NetDport) + size, _ := e.Params.GetUint32(params.NetSize) return printSummary(e, fmt.Sprintf("sent %d bytes to %v and %d port", size, ip, port)) - case ktypes.RecvTCPv4, ktypes.RecvTCPv6, ktypes.RecvUDPv4, ktypes.RecvUDPv6: - ip, _ := e.Kparams.GetIP(kparams.NetSIP) - port, _ := e.Kparams.GetUint16(kparams.NetSport) - size, _ := e.Kparams.GetUint32(kparams.NetSize) + case RecvTCPv4, RecvTCPv6, RecvUDPv4, RecvUDPv6: + ip, _ := e.Params.GetIP(params.NetSIP) + port, _ := e.Params.GetUint16(params.NetSport) + size, _ := e.Params.GetUint32(params.NetSize) return printSummary(e, fmt.Sprintf("received %d bytes from %v and %d port", size, ip, port)) - case ktypes.CreateHandle: - handleType := e.GetParamAsString(kparams.HandleObjectTypeID) - handleName := e.GetParamAsString(kparams.HandleObjectName) + case CreateHandle: + handleType := e.GetParamAsString(params.HandleObjectTypeID) + handleName := e.GetParamAsString(params.HandleObjectName) return printSummary(e, fmt.Sprintf("created %s handle of %s type", handleName, handleType)) - case ktypes.CloseHandle: - handleType := e.GetParamAsString(kparams.HandleObjectTypeID) - handleName := e.GetParamAsString(kparams.HandleObjectName) + case CloseHandle: + handleType := e.GetParamAsString(params.HandleObjectTypeID) + handleName := e.GetParamAsString(params.HandleObjectName) return printSummary(e, fmt.Sprintf("closed %s handle of %s type", handleName, handleType)) - case ktypes.VirtualAlloc: - addr := e.GetParamAsString(kparams.MemBaseAddress) + case VirtualAlloc: + addr := e.GetParamAsString(params.MemBaseAddress) return printSummary(e, fmt.Sprintf("allocated memory at %s address", addr)) - case ktypes.VirtualFree: - addr := e.GetParamAsString(kparams.MemBaseAddress) + case VirtualFree: + addr := e.GetParamAsString(params.MemBaseAddress) return printSummary(e, fmt.Sprintf("released memory at %s address", addr)) - case ktypes.MapViewFile: - sec := e.GetParamAsString(kparams.FileViewSectionType) + case MapViewFile: + sec := e.GetParamAsString(params.FileViewSectionType) return printSummary(e, fmt.Sprintf("mapped view of %s section", sec)) - case ktypes.UnmapViewFile: - sec := e.GetParamAsString(kparams.FileViewSectionType) + case UnmapViewFile: + sec := e.GetParamAsString(params.FileViewSectionType) return printSummary(e, fmt.Sprintf("unmapped view of %s section", sec)) - case ktypes.DuplicateHandle: - handleType := e.GetParamAsString(kparams.HandleObjectTypeID) + case DuplicateHandle: + handleType := e.GetParamAsString(params.HandleObjectTypeID) return printSummary(e, fmt.Sprintf("duplicated %s handle", handleType)) - case ktypes.QueryDNS: - dnsName := e.GetParamAsString(kparams.DNSName) + case QueryDNS: + dnsName := e.GetParamAsString(params.DNSName) return printSummary(e, fmt.Sprintf("sent %s DNS query", dnsName)) - case ktypes.ReplyDNS: - dnsName := e.GetParamAsString(kparams.DNSName) + case ReplyDNS: + dnsName := e.GetParamAsString(params.DNSName) return printSummary(e, fmt.Sprintf("received DNS response for %s query", dnsName)) - case ktypes.CreateSymbolicLinkObject: - src := e.GetParamAsString(kparams.LinkSource) - target := e.GetParamAsString(kparams.LinkTarget) + case CreateSymbolicLinkObject: + src := e.GetParamAsString(params.LinkSource) + target := e.GetParamAsString(params.LinkTarget) return printSummary(e, fmt.Sprintf("created symbolic link from %s to %s", src, target)) - case ktypes.SubmitThreadpoolWork: + case SubmitThreadpoolWork: return printSummary(e, "enqueued the work item to the thread pool") - case ktypes.SubmitThreadpoolCallback: + case SubmitThreadpoolCallback: return printSummary(e, "Submitted the thread pool callback for execution within the work item") - case ktypes.SetThreadpoolTimer: + case SetThreadpoolTimer: return printSummary(e, "set thread pool timer object") } return "" } -func printSummary(e *Kevent, text string) string { +func printSummary(e *Event, text string) string { ps := e.PS if ps != nil { return fmt.Sprintf("%s %s", ps.Name, text) diff --git a/pkg/kevent/kevent_windows_test.go b/pkg/event/event_windows_test.go similarity index 59% rename from pkg/kevent/kevent_windows_test.go rename to pkg/event/event_windows_test.go index 4347b3649..573c2c511 100644 --- a/pkg/kevent/kevent_windows_test.go +++ b/pkg/event/event_windows_test.go @@ -16,12 +16,11 @@ * limitations under the License. */ -package kevent +package event import ( + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/fs" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -29,37 +28,37 @@ import ( "time" ) -func TestKeventIsNetworkTCP(t *testing.T) { - e1 := Kevent{Type: ktypes.AcceptTCPv4, Category: ktypes.Net} - e2 := Kevent{Type: ktypes.SendUDPv6, Category: ktypes.Net} +func TestEventIsNetworkTCP(t *testing.T) { + e1 := Event{Type: AcceptTCPv4, Category: Net} + e2 := Event{Type: SendUDPv6, Category: Net} assert.True(t, e1.IsNetworkTCP()) assert.False(t, e2.IsNetworkTCP()) } -func TestKeventIsNetworkUDP(t *testing.T) { - e1 := Kevent{Type: ktypes.RecvUDPv4} - e2 := Kevent{Type: ktypes.SendTCPv6} +func TestEventIsNetworkUDP(t *testing.T) { + e1 := Event{Type: RecvUDPv4} + e2 := Event{Type: SendTCPv6} assert.True(t, e1.IsNetworkUDP()) assert.False(t, e2.IsNetworkUDP()) } -func TestKeventSummary(t *testing.T) { - kevt := &Kevent{ - Type: ktypes.CreateFile, +func TestEventSummary(t *testing.T) { + evt := &Event{ + Type: CreateFile, Tid: 2484, PID: 859, CPU: 1, Seq: 2, Name: "CreateFile", Timestamp: time.Now(), - Category: ktypes.File, + Category: File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", - Kparams: Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.Enum, Value: uint32(1), Enum: fs.FileCreateDispositions}, + Params: Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.Enum, Value: uint32(1), Enum: fs.FileCreateDispositions}, }, PS: &pstypes.PS{ PID: 2436, @@ -85,30 +84,30 @@ func TestKeventSummary(t *testing.T) { }, } - require.Equal(t, "firefox.exe opened a file C:\\Windows\\system32\\user32.dll", kevt.Summary()) - kevt.PS = nil - require.Equal(t, "process with 859 id opened a file C:\\Windows\\system32\\user32.dll", kevt.Summary()) + require.Equal(t, "firefox.exe opened a file C:\\Windows\\system32\\user32.dll", evt.Summary()) + evt.PS = nil + require.Equal(t, "process with 859 id opened a file C:\\Windows\\system32\\user32.dll", evt.Summary()) } func TestPartialKey(t *testing.T) { var tests = []struct { - evt *Kevent + evt *Event key uint64 }{ { - &Kevent{Type: ktypes.OpenProcess, PID: 1234, Kparams: Kparams{kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(1221)}, kparams.DesiredAccess: {Name: kparams.DesiredAccess, Type: kparams.Uint32, Value: uint32(5)}}}, + &Event{Type: OpenProcess, PID: 1234, Params: Params{params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1221)}, params.DesiredAccess: {Name: params.DesiredAccess, Type: params.Uint32, Value: uint32(5)}}}, 0x99c, }, { - &Kevent{Type: ktypes.OpenThread, PID: 11234, Kparams: Kparams{kparams.ThreadID: {Name: kparams.ThreadID, Type: kparams.TID, Value: uint32(8452)}, kparams.DesiredAccess: {Name: kparams.DesiredAccess, Type: kparams.Uint32, Value: uint32(15)}}}, + &Event{Type: OpenThread, PID: 11234, Params: Params{params.ThreadID: {Name: params.ThreadID, Type: params.TID, Value: uint32(8452)}, params.DesiredAccess: {Name: params.DesiredAccess, Type: params.Uint32, Value: uint32(15)}}}, 0x4cf5, }, { - &Kevent{Type: ktypes.CreateFile, PID: 4321, Kparams: Kparams{kparams.FilePath: {Name: kparams.FilePath, Type: kparams.DOSPath, Value: "C:\\Windows\\System32\\kernelbase.dll"}}}, + &Event{Type: CreateFile, PID: 4321, Params: Params{params.FilePath: {Name: params.FilePath, Type: params.DOSPath, Value: "C:\\Windows\\System32\\kernelbase.dll"}}}, 0x7ec254f31df879ec, }, { - &Kevent{Type: ktypes.CreateFile, PID: 4321, Kparams: Kparams{kparams.FilePath: {Name: kparams.FilePath, Type: kparams.DOSPath, Value: "C:\\Windows\\System32\\kernel32.dll"}}}, + &Event{Type: CreateFile, PID: 4321, Params: Params{params.FilePath: {Name: params.FilePath, Type: params.DOSPath, Value: "C:\\Windows\\System32\\kernel32.dll"}}}, 0xb6380d9159ccd174, }, } diff --git a/pkg/kevent/ktypes/eventset.go b/pkg/event/eventset.go similarity index 93% rename from pkg/kevent/ktypes/eventset.go rename to pkg/event/eventset.go index a8e62333f..614029027 100644 --- a/pkg/kevent/ktypes/eventset.go +++ b/pkg/event/eventset.go @@ -16,7 +16,7 @@ * limitations under the License. */ -package ktypes +package event import ( "fmt" @@ -34,13 +34,13 @@ type EventsetMasks struct { } // Set puts a new event type into the bitset. -func (e *EventsetMasks) Set(ktype Ktype) { - g := ktype.GUID() +func (e *EventsetMasks) Set(typ Type) { + g := typ.GUID() i := e.bitsetIndex(g) if i < 0 { panic(fmt.Sprintf("invalid event bitset index: %s", g.String())) } - e.masks[e.bitsetIndex(ktype.GUID())].Set(uint(ktype.HookID())) + e.masks[e.bitsetIndex(typ.GUID())].Set(uint(typ.HookID())) } // Test checks if the given provider GUID and diff --git a/pkg/kevent/ktypes/eventset_test.go b/pkg/event/eventset_test.go similarity index 98% rename from pkg/kevent/ktypes/eventset_test.go rename to pkg/event/eventset_test.go index 4d0f35fd3..89d483523 100644 --- a/pkg/kevent/ktypes/eventset_test.go +++ b/pkg/event/eventset_test.go @@ -16,7 +16,7 @@ * limitations under the License. */ -package ktypes +package event import ( "github.com/rabbitstack/fibratus/pkg/sys/etw" @@ -63,7 +63,7 @@ func BenchmarkEventsetMasks(b *testing.B) { func BenchmarkStdlibMap(b *testing.B) { b.ReportAllocs() - evts := make(map[Ktype]bool) + evts := make(map[Type]bool) evts[TerminateThread] = true evts[CreateThread] = true evts[TerminateProcess] = true diff --git a/pkg/kevent/flags.go b/pkg/event/flags.go similarity index 99% rename from pkg/kevent/flags.go rename to pkg/event/flags.go index c2065b57a..cb8b45c1a 100644 --- a/pkg/kevent/flags.go +++ b/pkg/event/flags.go @@ -16,7 +16,7 @@ * limitations under the License. */ -package kevent +package event import ( "github.com/rabbitstack/fibratus/pkg/sys" diff --git a/pkg/kevent/flags_test.go b/pkg/event/flags_test.go similarity index 98% rename from pkg/kevent/flags_test.go rename to pkg/event/flags_test.go index 2abd116f0..48e6351dd 100644 --- a/pkg/kevent/flags_test.go +++ b/pkg/event/flags_test.go @@ -16,7 +16,7 @@ * limitations under the License. */ -package kevent +package event import "testing" diff --git a/pkg/kevent/formatter.go b/pkg/event/formatter.go similarity index 72% rename from pkg/kevent/formatter.go rename to pkg/event/formatter.go index 0c4c8a8b0..3acd05e6e 100644 --- a/pkg/kevent/formatter.go +++ b/pkg/event/formatter.go @@ -16,7 +16,7 @@ * limitations under the License. */ -package kevent +package event import ( "fmt" @@ -33,29 +33,29 @@ const ( // endTag represents the trailing tag surrounding field name endTag = "}}" - seq = ".Seq" - ts = ".Timestamp" - pid = ".Pid" - ppid = ".Ppid" - pexe = ".Pexe" - pcmd = ".Pcmd" - pproc = ".Pname" - cwd = ".Cwd" - exe = ".Exe" - cmd = ".Cmd" - tid = ".Tid" - sid = ".Sid" - proc = ".Process" - cat = ".Category" - desc = ".Description" - cpu = ".CPU" - typ = ".Type" - kparameters = ".Kparams" - meta = ".Meta" - host = ".Host" - pe = ".PE" - kparsAccessor = ".Kparams." - cstack = ".Callstack" + seq = ".Seq" + ts = ".Timestamp" + pid = ".Pid" + ppid = ".Ppid" + pexe = ".Pexe" + pcmd = ".Pcmd" + pproc = ".Pname" + cwd = ".Cwd" + exe = ".Exe" + cmd = ".Cmd" + tid = ".Tid" + sid = ".Sid" + proc = ".Process" + cat = ".Category" + desc = ".Description" + cpu = ".CPU" + typ = ".Type" + parameters = ".Params" + meta = ".Meta" + host = ".Host" + pe = ".PE" + parsAccessor = ".Params." + cstack = ".Callstack" ) var ( @@ -64,38 +64,38 @@ var ( // tmplNormRegepx defines the regular expression for normalizing the template. This basically consists in removing // the brackets and trailing/leading spaces from the field name. tmplNormRegexp = regexp.MustCompile(`({{2}\s*([A-Za-z.]+)\s*}{2})`) - // tmplExpandKparamsRegexp determines whether Kparams. fields are expanded - tmplExpandKparamsRegexp = regexp.MustCompile(`{{\s*.Kparams.\S+}}`) + // tmplExpandKparamsRegexp determines whether Params. fields are expanded + tmplExpandKparamsRegexp = regexp.MustCompile(`{{\s*.Params.\S+}}`) ) -var kfields = map[string]bool{ - seq: true, - ts: true, - pid: true, - ppid: true, - pexe: true, - pcmd: true, - pproc: true, - cwd: true, - exe: true, - cmd: true, - tid: true, - sid: true, - proc: true, - cat: true, - desc: true, - cpu: true, - typ: true, - kparameters: true, - meta: true, - host: true, - pe: true, - cstack: true, +var fields = map[string]bool{ + seq: true, + ts: true, + pid: true, + ppid: true, + pexe: true, + pcmd: true, + pproc: true, + cwd: true, + exe: true, + cmd: true, + tid: true, + sid: true, + proc: true, + cat: true, + desc: true, + cpu: true, + typ: true, + parameters: true, + meta: true, + host: true, + pe: true, + cstack: true, } func hintFields() string { - s := make([]string, 0, len(kfields)) - for field := range kfields { + s := make([]string, 0, len(fields)) + for field := range fields { s = append(s, field) } sort.Slice(s, func(i, j int) bool { return s[i] < s[j] }) @@ -112,23 +112,23 @@ type Formatter struct { func NewFormatter(template string) (*Formatter, error) { // check basic template format and ensure all fields // defined in the template are known to us - fields := tmplRegexp.FindAllStringSubmatch(template, -1) - if len(fields) == 0 { + flds := tmplRegexp.FindAllStringSubmatch(template, -1) + if len(flds) == 0 { return nil, fmt.Errorf("invalid template format: %q", template) } if ok, pos := isTemplateBalanced(template); !ok { return nil, fmt.Errorf("template syntax error near field #%d: %q", pos, template) } - for i, field := range fields { + for i, field := range flds { if len(field) > 0 { name := sanitize(field[0]) - if strings.HasPrefix(name, kparsAccessor) { + if strings.HasPrefix(name, parsAccessor) { continue } if name == "" { return nil, fmt.Errorf("empty field found at position %d", i+1) } - if _, ok := kfields[name]; !ok { + if _, ok := fields[name]; !ok { return nil, fmt.Errorf("%s is not a known field name. Maybe you meant one "+ "of the following fields: %s", name, hintFields()) } diff --git a/pkg/kevent/formatter_test.go b/pkg/event/formatter_test.go similarity index 79% rename from pkg/kevent/formatter_test.go rename to pkg/event/formatter_test.go index 46198f20f..044a4c128 100644 --- a/pkg/kevent/formatter_test.go +++ b/pkg/event/formatter_test.go @@ -16,14 +16,14 @@ * limitations under the License. */ -package kevent +package event import ( htypes "github.com/rabbitstack/fibratus/pkg/handle/types" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" "github.com/stretchr/testify/assert" - kpars "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + kpars "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/stretchr/testify/require" "testing" ) @@ -31,7 +31,7 @@ import ( func TestTemplateUnknownField(t *testing.T) { template := "{{ .Seq }} {{NUllField1}} {{.Type}}" _, err := NewFormatter(template) - require.Error(t, err, "NUllField1 is not a known field name. Maybe you meant one of the following fields: .CPU .Category .Cmd .Cwd .Description .Exe .Handles .Host .Kparams .Meta .Pid .Ppid .Process .Seq .Sid .Tid .Timestamp .Type") + require.Error(t, err, "NUllField1 is not a known field name. Maybe you meant one of the following fields: .CPU .Category .Cmd .Cwd .Description .Exe .Handles .Host .Params .Meta .Pid .Ppid .Process .Seq .Sid .Tid .Timestamp .Type") } func TestTemplateEmptyField(t *testing.T) { @@ -39,7 +39,7 @@ func TestTemplateEmptyField(t *testing.T) { _, err := NewFormatter(template) require.Error(t, err, "empty field found at position 2") - template1 := "{{ .Seq }} {{.CPU}} - ({{.Type}}) -- pid: {{}} {{ .Kparams.Pid }} ({{.Kparams}}) {{ .Meta }}" + template1 := "{{ .Seq }} {{.CPU}} - ({{.Type}}) -- pid: {{}} {{ .Params.Pid }} ({{.Params}}) {{ .Meta }}" _, err = NewFormatter(template1) require.Error(t, err, "empty field found at position 4") } @@ -51,13 +51,13 @@ func TestTemplateSyntaxError(t *testing.T) { } func TestFormat(t *testing.T) { - template := "{{ .Seq }} {{.CPU}} - ({{.Type}}) -- pid: {{ .Kparams.Pid }} ({{.Kparams}}) {{ .Meta }}" + template := "{{ .Seq }} {{.CPU}} - ({{.Type}}) -- pid: {{ .Params.Pid }} ({{.Params}}) {{ .Meta }}" f, err := NewFormatter(template) require.NoError(t, err) - params := Kparams{ + params := Params{ kpars.ProcessID: {Name: kpars.ProcessID, Type: kpars.PID, Value: uint32(876)}, } - s := f.Format(&Kevent{CPU: uint8(4), Name: "CreateProcess", Seq: uint64(1999), Kparams: params, Metadata: map[MetadataKey]any{"key1": "value1"}}) + s := f.Format(&Event{CPU: uint8(4), Name: "CreateProcess", Seq: uint64(1999), Params: params, Metadata: map[MetadataKey]any{"key1": "value1"}}) assert.Equal(t, "1999 4 - (CreateProcess) -- pid: 876 (pidâžœ 876) key1: value1", string(s)) } @@ -65,14 +65,14 @@ func TestFormatPS(t *testing.T) { template := "{{ .Seq }} {{ .Process }} ({{ .Cwd }}) {{ .Ppid }} ({{ .Sid }})" f, err := NewFormatter(template) require.NoError(t, err) - params := Kparams{ + params := Params{ kpars.ProcessID: {Name: kpars.ProcessID, Type: kpars.PID, Value: uint32(876)}, } - s := f.Format(&Kevent{ - CPU: uint8(4), - Name: "CreateProcess", - Seq: uint64(1999), - Kparams: params, + s := f.Format(&Event{ + CPU: uint8(4), + Name: "CreateProcess", + Seq: uint64(1999), + Params: params, PS: &pstypes.PS{ Name: "cmd.exe", Cwd: "C:/Windows/System32", @@ -112,15 +112,15 @@ func TestIsTemplateBalanced(t *testing.T) { require.False(t, ok) assert.Equal(t, 2, pos) - ok, pos = isTemplateBalanced("{{{ .Seq }} {{.CPU}} {{} {{ .Kparams }} { .Kparams.pid}}") + ok, pos = isTemplateBalanced("{{{ .Seq }} {{.CPU}} {{} {{ .Params }} { .Params.pid}}") require.False(t, ok) assert.Equal(t, 1, pos) - ok, pos = isTemplateBalanced("{{ .Seq }} {{.CPU}} {{} {{ .Kparams }} { .Kparams.pid}}") + ok, pos = isTemplateBalanced("{{ .Seq }} {{.CPU}} {{} {{ .Params }} { .Params.pid}}") require.False(t, ok) assert.Equal(t, 3, pos) - ok, pos = isTemplateBalanced("({{ .Seq }}) {{.CPU}} {{}} {{ .Kparams }} { .Kparams.pid}}") + ok, pos = isTemplateBalanced("({{ .Seq }}) {{.CPU}} {{}} {{ .Params }} { .Params.pid}}") require.False(t, ok) assert.Equal(t, 5, pos) @@ -132,7 +132,7 @@ func TestIsTemplateBalanced(t *testing.T) { require.False(t, ok) assert.Equal(t, 3, pos) - ok, pos = isTemplateBalanced("{{ .Seq }} {{.CPU}} - ({{.Type}}) -- pid: {{]} {{ .Kparams.Pid }} ({{.Kparams}}) {{ .Meta }}") + ok, pos = isTemplateBalanced("{{ .Seq }} {{.CPU}} - ({{.Type}}) -- pid: {{]} {{ .Params.Pid }} ({{.Params}}) {{ .Meta }}") require.False(t, ok) assert.Equal(t, 4, pos) } diff --git a/pkg/kevent/formatter_windows.go b/pkg/event/formatter_windows.go similarity index 66% rename from pkg/kevent/formatter_windows.go rename to pkg/event/formatter_windows.go index 2692fe7d5..7369c0f3e 100644 --- a/pkg/kevent/formatter_windows.go +++ b/pkg/event/formatter_windows.go @@ -16,33 +16,33 @@ * limitations under the License. */ -package kevent +package event import ( "strconv" ) // Format applies the template on the provided kernel event. -func (f *Formatter) Format(kevt *Kevent) []byte { - if kevt == nil { +func (f *Formatter) Format(evt *Event) []byte { + if evt == nil { return []byte{} } values := map[string]interface{}{ - ts: kevt.Timestamp.String(), - pid: strconv.FormatUint(uint64(kevt.PID), 10), - tid: strconv.FormatUint(uint64(kevt.Tid), 10), - seq: strconv.FormatUint(kevt.Seq, 10), - cpu: strconv.FormatUint(uint64(kevt.CPU), 10), - typ: kevt.Name, - cat: kevt.Category, - desc: kevt.Description, - host: kevt.Host, - meta: kevt.Metadata.String(), - kparameters: kevt.Kparams.String(), + ts: evt.Timestamp.String(), + pid: strconv.FormatUint(uint64(evt.PID), 10), + tid: strconv.FormatUint(uint64(evt.Tid), 10), + seq: strconv.FormatUint(evt.Seq, 10), + cpu: strconv.FormatUint(uint64(evt.CPU), 10), + typ: evt.Name, + cat: evt.Category, + desc: evt.Description, + host: evt.Host, + meta: evt.Metadata.String(), + parameters: evt.Params.String(), } // add process metadata - ps := kevt.PS + ps := evt.PS if ps != nil { values[proc] = ps.Name values[ppid] = strconv.FormatUint(uint64(ps.Ppid), 10) @@ -61,15 +61,15 @@ func (f *Formatter) Format(kevt *Kevent) []byte { } } // add callstack summary - if !kevt.Callstack.IsEmpty() { - values[cstack] = kevt.Callstack.String() + if !evt.Callstack.IsEmpty() { + values[cstack] = evt.Callstack.String() } if f.expandKparamsDot { // expand all parameters into the map, so we can ask // for specific parameter names in the template - for _, kpar := range kevt.Kparams { - values[".Kparams."+caser.String(kpar.Name)] = kpar.String() + for _, par := range evt.Params { + values[".Params."+caser.String(par.Name)] = par.String() } } diff --git a/pkg/kevent/marshaller.go b/pkg/event/marshaller.go similarity index 99% rename from pkg/kevent/marshaller.go rename to pkg/event/marshaller.go index d3ed80d89..bcaec4aae 100644 --- a/pkg/kevent/marshaller.go +++ b/pkg/event/marshaller.go @@ -16,7 +16,7 @@ * limitations under the License. */ -package kevent +package event import ( "math" diff --git a/pkg/kevent/marshaller_test.go b/pkg/event/marshaller_test.go similarity index 71% rename from pkg/kevent/marshaller_test.go rename to pkg/event/marshaller_test.go index 059266ebb..3e4773906 100644 --- a/pkg/kevent/marshaller_test.go +++ b/pkg/event/marshaller_test.go @@ -16,20 +16,19 @@ * limitations under the License. */ -package kevent +package event import ( "encoding/json" - kcapver "github.com/rabbitstack/fibratus/pkg/kcap/version" + capver "github.com/rabbitstack/fibratus/pkg/cap/version" "github.com/rabbitstack/fibratus/pkg/util/va" "golang.org/x/sys/windows" "os" "testing" "time" + "github.com/rabbitstack/fibratus/pkg/event/params" htypes "github.com/rabbitstack/fibratus/pkg/handle/types" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" pex "github.com/rabbitstack/fibratus/pkg/pe" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" "github.com/stretchr/testify/assert" @@ -48,55 +47,55 @@ func TestMarshaller(t *testing.T) { now, err := time.Parse(time.RFC3339Nano, time.Now().Format(time.RFC3339Nano)) require.NoError(t, err) - kevt := &Kevent{ - Type: ktypes.CreateFile, + evt := &Event{ + Type: CreateFile, Tid: 2484, PID: 859, CPU: 1, Seq: 2, Name: "CreateFile", Timestamp: now, - Category: ktypes.File, + Category: File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", - Kparams: Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, - kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, - kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, - kparams.KstackLimit: {Name: kparams.KstackLimit, Type: kparams.Address, Value: uint64(1888833888)}, - kparams.StartTime: {Name: kparams.StartTime, Type: kparams.Time, Value: time.Now()}, - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(1204)}, - kparams.NetDIPNames: {Name: kparams.NetDIPNames, Type: kparams.Slice, Value: []string{"dns.google.", "github.com."}}, + Params: Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.AnsiString, Value: "open"}, + params.BasePrio: {Name: params.BasePrio, Type: params.Int8, Value: int8(2)}, + params.PagePrio: {Name: params.PagePrio, Type: params.Uint8, Value: uint8(2)}, + params.KstackLimit: {Name: params.KstackLimit, Type: params.Address, Value: uint64(1888833888)}, + params.StartTime: {Name: params.StartTime, Type: params.Time, Value: time.Now()}, + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1204)}, + params.NetDIPNames: {Name: params.NetDIPNames, Type: params.Slice, Value: []string{"dns.google.", "github.com."}}, }, Metadata: map[MetadataKey]any{"foo": "bar", "fooz": "barzz"}, } - b := kevt.MarshalRaw() + b := evt.MarshalRaw() require.NotEmpty(t, b) - clone, err := NewFromKcap(b, kcapver.KevtSecV2) + clone, err := NewFromCapture(b, capver.EvtSecV2) require.NoError(t, err) assert.Equal(t, uint64(2), clone.Seq) assert.Equal(t, uint32(859), clone.PID) assert.Equal(t, uint32(2484), clone.Tid) - assert.Equal(t, ktypes.CreateFile, clone.Type) + assert.Equal(t, CreateFile, clone.Type) assert.Equal(t, uint8(1), clone.CPU) assert.Equal(t, "CreateFile", clone.Name) - assert.Equal(t, ktypes.File, clone.Category) + assert.Equal(t, File, clone.Category) assert.Equal(t, "Creates or opens a new file, directory, I/O device, pipe, console", clone.Description) assert.Equal(t, "archrabbit", clone.Host) assert.Equal(t, now, clone.Timestamp) - assert.Len(t, clone.Kparams, 10) + assert.Len(t, clone.Params, 10) - filename, err := clone.Kparams.GetString(kparams.FilePath) + filename, err := clone.Params.GetString(params.FilePath) require.NoError(t, err) assert.Equal(t, "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll", filename) - fileobject, err := clone.Kparams.GetUint64(kparams.FileObject) + fileobject, err := clone.Params.GetUint64(params.FileObject) require.NoError(t, err) assert.Equal(t, uint64(12456738026482168384), fileobject) @@ -107,25 +106,25 @@ func TestMarshaller(t *testing.T) { } func TestKeventMarshalJSON(t *testing.T) { - kevt := &Kevent{ - Type: ktypes.CreateFile, + evt := &Event{ + Type: CreateFile, Tid: 2484, PID: 859, CPU: 1, Seq: 2, Name: "CreateFile", Timestamp: time.Now(), - Category: ktypes.File, + Category: File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", - Kparams: Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, - kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, - kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, - kparams.NetDIPNames: {Name: kparams.NetDIPNames, Type: kparams.Slice, Value: []string{"dns.google.", "github.com."}}, + Params: Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.AnsiString, Value: "open"}, + params.BasePrio: {Name: params.BasePrio, Type: params.Int8, Value: int8(2)}, + params.PagePrio: {Name: params.PagePrio, Type: params.Uint8, Value: uint8(2)}, + params.NetDIPNames: {Name: params.NetDIPNames, Type: params.Slice, Value: []string{"dns.google.", "github.com."}}, }, Metadata: map[MetadataKey]any{"foo": "bar", "fooz": "baarz"}, PS: &pstypes.PS{ @@ -195,25 +194,26 @@ func TestKeventMarshalJSON(t *testing.T) { }, }, } - s := kevt.MarshalJSON() - var newKevt Kevent - err := json.Unmarshal(s, &newKevt) + + s := evt.MarshalJSON() + var newEvt Event + err := json.Unmarshal(s, &newEvt) require.NoError(t, err) - assert.Equal(t, uint32(2484), newKevt.Tid) - assert.Equal(t, uint32(859), newKevt.PID) - assert.Equal(t, "archrabbit\\SYSTEM", newKevt.PS.SID) - assert.Len(t, newKevt.PS.Envs, 2) - assert.Len(t, newKevt.PS.Handles, 3) + assert.Equal(t, uint32(2484), newEvt.Tid) + assert.Equal(t, uint32(859), newEvt.PID) + assert.Equal(t, "archrabbit\\SYSTEM", newEvt.PS.SID) + assert.Len(t, newEvt.PS.Envs, 2) + assert.Len(t, newEvt.PS.Handles, 3) - assert.NotNil(t, newKevt.PS.PE) - assert.Equal(t, "explorer.exe", newKevt.PS.Parent.Name) - assert.Equal(t, uint32(10), newKevt.PS.PE.NumberOfSymbols) - assert.Equal(t, uint16(2), newKevt.PS.PE.NumberOfSections) - assert.Len(t, newKevt.PS.PE.Sections, 2) - assert.Len(t, newKevt.PS.PE.Symbols, 5) - assert.Len(t, newKevt.PS.PE.Imports, 4) - assert.Len(t, newKevt.PS.PE.VersionResources, 3) + assert.NotNil(t, newEvt.PS.PE) + assert.Equal(t, "explorer.exe", newEvt.PS.Parent.Name) + assert.Equal(t, uint32(10), newEvt.PS.PE.NumberOfSymbols) + assert.Equal(t, uint16(2), newEvt.PS.PE.NumberOfSections) + assert.Len(t, newEvt.PS.PE.Sections, 2) + assert.Len(t, newEvt.PS.PE.Symbols, 5) + assert.Len(t, newEvt.PS.PE.Imports, 4) + assert.Len(t, newEvt.PS.PE.VersionResources, 3) } func TestUnmarshalHugeHandles(t *testing.T) { @@ -223,24 +223,24 @@ func TestUnmarshalHugeHandles(t *testing.T) { err = json.Unmarshal(b, &handles) require.NoError(t, err) - kevt := &Kevent{ - Type: ktypes.CreateProcess, + evt := &Event{ + Type: CreateProcess, Tid: 2484, PID: 859, CPU: 1, Seq: 2, Name: "CreateProcess", Timestamp: time.Now(), - Category: ktypes.File, + Category: File, Host: "archrabbit", Description: "Creates a new process", - Kparams: Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, - kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, - kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + Params: Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.AnsiString, Value: "open"}, + params.BasePrio: {Name: params.BasePrio, Type: params.Int8, Value: int8(2)}, + params.PagePrio: {Name: params.PagePrio, Type: params.Uint8, Value: uint8(2)}, }, Metadata: map[MetadataKey]any{"foo": "bar", "fooz": "baarz"}, PS: &pstypes.PS{ @@ -276,8 +276,8 @@ func TestUnmarshalHugeHandles(t *testing.T) { }, } - s := kevt.MarshalRaw() - clone, err := NewFromKcap(s, kcapver.KevtSecV2) + s := evt.MarshalRaw() + clone, err := NewFromCapture(s, capver.EvtSecV2) require.NoError(t, err) require.NotNil(t, clone) } @@ -285,24 +285,24 @@ func TestUnmarshalHugeHandles(t *testing.T) { func TestKeventMarshalJSONMultiple(t *testing.T) { for i := 0; i < 10; i++ { seq := uint64(i + 1) - kevt := &Kevent{ - Type: ktypes.CreateFile, + evt := &Event{ + Type: CreateFile, Tid: 2484, PID: 859, CPU: 1, Seq: seq, Name: "CreateFile", Timestamp: time.Now(), - Category: ktypes.File, + Category: File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", - Kparams: Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, - kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, - kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + Params: Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.AnsiString, Value: "open"}, + params.BasePrio: {Name: params.BasePrio, Type: params.Int8, Value: int8(2)}, + params.PagePrio: {Name: params.PagePrio, Type: params.Uint8, Value: uint8(2)}, }, Metadata: map[MetadataKey]any{"foo": "bar", "fooz": "baarz"}, PS: &pstypes.PS{ @@ -352,8 +352,8 @@ func TestKeventMarshalJSONMultiple(t *testing.T) { }, }, } - s := kevt.MarshalJSON() - var newKevt Kevent + s := evt.MarshalJSON() + var newKevt Event err := json.Unmarshal(s, &newKevt) require.NoError(t, err) @@ -365,24 +365,24 @@ func TestKeventMarshalJSONMultiple(t *testing.T) { } func BenchmarkKeventMarshalJSON(b *testing.B) { - kevt := &Kevent{ - Type: ktypes.CreateFile, + evt := &Event{ + Type: CreateFile, Tid: 2484, PID: 859, CPU: 1, Seq: 2, Name: "CreateFile", Timestamp: time.Now(), - Category: ktypes.File, + Category: File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", - Kparams: Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, - kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, - kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + Params: Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.AnsiString, Value: "open"}, + params.BasePrio: {Name: params.BasePrio, Type: params.Int8, Value: int8(2)}, + params.PagePrio: {Name: params.PagePrio, Type: params.Uint8, Value: uint8(2)}, }, Metadata: map[MetadataKey]any{"foo": "bar", "fooz": "baarz"}, PS: &pstypes.PS{ @@ -444,29 +444,29 @@ func BenchmarkKeventMarshalJSON(b *testing.B) { } b.ReportAllocs() for i := 0; i < b.N; i++ { - kevt.MarshalJSON() + evt.MarshalJSON() } } func BenchmarkKeventMarshalJSONStdlib(b *testing.B) { - kevt := &Kevent{ - Type: ktypes.CreateFile, + evt := &Event{ + Type: CreateFile, Tid: 2484, PID: 859, CPU: 1, Seq: 2, Name: "CreateFile", Timestamp: time.Now(), - Category: ktypes.File, + Category: File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", - Kparams: Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, - kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, - kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + Params: Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.AnsiString, Value: "open"}, + params.BasePrio: {Name: params.BasePrio, Type: params.Int8, Value: int8(2)}, + params.PagePrio: {Name: params.PagePrio, Type: params.Uint8, Value: uint8(2)}, }, Metadata: map[MetadataKey]any{"foo": "bar", "fooz": "baarz"}, PS: &pstypes.PS{ @@ -528,64 +528,64 @@ func BenchmarkKeventMarshalJSONStdlib(b *testing.B) { } b.ReportAllocs() for i := 0; i < b.N; i++ { - if _, err := json.Marshal(kevt); err != nil { + if _, err := json.Marshal(evt); err != nil { b.Fatal(err) } } } func BenchmarkMarshal(b *testing.B) { - kevt := &Kevent{ - Type: ktypes.CreateFile, + evt := &Event{ + Type: CreateFile, Tid: 2484, PID: 859, CPU: 1, Seq: 2, Name: "CreateFile", Timestamp: time.Now(), - Category: ktypes.File, + Category: File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", - Kparams: Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, + Params: Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.AnsiString, Value: "open"}, }, Metadata: map[MetadataKey]any{"foo": "bar", "fooz": "barz"}, } b.ReportAllocs() for i := 0; i < b.N; i++ { - if buf := kevt.MarshalRaw(); len(buf) == 0 { + if buf := evt.MarshalRaw(); len(buf) == 0 { b.Fatal("empty buffer") } } } func BenchmarkUnmarshal(b *testing.B) { - kevt := &Kevent{ - Type: ktypes.CreateFile, + evt := &Event{ + Type: CreateFile, Tid: 2484, PID: 859, CPU: 1, Seq: 2, Name: "CreateFile", Timestamp: time.Now(), - Category: ktypes.File, + Category: File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", - Kparams: Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, + Params: Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.AnsiString, Value: "open"}, }, Metadata: map[MetadataKey]any{"foo": "bar", "fooz": "barz"}, } - buf := kevt.MarshalRaw() + buf := evt.MarshalRaw() b.ReportAllocs() for i := 0; i < b.N; i++ { - ke, err := NewFromKcap(buf, kcapver.KevtSecV2) + ke, err := NewFromCapture(buf, capver.EvtSecV2) if err != nil { b.Fatal(err) } diff --git a/pkg/kevent/marshaller_windows.go b/pkg/event/marshaller_windows.go similarity index 74% rename from pkg/kevent/marshaller_windows.go rename to pkg/event/marshaller_windows.go index 378f9335d..5566f9329 100644 --- a/pkg/kevent/marshaller_windows.go +++ b/pkg/event/marshaller_windows.go @@ -16,7 +16,7 @@ * limitations under the License. */ -package kevent +package event import ( "expvar" @@ -29,33 +29,32 @@ import ( "time" "unsafe" - "github.com/rabbitstack/fibratus/pkg/kcap/section" - kcapver "github.com/rabbitstack/fibratus/pkg/kcap/version" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/cap/section" + capver "github.com/rabbitstack/fibratus/pkg/cap/version" + "github.com/rabbitstack/fibratus/pkg/event/params" ptypes "github.com/rabbitstack/fibratus/pkg/ps/types" "github.com/rabbitstack/fibratus/pkg/util/bytes" "github.com/rabbitstack/fibratus/pkg/util/ip" ) var ( - // SerializeHandles indicates if handles are serialized as part of the process' state + // SerializeHandles indicates if handles are serialized as part of the process state SerializeHandles bool - // SerializeThreads indicates if threads are serialized as part of the process' state + // SerializeThreads indicates if threads are serialized as part of the process state SerializeThreads bool - // SerializeImages indicates if images are serialized as part of the process' state + // SerializeImages indicates if images are serialized as part of the process state SerializeImages bool - // SerializePE indicates if PE metadata are serialized as part of the process' state + // SerializePE indicates if PE metadata are serialized as part of the process state SerializePE bool - // SerializeEnvs indicates if the environment variables are serialized as part of the process's state + // SerializeEnvs indicates if the environment variables are serialized as part of the process state SerializeEnvs bool ) // unmarshalTimestampErrors counts timestamp unmarshal errors -var unmarshalTimestampErrors = expvar.NewInt("kevent.timestamp.unmarshal.errors") +var unmarshalTimestampErrors = expvar.NewInt("event.timestamp.unmarshal.errors") // MarshalRaw produces a byte stream of the kernel event suitable for writing to disk. -func (e *Kevent) MarshalRaw() []byte { +func (e *Event) MarshalRaw() []byte { b := make([]byte, 0) // write seq, pid, tid fields @@ -90,56 +89,56 @@ func (e *Kevent) MarshalRaw() []byte { b = append(b, timestamp...) // write the number of event parameters followed by each parameter - b = append(b, bytes.WriteUint16(uint16(len(e.Kparams)))...) - for _, kpar := range e.Kparams { + b = append(b, bytes.WriteUint16(uint16(len(e.Params)))...) + for _, par := range e.Params { // append the type, parameter size and name - b = append(b, bytes.WriteUint16(uint16(kpar.KcapType()))...) - b = append(b, bytes.WriteUint16(uint16(len(kpar.Name)))...) - b = append(b, kpar.Name...) - switch kpar.Type { - case kparams.AnsiString, kparams.UnicodeString: - b = append(b, bytes.WriteUint16(uint16(len(kpar.Value.(string))))...) - b = append(b, kpar.Value.(string)...) - case kparams.Key, kparams.Path, kparams.DOSPath, kparams.HandleType: - v := e.GetParamAsString(kpar.Name) + b = append(b, bytes.WriteUint16(uint16(par.CaptureType()))...) + b = append(b, bytes.WriteUint16(uint16(len(par.Name)))...) + b = append(b, par.Name...) + switch par.Type { + case params.AnsiString, params.UnicodeString: + b = append(b, bytes.WriteUint16(uint16(len(par.Value.(string))))...) + b = append(b, par.Value.(string)...) + case params.Key, params.Path, params.DOSPath, params.HandleType: + v := e.GetParamAsString(par.Name) b = append(b, bytes.WriteUint16(uint16(len(v)))...) b = append(b, v...) - case kparams.Uint8: - b = append(b, kpar.Value.(uint8)) - case kparams.Int8: - b = append(b, byte(kpar.Value.(int8))) - case kparams.Uint16, kparams.Port: - b = append(b, bytes.WriteUint16(kpar.Value.(uint16))...) - case kparams.Int16: - b = append(b, bytes.WriteUint16(uint16(kpar.Value.(int16)))...) - case kparams.Uint32, kparams.Status, kparams.Enum, kparams.Flags: - b = append(b, bytes.WriteUint32(kpar.Value.(uint32))...) - case kparams.Int32: - b = append(b, bytes.WriteUint32(uint32(kpar.Value.(int32)))...) - case kparams.Uint64, kparams.Address, kparams.Flags64: - b = append(b, bytes.WriteUint64(kpar.Value.(uint64))...) - case kparams.Int64: - b = append(b, bytes.WriteUint64(uint64(kpar.Value.(int64)))...) - case kparams.Double: - b = append(b, bytes.WriteUint32(math.Float32bits(kpar.Value.(float32)))...) - case kparams.Float: - b = append(b, bytes.WriteUint64(math.Float64bits(kpar.Value.(float64)))...) - case kparams.IPv4: - b = append(b, kpar.Value.(net.IP).To4()...) - case kparams.IPv6: - b = append(b, kpar.Value.(net.IP).To16()...) - case kparams.PID, kparams.TID: - b = append(b, bytes.WriteUint32(kpar.Value.(uint32))...) - case kparams.Bool: - b = append(b, convert.Btoi(kpar.Value.(bool))) - case kparams.Time: - v := kpar.Value.(time.Time) + case params.Uint8: + b = append(b, par.Value.(uint8)) + case params.Int8: + b = append(b, byte(par.Value.(int8))) + case params.Uint16, params.Port: + b = append(b, bytes.WriteUint16(par.Value.(uint16))...) + case params.Int16: + b = append(b, bytes.WriteUint16(uint16(par.Value.(int16)))...) + case params.Uint32, params.Status, params.Enum, params.Flags: + b = append(b, bytes.WriteUint32(par.Value.(uint32))...) + case params.Int32: + b = append(b, bytes.WriteUint32(uint32(par.Value.(int32)))...) + case params.Uint64, params.Address, params.Flags64: + b = append(b, bytes.WriteUint64(par.Value.(uint64))...) + case params.Int64: + b = append(b, bytes.WriteUint64(uint64(par.Value.(int64)))...) + case params.Double: + b = append(b, bytes.WriteUint32(math.Float32bits(par.Value.(float32)))...) + case params.Float: + b = append(b, bytes.WriteUint64(math.Float64bits(par.Value.(float64)))...) + case params.IPv4: + b = append(b, par.Value.(net.IP).To4()...) + case params.IPv6: + b = append(b, par.Value.(net.IP).To16()...) + case params.PID, params.TID: + b = append(b, bytes.WriteUint32(par.Value.(uint32))...) + case params.Bool: + b = append(b, convert.Btoi(par.Value.(bool))) + case params.Time: + v := par.Value.(time.Time) ts := make([]byte, 0) ts = v.AppendFormat(ts, time.RFC3339Nano) b = append(b, bytes.WriteUint16(uint16(len(ts)))...) b = append(b, ts...) - case kparams.Slice: - switch slice := kpar.Value.(type) { + case params.Slice: + switch slice := par.Value.(type) { case []string: // append the type for slice elements b = append(b, uint8('s')) @@ -156,9 +155,9 @@ func (e *Kevent) MarshalRaw() []byte { b = append(b, bytes.WriteUint64(v.Uint64())...) } } - case kparams.Binary, kparams.SID, kparams.WbemSID: - b = append(b, bytes.WriteUint32(uint32(len(kpar.Value.([]byte))))...) - b = append(b, kpar.Value.([]byte)...) + case params.Binary, params.SID, params.WbemSID: + b = append(b, bytes.WriteUint32(uint32(len(par.Value.([]byte))))...) + b = append(b, par.Value.([]byte)...) } } @@ -175,11 +174,11 @@ func (e *Kevent) MarshalRaw() []byte { // write process state if e.PS != nil && (e.IsCreateProcess() || e.IsProcessRundown()) { buf := e.PS.Marshal() - sec := section.New(section.Process, kcapver.ProcessSecV4, 0, uint32(len(buf))) + sec := section.New(section.Process, capver.ProcessSecV4, 0, uint32(len(buf))) b = append(b, sec[:]...) b = append(b, buf...) } else { - sec := section.New(section.Process, kcapver.ProcessSecV4, 0, 0) + sec := section.New(section.Process, capver.ProcessSecV4, 0, 0) b = append(b, sec[:]...) } @@ -191,7 +190,7 @@ func inc(idx, inc uint32) uint32 { } // UnmarshalRaw recovers the state of the kernel event from the byte stream. -func (e *Kevent) UnmarshalRaw(b []byte, ver kcapver.Version) error { +func (e *Event) UnmarshalRaw(b []byte, ver capver.Version) error { if len(b) < 34 { return fmt.Errorf("expected at least 34 bytes but got %d bytes", len(b)) } @@ -202,18 +201,18 @@ func (e *Kevent) UnmarshalRaw(b []byte, ver kcapver.Version) error { e.Tid = bytes.ReadUint32(b[12:]) // read type and CPU - var ktype ktypes.Ktype + var typ Type // set start index depending // on event section version var idx uint32 switch ver { - case kcapver.KevtSecV1: + case capver.EvtSecV1: idx = 33 - case kcapver.KevtSecV2: + case capver.EvtSecV2: idx = 34 } - copy(ktype[:], b[16:idx]) - e.Type = ktype + copy(typ[:], b[16:idx]) + e.Type = typ e.CPU = b[idx : idx+1][0] idx++ // increment index @@ -229,7 +228,7 @@ func (e *Kevent) UnmarshalRaw(b []byte, ver kcapver.Version) error { l = bytes.ReadUint16(b[inc(idx, 2)+offset:]) buf = b[inc(idx, 4)+offset:] offset += uint32(l) - e.Category = ktypes.Category(string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l])) + e.Category = Category(string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l])) // read description l = bytes.ReadUint16(b[inc(idx, 4)+offset:]) @@ -261,91 +260,91 @@ func (e *Kevent) UnmarshalRaw(b []byte, ver kcapver.Version) error { var poffset uint32 for i := 0; i < int(nparams); i++ { - // read kparam type + // read Param type typ := bytes.ReadUint16(b[inc(idx, 12)+offset+poffset:]) - // read kparam name + // read Param name kparamNameLength := uint32(bytes.ReadUint16(b[inc(idx, 14)+offset+poffset:])) buf = b[inc(idx, 16)+offset+poffset:] kparamName := string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:kparamNameLength:kparamNameLength]) pi := inc(idx, 16) // parameter index - var kval kparams.Value - switch kparams.Type(typ) { - case kparams.AnsiString, kparams.UnicodeString, kparams.Path: + var val params.Value + switch params.Type(typ) { + case params.AnsiString, params.UnicodeString, params.Path: // read string parameter l := bytes.ReadUint16(b[pi+offset+kparamNameLength+poffset:]) buf = b[inc(idx, 18)+offset+kparamNameLength+poffset:] if len(buf) > 0 { - kval = string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l]) + val = string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l]) } // increment parameter offset by string by type length + name length bytes + length of // the string parameter + string parameter size poffset += kparamNameLength + 6 + uint32(l) - case kparams.Uint64, kparams.Address, kparams.Flags64: - kval = bytes.ReadUint64(b[pi+offset+kparamNameLength+poffset:]) + case params.Uint64, params.Address, params.Flags64: + val = bytes.ReadUint64(b[pi+offset+kparamNameLength+poffset:]) // increment parameter offset by type length + name length sizes + size of uint64 poffset += kparamNameLength + 4 + 8 - case kparams.Int64: - kval = int64(bytes.ReadUint64(b[pi+offset+kparamNameLength+poffset:])) + case params.Int64: + val = int64(bytes.ReadUint64(b[pi+offset+kparamNameLength+poffset:])) // increment parameter offset by type length + name length sizes + size of int64 poffset += kparamNameLength + 4 + 8 - case kparams.Double: - kval = float64(bytes.ReadUint64(b[pi+offset+kparamNameLength+poffset:])) + case params.Double: + val = float64(bytes.ReadUint64(b[pi+offset+kparamNameLength+poffset:])) poffset += kparamNameLength + 4 + 8 - case kparams.Float: - kval = float32(bytes.ReadUint32(b[pi+offset+kparamNameLength+poffset:])) + case params.Float: + val = float32(bytes.ReadUint32(b[pi+offset+kparamNameLength+poffset:])) poffset += kparamNameLength + 4 + 4 - case kparams.IPv4: - kval = ip.ToIPv4(bytes.ReadUint32(b[pi+offset+kparamNameLength+poffset:])) + case params.IPv4: + val = ip.ToIPv4(bytes.ReadUint32(b[pi+offset+kparamNameLength+poffset:])) // // increment by IPv4 length poffset += kparamNameLength + 4 + 4 - case kparams.IPv6: - kval = ip.ToIPv6(b[pi+offset+kparamNameLength+poffset : pi+offset+kparamNameLength+poffset+16]) + case params.IPv6: + val = ip.ToIPv6(b[pi+offset+kparamNameLength+poffset : pi+offset+kparamNameLength+poffset+16]) // increment by IPv6 length poffset += kparamNameLength + 4 + 16 - case kparams.PID, kparams.TID: - kval = bytes.ReadUint32(b[pi+offset+kparamNameLength+poffset:]) + case params.PID, params.TID: + val = bytes.ReadUint32(b[pi+offset+kparamNameLength+poffset:]) poffset += kparamNameLength + 4 + 4 - case kparams.Int32: - kval = int32(bytes.ReadUint32(b[pi+offset+kparamNameLength+poffset:])) + case params.Int32: + val = int32(bytes.ReadUint32(b[pi+offset+kparamNameLength+poffset:])) poffset += kparamNameLength + 4 + 4 - case kparams.Uint32, kparams.Enum, kparams.Flags, kparams.Status: - kval = bytes.ReadUint32(b[pi+offset+kparamNameLength+poffset:]) + case params.Uint32, params.Enum, params.Flags, params.Status: + val = bytes.ReadUint32(b[pi+offset+kparamNameLength+poffset:]) poffset += kparamNameLength + 4 + 4 - case kparams.Uint16, kparams.Port: - kval = bytes.ReadUint16(b[pi+offset+kparamNameLength+poffset:]) + case params.Uint16, params.Port: + val = bytes.ReadUint16(b[pi+offset+kparamNameLength+poffset:]) poffset += kparamNameLength + 4 + 2 - case kparams.Int16: - kval = int16(bytes.ReadUint16(b[pi+offset+kparamNameLength+poffset:])) + case params.Int16: + val = int16(bytes.ReadUint16(b[pi+offset+kparamNameLength+poffset:])) poffset += kparamNameLength + 4 + 2 - case kparams.Uint8: - kval = b[pi+offset+kparamNameLength+poffset : pi+offset+kparamNameLength+poffset+1][0] + case params.Uint8: + val = b[pi+offset+kparamNameLength+poffset : pi+offset+kparamNameLength+poffset+1][0] poffset += kparamNameLength + 4 + 1 - case kparams.Int8: - kval = int8(b[pi+offset+kparamNameLength+poffset : pi+offset+kparamNameLength+poffset+1][0]) + case params.Int8: + val = int8(b[pi+offset+kparamNameLength+poffset : pi+offset+kparamNameLength+poffset+1][0]) poffset += kparamNameLength + 4 + 1 - case kparams.Bool: + case params.Bool: v := b[pi+offset+kparamNameLength+poffset : pi+offset+kparamNameLength+poffset+1][0] if v == 1 { - kval = true + val = true } else { - kval = false + val = false } poffset += kparamNameLength + 4 + 1 - case kparams.Time: + case params.Time: // read ts length l := bytes.ReadUint16(b[pi+offset+kparamNameLength+poffset:]) buf = b[inc(idx, 18)+offset+kparamNameLength+poffset:] if len(buf) > 0 { var err error - kval, err = time.Parse(time.RFC3339Nano, string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l])) + val, err = time.Parse(time.RFC3339Nano, string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l])) if err != nil { unmarshalTimestampErrors.Add(1) } } poffset += kparamNameLength + 6 + uint32(l) - case kparams.Slice: + case params.Slice: // read slice element type typ := b[pi+offset+kparamNameLength+poffset] // read slice size @@ -360,27 +359,27 @@ func (e *Kevent) UnmarshalRaw(b []byte, ver kcapver.Version) error { s[i] = string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:size:size]) off += 2 + uint32(size) } - kval = s + val = s case '8': v := make([]uint64, l) for i := 0; i < int(l); i++ { bytes.ReadUint64(b[inc(idx, 22)+offset+kparamNameLength+poffset+off:]) off += 8 } - kval = v + val = v } poffset += kparamNameLength + 4 + 1 + 2 + off - case kparams.Binary, kparams.SID, kparams.WbemSID: + case params.Binary, params.SID, params.WbemSID: l := bytes.ReadUint32(b[pi+offset+kparamNameLength+poffset:]) buf = b[inc(idx, 18)+offset+kparamNameLength+poffset:] if len(buf) > 0 { - kval = buf[:l] + val = buf[:l] } poffset += kparamNameLength + 8 + l } - if kval != nil { - e.Kparams.AppendFromKcap(kparamName, kparams.Type(typ), kval, e.Type) + if val != nil { + e.Params.AppendFromCapture(kparamName, params.Type(typ), val, e.Type) } } @@ -427,8 +426,8 @@ func writePsResources() bool { return SerializeHandles || SerializeThreads || SerializeImages || SerializePE } -// MarshalJSON produces a JSON payload for this kevent. -func (e *Kevent) MarshalJSON() []byte { +// MarshalJSON produces a JSON payload for this event. +func (e *Event) MarshalJSON() []byte { if e == nil { return []byte{} } @@ -450,50 +449,50 @@ func (e *Kevent) MarshalJSON() []byte { timestamp = e.Timestamp.AppendFormat(timestamp, time.RFC3339Nano) js.writeObjectField("timestamp").writeString(string(timestamp)).writeMore() - // start kparams - js.writeObjectField("kparams") + // start params + js.writeObjectField("params") js.writeObjectStart() - pars := make([]*Kparam, 0, len(e.Kparams)) - for _, kpar := range e.Kparams { - pars = append(pars, kpar) + pars := make([]*Param, 0, len(e.Params)) + for _, par := range e.Params { + pars = append(pars, par) } sort.Slice(pars, func(i, j int) bool { return pars[i].Name < pars[j].Name }) - for i, kpar := range pars { + for i, par := range pars { writeMore := js.shouldWriteMore(i, len(pars)) - js.writeObjectField(kpar.Name) - switch kpar.Type { - case kparams.Int64: - js.writeInt64(kpar.Value.(int64)) - case kparams.Uint64: - js.writeUint64(kpar.Value.(uint64)) - case kparams.Int32: - js.writeInt32(kpar.Value.(int32)) - case kparams.Uint32: - js.writeUint32(kpar.Value.(uint32)) - case kparams.Int16: - js.writeInt16(kpar.Value.(int16)) - case kparams.Uint16, kparams.Port: - js.writeUint16(kpar.Value.(uint16)) - case kparams.Int8: - js.writeInt8(kpar.Value.(int8)) - case kparams.Uint8: - js.writeUint8(kpar.Value.(uint8)) - case kparams.Float: - js.writeFloat32(kpar.Value.(float32)) - case kparams.Double: - js.writeFloat64(kpar.Value.(float64)) - case kparams.PID, kparams.TID: - js.writeUint32(kpar.Value.(uint32)) - case kparams.IPv4, kparams.IPv6: - js.writeString(kpar.Value.(net.IP).String()) - case kparams.Bool: - js.writeBool(kpar.Value.(bool)) - case kparams.Time: - js.writeString(kpar.Value.(time.Time).String()) - case kparams.Slice: - switch slice := kpar.Value.(type) { + js.writeObjectField(par.Name) + switch par.Type { + case params.Int64: + js.writeInt64(par.Value.(int64)) + case params.Uint64: + js.writeUint64(par.Value.(uint64)) + case params.Int32: + js.writeInt32(par.Value.(int32)) + case params.Uint32: + js.writeUint32(par.Value.(uint32)) + case params.Int16: + js.writeInt16(par.Value.(int16)) + case params.Uint16, params.Port: + js.writeUint16(par.Value.(uint16)) + case params.Int8: + js.writeInt8(par.Value.(int8)) + case params.Uint8: + js.writeUint8(par.Value.(uint8)) + case params.Float: + js.writeFloat32(par.Value.(float32)) + case params.Double: + js.writeFloat64(par.Value.(float64)) + case params.PID, params.TID: + js.writeUint32(par.Value.(uint32)) + case params.IPv4, params.IPv6: + js.writeString(par.Value.(net.IP).String()) + case params.Bool: + js.writeBool(par.Value.(bool)) + case params.Time: + js.writeString(par.Value.(time.Time).String()) + case params.Slice: + switch slice := par.Value.(type) { case []string: js.writeArrayStart() for i, s := range slice { @@ -506,13 +505,13 @@ func (e *Kevent) MarshalJSON() []byte { js.writeArrayEnd() } default: - js.writeEscapeString(e.GetParamAsString(kpar.Name)) + js.writeEscapeString(e.GetParamAsString(par.Name)) } if writeMore { js.writeMore() } } - // end kparams + // end params js.writeObjectEnd().writeMore() // start metadata diff --git a/pkg/kevent/ktypes/metainfo_windows.go b/pkg/event/metainfo_windows.go similarity index 74% rename from pkg/kevent/ktypes/metainfo_windows.go rename to pkg/event/metainfo_windows.go index db2c7cc22..a052be00c 100644 --- a/pkg/kevent/ktypes/metainfo_windows.go +++ b/pkg/event/metainfo_windows.go @@ -16,16 +16,15 @@ * limitations under the License. */ -package ktypes +package event import ( "cmp" "slices" ) -// KeventInfo describes the kernel event meta info such as human-readable name, category -// and event's description. -type KeventInfo struct { +// Info describes the event meta info such as human-readable name, category and description. +type Info struct { // Name is the human-readable representation of the event (e.g. CreateProcess, DeleteFile). Name string // Category designates the category to which event pertains. (e.g. process, net) @@ -34,7 +33,7 @@ type KeventInfo struct { Description string } -var kevents = map[Ktype]KeventInfo{ +var events = map[Type]Info{ CreateProcess: {"CreateProcess", Process, "Creates a new process and its primary thread"}, TerminateProcess: {"TerminateProcess", Process, "Terminates the process and all of its threads"}, OpenProcess: {"OpenProcess", Process, "Opens the process handle"}, @@ -93,7 +92,7 @@ var kevents = map[Ktype]KeventInfo{ SetThreadpoolTimer: {"SetThreadpoolTimer", Threadpool, "Sets the thread pool timer object"}, } -var ktypes = map[string]Ktype{ +var types = map[string]Type{ "CreateProcess": CreateProcess, "TerminateProcess": TerminateProcess, "OpenProcess": OpenProcess, @@ -152,135 +151,135 @@ var ktypes = map[string]Ktype{ "SetThreadpoolTimer": SetThreadpoolTimer, } -// indexedKevents keeps the slice of event infos. When the +// indexedEvents keeps the slice of event infos. When the // new event type is added, MAKE SURE TO ADD the event info // at the END of the slice. This way the event index is guaranteed // to remain static which is important for eventlog message identifiers. -var indexedKevents = []KeventInfo{ - kevents[CreateProcess], - kevents[TerminateProcess], - kevents[OpenProcess], - kevents[CreateThread], - kevents[TerminateThread], - kevents[OpenThread], - kevents[SetThreadContext], - kevents[LoadImage], - kevents[UnloadImage], - kevents[CreateFile], - kevents[CloseFile], - kevents[ReadFile], - kevents[WriteFile], - kevents[SetFileInformation], - kevents[DeleteFile], - kevents[RenameFile], - kevents[EnumDirectory], - kevents[RegCreateKey], - kevents[RegOpenKey], - kevents[RegSetValue], - kevents[RegQueryValue], - kevents[RegQueryKey], - kevents[RegDeleteKey], - kevents[RegDeleteValue], - kevents[AcceptTCPv4], - kevents[AcceptTCPv6], - kevents[SendTCPv4], - kevents[SendTCPv6], - kevents[SendUDPv4], - kevents[SendUDPv6], - kevents[RecvTCPv4], - kevents[RecvTCPv6], - kevents[RecvUDPv4], - kevents[RecvUDPv6], - kevents[ConnectTCPv4], - kevents[ConnectTCPv6], - kevents[ReconnectTCPv4], - kevents[ReconnectTCPv6], - kevents[DisconnectTCPv4], - kevents[DisconnectTCPv6], - kevents[RetransmitTCPv4], - kevents[RetransmitTCPv6], - kevents[CreateHandle], - kevents[CloseHandle], - kevents[DuplicateHandle], - kevents[VirtualAlloc], - kevents[VirtualFree], - kevents[MapViewFile], - kevents[UnmapViewFile], - kevents[QueryDNS], - kevents[ReplyDNS], - kevents[CreateSymbolicLinkObject], - kevents[SubmitThreadpoolWork], - kevents[SubmitThreadpoolCallback], - kevents[SetThreadpoolTimer], +var indexedEvents = []Info{ + events[CreateProcess], + events[TerminateProcess], + events[OpenProcess], + events[CreateThread], + events[TerminateThread], + events[OpenThread], + events[SetThreadContext], + events[LoadImage], + events[UnloadImage], + events[CreateFile], + events[CloseFile], + events[ReadFile], + events[WriteFile], + events[SetFileInformation], + events[DeleteFile], + events[RenameFile], + events[EnumDirectory], + events[RegCreateKey], + events[RegOpenKey], + events[RegSetValue], + events[RegQueryValue], + events[RegQueryKey], + events[RegDeleteKey], + events[RegDeleteValue], + events[AcceptTCPv4], + events[AcceptTCPv6], + events[SendTCPv4], + events[SendTCPv6], + events[SendUDPv4], + events[SendUDPv6], + events[RecvTCPv4], + events[RecvTCPv6], + events[RecvUDPv4], + events[RecvUDPv6], + events[ConnectTCPv4], + events[ConnectTCPv6], + events[ReconnectTCPv4], + events[ReconnectTCPv6], + events[DisconnectTCPv4], + events[DisconnectTCPv6], + events[RetransmitTCPv4], + events[RetransmitTCPv6], + events[CreateHandle], + events[CloseHandle], + events[DuplicateHandle], + events[VirtualAlloc], + events[VirtualFree], + events[MapViewFile], + events[UnmapViewFile], + events[QueryDNS], + events[ReplyDNS], + events[CreateSymbolicLinkObject], + events[SubmitThreadpoolWork], + events[SubmitThreadpoolCallback], + events[SetThreadpoolTimer], } // All returns all event types. -func All() []Ktype { - s := make([]Ktype, 0, len(ktypes)) - for _, ktype := range ktypes { - s = append(s, ktype) +func All() []Type { + s := make([]Type, 0, len(types)) + for _, Type := range types { + s = append(s, Type) } return s } -// KtypeToKeventInfo maps the event type to the structure storing detailed information about the event. -func KtypeToKeventInfo(ktype Ktype) KeventInfo { - if kinfo, ok := kevents[ktype]; ok { - return kinfo +// TypeToEventInfo maps the event type to the structure storing detailed information about the event. +func TypeToEventInfo(typ Type) Info { + if info, ok := events[typ]; ok { + return info } - return KeventInfo{Name: "N/A", Category: Unknown} + return Info{Name: "N/A", Category: Unknown} } -// KeventNameToKtype converts a human-readable event name to its internal type representation. -func KeventNameToKtype(name string) Ktype { - if ktype, ok := ktypes[name]; ok { - return ktype +// NameToType converts a human-readable event name to its internal type representation. +func NameToType(name string) Type { + if typ, ok := types[name]; ok { + return typ } - return UnknownKtype + return UnknownType } -// KeventNameToKtypes maps the event name to internal type representations, specifically, network -// events that have multiple internal types for a single event name. For example, Accept event name -// have AcceptTCP4 and AcceptTCP6 types. -func KeventNameToKtypes(name string) []Ktype { +// NameToTypes maps the event name to internal type representations, specifically, network +// events that have multiple internal types for a single event name. For example, the Accept +// event name has AcceptTCP4 and AcceptTCP6 types. +func NameToTypes(name string) []Type { switch name { case "Accept": - return []Ktype{AcceptTCPv4, AcceptTCPv6} + return []Type{AcceptTCPv4, AcceptTCPv6} case "Send": - return []Ktype{SendTCPv4, SendTCPv6, SendUDPv4, SendUDPv6} + return []Type{SendTCPv4, SendTCPv6, SendUDPv4, SendUDPv6} case "Recv": - return []Ktype{RecvTCPv4, RecvTCPv6, RecvUDPv4, RecvUDPv6} + return []Type{RecvTCPv4, RecvTCPv6, RecvUDPv4, RecvUDPv6} case "Connect": - return []Ktype{ConnectTCPv4, ConnectTCPv6} + return []Type{ConnectTCPv4, ConnectTCPv6} case "Reconnect": - return []Ktype{ReconnectTCPv4, ReconnectTCPv6} + return []Type{ReconnectTCPv4, ReconnectTCPv6} case "Disconnect": - return []Ktype{DisconnectTCPv4, DisconnectTCPv6} + return []Type{DisconnectTCPv4, DisconnectTCPv6} case "Retransmit": - return []Ktype{RetransmitTCPv4, RetransmitTCPv6} + return []Type{RetransmitTCPv4, RetransmitTCPv6} default: - return []Ktype{KeventNameToKtype(name)} + return []Type{NameToType(name)} } } -// GetKtypesMeta returns event types metadata. -func GetKtypesMeta() []KeventInfo { - ktypes := make([]KeventInfo, 0) +// GetTypesMeta returns event types metadata. +func GetTypesMeta() []Info { + typs := make([]Info, 0) outer: - for _, ktyp := range kevents { - for _, typ := range ktypes { - if typ.Name == ktyp.Name { + for _, ev := range events { + for _, typ := range typs { + if typ.Name == ev.Name { continue outer } } - ktypes = append(ktypes, ktyp) + typs = append(typs, ev) } - slices.SortFunc(ktypes, func(a, b KeventInfo) int { + slices.SortFunc(typs, func(a, b Info) int { return cmp.Or(cmp.Compare(a.Category, b.Category), cmp.Compare(a.Name, b.Name)) }) - return ktypes + return typs } -// GetKtypesMetaIndexed returns indexed event types metadata +// GetTypesMetaIndexed returns indexed event types metadata // that is guaranteed to always return the same event indices. -func GetKtypesMetaIndexed() []KeventInfo { return indexedKevents } +func GetTypesMetaIndexed() []Info { return indexedEvents } diff --git a/pkg/kevent/ktypes/metainfo_windows_test.go b/pkg/event/metainfo_windows_test.go similarity index 57% rename from pkg/kevent/ktypes/metainfo_windows_test.go rename to pkg/event/metainfo_windows_test.go index 7750174fa..e23be0d5c 100644 --- a/pkg/kevent/ktypes/metainfo_windows_test.go +++ b/pkg/event/metainfo_windows_test.go @@ -16,31 +16,31 @@ * limitations under the License. */ -package ktypes +package event import ( "github.com/stretchr/testify/assert" "testing" ) -func TestKeventNameToKtype(t *testing.T) { - ktype := KeventNameToKtype("CreateProcess") +func TestEventNameToType(t *testing.T) { + typ := NameToType("CreateProcess") - assert.Equal(t, CreateProcess, ktype) + assert.Equal(t, CreateProcess, typ) - ktype = KeventNameToKtype("CreateRemoteThread") - assert.Equal(t, UnknownKtype, ktype) + typ = NameToType("CreateRemoteThread") + assert.Equal(t, UnknownType, typ) } -func TestKtypeToKeventInfo(t *testing.T) { - kinfo := KtypeToKeventInfo(CreateProcess) +func TestEventToEventInfo(t *testing.T) { + info := TypeToEventInfo(CreateProcess) - assert.Equal(t, "CreateProcess", kinfo.Name) - assert.Equal(t, Process, kinfo.Category) - assert.Equal(t, "Creates a new process and its primary thread", kinfo.Description) + assert.Equal(t, "CreateProcess", info.Name) + assert.Equal(t, Process, info.Category) + assert.Equal(t, "Creates a new process and its primary thread", info.Description) - kinfo = KtypeToKeventInfo(UnknownKtype) - assert.Equal(t, "N/A", kinfo.Name) - assert.Equal(t, Unknown, kinfo.Category) - assert.Empty(t, kinfo.Description) + info = TypeToEventInfo(UnknownType) + assert.Equal(t, "N/A", info.Name) + assert.Equal(t, Unknown, info.Category) + assert.Empty(t, info.Description) } diff --git a/pkg/kevent/kparam.go b/pkg/event/param.go similarity index 61% rename from pkg/kevent/kparam.go rename to pkg/event/param.go index 147d0e15a..62e5209c2 100644 --- a/pkg/kevent/kparam.go +++ b/pkg/event/param.go @@ -16,12 +16,11 @@ * limitations under the License. */ -package kevent +package event import ( "fmt" "github.com/rabbitstack/fibratus/pkg/fs" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/rabbitstack/fibratus/pkg/network" "github.com/rabbitstack/fibratus/pkg/util/key" "github.com/rabbitstack/fibratus/pkg/util/va" @@ -33,8 +32,8 @@ import ( "strings" "time" - kerrors "github.com/rabbitstack/fibratus/pkg/errors" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/errors" + "github.com/rabbitstack/fibratus/pkg/event/params" ) var caser = cases.Title(language.English) @@ -53,7 +52,7 @@ const ( CamelCase ParamCaseStyle = 4 ) -// ParamNameCaseStyle designates the case style for kernel parameter names +// ParamNameCaseStyle designates the case style for the parameter names var ParamNameCaseStyle = SnakeCase // ParamKVDelimiter specifies the character that delimits parameter's key from its value @@ -85,13 +84,13 @@ func WithEnum(enum ParamEnum) ParamOption { } } -// Kparam defines the layout of the kernel event parameter. -type Kparam struct { +// Param defines the layout of the event parameter. +type Param struct { // Type is the type of the parameter. For example, `sport` parameter has the `Port` type although its value // is the uint16 numeric type. - Type kparams.Type `json:"-"` + Type params.Type `json:"-"` // Value is the container for parameter values. To access the underlying value use the appropriate `Get` methods. - Value kparams.Value `json:"value"` + Value params.Value `json:"value"` // Name represents the name of the parameter (e.g. pid, sport). Name string `json:"name"` // Flags represents parameter flags @@ -100,149 +99,149 @@ type Kparam struct { Enum ParamEnum `json:"enum"` } -// KcapType returns the event type saved inside the capture file. +// CaptureType returns the event type saved inside the capture file. // Captures usually override the type of the parameter to provide // consistent replay experience. For example, the file path param // type is converted to string param type, as drive mapping is performed // on the target where the capture is being taken. -func (k Kparam) KcapType() kparams.Type { - switch k.Type { - case kparams.HandleType, kparams.DOSPath, kparams.Key: - return kparams.UnicodeString +func (p Param) CaptureType() params.Type { + switch p.Type { + case params.HandleType, params.DOSPath, params.Key: + return params.UnicodeString default: - return k.Type + return p.Type } } -// Kparams is the type that represents the sequence of kernel event parameters -type Kparams map[string]*Kparam +// Params is the type that represents the sequence of event parameters +type Params map[string]*Param -// NewKparamFromKcap builds a kparam instance from the restored state. -func NewKparamFromKcap(name string, typ kparams.Type, value kparams.Value, ktype ktypes.Ktype) *Kparam { +// NewParamFromCapture builds a parameter instance from the restored capture state. +func NewParamFromCapture(name string, typ params.Type, value params.Value, etype Type) *Param { var enum ParamEnum var flags ParamFlags switch name { - case kparams.FileOperation: + case params.FileOperation: enum = fs.FileCreateDispositions - case kparams.FileCreateOptions: + case params.FileCreateOptions: flags = FileCreateOptionsFlags - case kparams.FileAttributes: + case params.FileAttributes: flags = FileAttributeFlags - case kparams.FileShareMask: + case params.FileShareMask: flags = FileShareModeFlags - case kparams.FileInfoClass: + case params.FileInfoClass: enum = fs.FileInfoClasses - case kparams.FileType: + case params.FileType: enum = fs.FileTypes - case kparams.NetL4Proto: + case params.NetL4Proto: enum = network.ProtoNames - case kparams.RegValueType: + case params.RegValueType: enum = key.RegistryValueTypes - case kparams.MemAllocType: + case params.MemAllocType: flags = MemAllocationFlags - case kparams.FileViewSectionType: + case params.FileViewSectionType: enum = ViewSectionTypes - case kparams.DNSOpts: + case params.DNSOpts: flags = DNSOptsFlags - case kparams.DNSRR: + case params.DNSRR: enum = DNSRecordTypes - case kparams.DNSRcode: + case params.DNSRcode: enum = DNSResponseCodes - case kparams.DesiredAccess: - if ktype == ktypes.OpenProcess { + case params.DesiredAccess: + if etype == OpenProcess { flags = PsAccessRightFlags } else { flags = ThreadAccessRightFlags } - case kparams.MemProtect: - if ktype == ktypes.VirtualAlloc || ktype == ktypes.VirtualFree { + case params.MemProtect: + if etype == VirtualAlloc || etype == VirtualFree { flags = MemProtectionFlags } else { flags = ViewProtectionFlags } } - return &Kparam{Name: name, Type: typ, Value: value, Enum: enum, Flags: flags} + return &Param{Name: name, Type: typ, Value: value, Enum: enum, Flags: flags} } // Append adds a new parameter with the specified name, type and value. -func (kpars Kparams) Append(name string, typ kparams.Type, value kparams.Value, opts ...ParamOption) Kparams { - kpars[name] = NewKparam(name, typ, value, opts...) - return kpars +func (pars Params) Append(name string, typ params.Type, value params.Value, opts ...ParamOption) Params { + pars[name] = NewParam(name, typ, value, opts...) + return pars } -// AppendFromKcap adds a new parameter with the specified name, type and value from the kcap state. -func (kpars Kparams) AppendFromKcap(name string, typ kparams.Type, value kparams.Value, ktype ktypes.Ktype) Kparams { - kpars[name] = NewKparamFromKcap(name, typ, value, ktype) - return kpars +// AppendFromCapture adds a new parameter with the specified name, type and value from the cap state. +func (pars Params) AppendFromCapture(name string, typ params.Type, value params.Value, etype Type) Params { + pars[name] = NewParamFromCapture(name, typ, value, etype) + return pars } // Contains determines whether the specified parameter name exists. -func (kpars Kparams) Contains(name string) bool { - _, err := kpars.findParam(name) +func (pars Params) Contains(name string) bool { + _, err := pars.findParam(name) return err == nil } // Remove deletes the specified parameter from the map. -func (kpars Kparams) Remove(name string) { - delete(kpars, name) +func (pars Params) Remove(name string) { + delete(pars, name) } // Get returns the event parameter with specified name. -func (kpars Kparams) Get(name string) (*Kparam, error) { - return kpars.findParam(name) +func (pars Params) Get(name string) (*Param, error) { + return pars.findParam(name) } // Len returns the number of parameters. -func (kpars Kparams) Len() int { return len(kpars) } +func (pars Params) Len() int { return len(pars) } // Set replaces the value that is indexed at existing parameter name. It will return an error // if the supplied parameter is not present. -func (kpars Kparams) Set(name string, value kparams.Value, typ kparams.Type) error { - _, err := kpars.findParam(name) +func (pars Params) Set(name string, value params.Value, typ params.Type) error { + _, err := pars.findParam(name) if err != nil { return fmt.Errorf("setting the value on a missing %q parameter is not allowed", name) } - kpars[name] = &Kparam{Name: name, Value: value, Type: typ} + pars[name] = &Param{Name: name, Value: value, Type: typ} return nil } // SetValue replaces the value for the given parameter name. It will return an error // if the supplied parameter is not present in the parameter map. -func (kpars Kparams) SetValue(name string, value kparams.Value) error { - _, err := kpars.findParam(name) +func (pars Params) SetValue(name string, value params.Value) error { + _, err := pars.findParam(name) if err != nil { return fmt.Errorf("setting the value on a missing %q parameter is not allowed", name) } - kpars[name].Value = value + pars[name].Value = value return nil } // GetRaw returns the raw value for given parameter name. It is the responsibility of the caller to probe type assertion // on the value before yielding its underlying type. -func (kpars Kparams) GetRaw(name string) (kparams.Value, error) { - kpar, err := kpars.findParam(name) +func (pars Params) GetRaw(name string) (params.Value, error) { + par, err := pars.findParam(name) if err != nil { return "", err } - return kpar.Value, nil + return par.Value, nil } // GetString returns the underlying string value from the parameter. -func (kpars Kparams) GetString(name string) (string, error) { - kpar, err := kpars.findParam(name) +func (pars Params) GetString(name string) (string, error) { + par, err := pars.findParam(name) if err != nil { return "", err } - if _, ok := kpar.Value.(string); !ok { + if _, ok := par.Value.(string); !ok { return "", fmt.Errorf("unable to type cast %q parameter to string value", name) } - return kpar.Value.(string), nil + return par.Value.(string), nil } // MustGetString returns the string parameter or panics // if an error occurs while trying to get the parameter. -func (kpars Kparams) MustGetString(name string) string { - s, err := kpars.GetString(name) +func (pars Params) MustGetString(name string) string { + s, err := pars.GetString(name) if err != nil { panic(err) } @@ -250,14 +249,14 @@ func (kpars Kparams) MustGetString(name string) string { } // GetPid returns the pid from the parameter. -func (kpars Kparams) GetPid() (uint32, error) { - return kpars.getPid(kparams.ProcessID) +func (pars Params) GetPid() (uint32, error) { + return pars.getPid(params.ProcessID) } // MustGetPid returns the pid parameter. It panics if // an error occurs while trying to get the pid parameter. -func (kpars Kparams) MustGetPid() uint32 { - pid, err := kpars.GetPid() +func (pars Params) MustGetPid() uint32 { + pid, err := pars.GetPid() if err != nil { panic(err) } @@ -265,29 +264,29 @@ func (kpars Kparams) MustGetPid() uint32 { } // GetPpid returns the parent pid from the parameter. -func (kpars Kparams) GetPpid() (uint32, error) { - return kpars.getPid(kparams.ProcessParentID) +func (pars Params) GetPpid() (uint32, error) { + return pars.getPid(params.ProcessParentID) } // MustGetPpid returns the parent pid parameter. It panics if // an error occurs while trying to get the pid parameter. -func (kpars Kparams) MustGetPpid() uint32 { - ppid, err := kpars.GetPpid() +func (pars Params) MustGetPpid() uint32 { + ppid, err := pars.GetPpid() if err != nil { panic(err) } return ppid } -func (kpars Kparams) getPid(name string) (uint32, error) { - kpar, err := kpars.findParam(name) +func (pars Params) getPid(name string) (uint32, error) { + par, err := pars.findParam(name) if err != nil { return uint32(0), err } - if kpar.Type != kparams.PID { + if par.Type != params.PID { return uint32(0), fmt.Errorf("%q parameter is not a PID", name) } - v, ok := kpar.Value.(uint32) + v, ok := par.Value.(uint32) if !ok { return uint32(0), fmt.Errorf("unable to type cast %q parameter to uint32 value from pid", name) } @@ -295,44 +294,44 @@ func (kpars Kparams) getPid(name string) (uint32, error) { } // GetTid returns the thread id from the parameter. -func (kpars Kparams) GetTid() (uint32, error) { - kpar, err := kpars.findParam(kparams.ThreadID) +func (pars Params) GetTid() (uint32, error) { + par, err := pars.findParam(params.ThreadID) if err != nil { return uint32(0), err } - if kpar.Type != kparams.TID { - return uint32(0), fmt.Errorf("%q parameter is not a TID", kparams.ThreadID) + if par.Type != params.TID { + return uint32(0), fmt.Errorf("%q parameter is not a TID", params.ThreadID) } - v, ok := kpar.Value.(uint32) + v, ok := par.Value.(uint32) if !ok { - return uint32(0), fmt.Errorf("unable to type cast %q parameter to uint32 value from tid", kparams.ThreadID) + return uint32(0), fmt.Errorf("unable to type cast %q parameter to uint32 value from tid", params.ThreadID) } return v, nil } // MustGetTid returns the thread id from the parameter or panics if an error occurs. -func (kpars Kparams) MustGetTid() uint32 { - kpar, err := kpars.findParam(kparams.ThreadID) +func (pars Params) MustGetTid() uint32 { + par, err := pars.findParam(params.ThreadID) if err != nil { panic(err) } - if kpar.Type != kparams.TID { - panic(fmt.Errorf("%q parameter is not a TID", kparams.ThreadID)) + if par.Type != params.TID { + panic(fmt.Errorf("%q parameter is not a TID", params.ThreadID)) } - v, ok := kpar.Value.(uint32) + v, ok := par.Value.(uint32) if !ok { - panic(fmt.Errorf("unable to type cast %q parameter to uint32 value from tid", kparams.ThreadID)) + panic(fmt.Errorf("unable to type cast %q parameter to uint32 value from tid", params.ThreadID)) } return v } // GetUint8 returns the underlying uint8 value from the parameter. -func (kpars Kparams) GetUint8(name string) (uint8, error) { - kpar, err := kpars.findParam(name) +func (pars Params) GetUint8(name string) (uint8, error) { + par, err := pars.findParam(name) if err != nil { return uint8(0), err } - v, ok := kpar.Value.(uint8) + v, ok := par.Value.(uint8) if !ok { return uint8(0), fmt.Errorf("unable to type cast %q parameter to uint8 value", name) } @@ -340,12 +339,12 @@ func (kpars Kparams) GetUint8(name string) (uint8, error) { } // GetBool returns the underlying boolean value from the parameter. -func (kpars Kparams) GetBool(name string) (bool, error) { - kpar, err := kpars.findParam(name) +func (pars Params) GetBool(name string) (bool, error) { + par, err := pars.findParam(name) if err != nil { return false, err } - v, ok := kpar.Value.(bool) + v, ok := par.Value.(bool) if !ok { return false, fmt.Errorf("unable to type cast %q parameter to bool value", name) } @@ -354,8 +353,8 @@ func (kpars Kparams) GetBool(name string) (bool, error) { // MustGetBool returns the underlying boolean value from the parameter or // panics if the parameter can't be retrieved. -func (kpars Kparams) MustGetBool(name string) bool { - val, err := kpars.GetBool(name) +func (pars Params) MustGetBool(name string) bool { + val, err := pars.GetBool(name) if err != nil { panic(err) } @@ -364,8 +363,8 @@ func (kpars Kparams) MustGetBool(name string) bool { // TryGetBool tries to retrieve the boolean value from the parameter. // Returns the underlying value on success, or false otherwise. -func (kpars Kparams) TryGetBool(name string) bool { - val, err := kpars.GetBool(name) +func (pars Params) TryGetBool(name string) bool { + val, err := pars.GetBool(name) if err != nil { return false } @@ -373,12 +372,12 @@ func (kpars Kparams) TryGetBool(name string) bool { } // GetInt8 returns the underlying int8 value from the parameter. -func (kpars Kparams) GetInt8(name string) (int8, error) { - kpar, err := kpars.findParam(name) +func (pars Params) GetInt8(name string) (int8, error) { + par, err := pars.findParam(name) if err != nil { return int8(0), err } - v, ok := kpar.Value.(int8) + v, ok := par.Value.(int8) if !ok { return int8(0), fmt.Errorf("unable to type cast %q parameter to int8 value", name) } @@ -386,12 +385,12 @@ func (kpars Kparams) GetInt8(name string) (int8, error) { } // GetUint16 returns the underlying int16 value from the parameter. -func (kpars Kparams) GetUint16(name string) (uint16, error) { - kpar, err := kpars.findParam(name) +func (pars Params) GetUint16(name string) (uint16, error) { + par, err := pars.findParam(name) if err != nil { return uint16(0), err } - v, ok := kpar.Value.(uint16) + v, ok := par.Value.(uint16) if !ok { return uint16(0), fmt.Errorf("unable to type cast %q parameter to uint16 value", name) } @@ -400,8 +399,8 @@ func (kpars Kparams) GetUint16(name string) (uint16, error) { // MustGetUint16 returns the underlying uint16 value parameter. It panics if // an error occurs while trying to get the parameter. -func (kpars Kparams) MustGetUint16(name string) uint16 { - v, err := kpars.GetUint16(name) +func (pars Params) MustGetUint16(name string) uint16 { + v, err := pars.GetUint16(name) if err != nil { panic(err) } @@ -410,8 +409,8 @@ func (kpars Kparams) MustGetUint16(name string) uint16 { // TryGetUint16 tries to retrieve the uint16 value from the parameter. // Returns the underlying value on success, or zero otherwise. -func (kpars Kparams) TryGetUint16(name string) uint16 { - val, err := kpars.GetUint16(name) +func (pars Params) TryGetUint16(name string) uint16 { + val, err := pars.GetUint16(name) if err != nil { return 0 } @@ -419,12 +418,12 @@ func (kpars Kparams) TryGetUint16(name string) uint16 { } // GetInt16 returns the underlying int16 value from the parameter. -func (kpars Kparams) GetInt16(name string) (int16, error) { - kpar, err := kpars.findParam(name) +func (pars Params) GetInt16(name string) (int16, error) { + par, err := pars.findParam(name) if err != nil { return int16(0), err } - v, ok := kpar.Value.(int16) + v, ok := par.Value.(int16) if !ok { return int16(0), fmt.Errorf("unable to type cast %q parameter to int16 value", name) } @@ -432,12 +431,12 @@ func (kpars Kparams) GetInt16(name string) (int16, error) { } // GetUint32 returns the underlying uint32 value from the parameter. -func (kpars Kparams) GetUint32(name string) (uint32, error) { - kpar, err := kpars.findParam(name) +func (pars Params) GetUint32(name string) (uint32, error) { + par, err := pars.findParam(name) if err != nil { return uint32(0), err } - v, ok := kpar.Value.(uint32) + v, ok := par.Value.(uint32) if !ok { return uint32(0), fmt.Errorf("unable to type cast %q parameter to uint32 value", name) } @@ -446,8 +445,8 @@ func (kpars Kparams) GetUint32(name string) (uint32, error) { // MustGetUint32 returns the underlying uint32 value parameter. It panics if // an error occurs while trying to get the parameter. -func (kpars Kparams) MustGetUint32(name string) uint32 { - v, err := kpars.GetUint32(name) +func (pars Params) MustGetUint32(name string) uint32 { + v, err := pars.GetUint32(name) if err != nil { panic(err) } @@ -456,8 +455,8 @@ func (kpars Kparams) MustGetUint32(name string) uint32 { // TryGetUint32 tries to retrieve the uint32 value from the parameter. // Returns the underlying value on success, or zero otherwise. -func (kpars Kparams) TryGetUint32(name string) uint32 { - val, err := kpars.GetUint32(name) +func (pars Params) TryGetUint32(name string) uint32 { + val, err := pars.GetUint32(name) if err != nil { return 0 } @@ -465,12 +464,12 @@ func (kpars Kparams) TryGetUint32(name string) uint32 { } // GetInt32 returns the underlying int32 value from the parameter. -func (kpars Kparams) GetInt32(name string) (int32, error) { - kpar, err := kpars.findParam(name) +func (pars Params) GetInt32(name string) (int32, error) { + par, err := pars.findParam(name) if err != nil { return int32(0), err } - v, ok := kpar.Value.(int32) + v, ok := par.Value.(int32) if !ok { return int32(0), fmt.Errorf("unable to type cast %q parameter to int32 value", name) } @@ -478,12 +477,12 @@ func (kpars Kparams) GetInt32(name string) (int32, error) { } // GetUint64 returns the underlying uint64 value from the parameter. -func (kpars Kparams) GetUint64(name string) (uint64, error) { - kpar, err := kpars.findParam(name) +func (pars Params) GetUint64(name string) (uint64, error) { + par, err := pars.findParam(name) if err != nil { return uint64(0), err } - v, ok := kpar.Value.(uint64) + v, ok := par.Value.(uint64) if !ok { return uint64(0), fmt.Errorf("unable to type cast %q parameter to uint64 value", name) } @@ -492,8 +491,8 @@ func (kpars Kparams) GetUint64(name string) (uint64, error) { // MustGetUint64 returns the underlying uint64 value parameter. It panics if // an error occurs while trying to get the parameter. -func (kpars Kparams) MustGetUint64(name string) uint64 { - v, err := kpars.GetUint64(name) +func (pars Params) MustGetUint64(name string) uint64 { + v, err := pars.GetUint64(name) if err != nil { panic(err) } @@ -502,8 +501,8 @@ func (kpars Kparams) MustGetUint64(name string) uint64 { // TryGetUint64 tries to retrieve the uint64 value from the parameter. // Returns the underlying value on success, or zero otherwise. -func (kpars Kparams) TryGetUint64(name string) uint64 { - val, err := kpars.GetUint64(name) +func (pars Params) TryGetUint64(name string) uint64 { + val, err := pars.GetUint64(name) if err != nil { return 0 } @@ -511,12 +510,12 @@ func (kpars Kparams) TryGetUint64(name string) uint64 { } // GetInt64 returns the underlying int64 value from the parameter. -func (kpars Kparams) GetInt64(name string) (int64, error) { - kpar, err := kpars.findParam(name) +func (pars Params) GetInt64(name string) (int64, error) { + par, err := pars.findParam(name) if err != nil { return int64(0), err } - v, ok := kpar.Value.(int64) + v, ok := par.Value.(int64) if !ok { return int64(0), fmt.Errorf("unable to type cast %q parameter to int64 value", name) } @@ -524,12 +523,12 @@ func (kpars Kparams) GetInt64(name string) (int64, error) { } // GetFloat returns the underlying float value from the parameter. -func (kpars Kparams) GetFloat(name string) (float32, error) { - kpar, err := kpars.findParam(name) +func (pars Params) GetFloat(name string) (float32, error) { + par, err := pars.findParam(name) if err != nil { return float32(0), err } - v, ok := kpar.Value.(float32) + v, ok := par.Value.(float32) if !ok { return float32(0), fmt.Errorf("unable to type cast %q parameter to float32 value", name) } @@ -537,12 +536,12 @@ func (kpars Kparams) GetFloat(name string) (float32, error) { } // GetDouble returns the underlying double (float64) value from the parameter. -func (kpars Kparams) GetDouble(name string) (float64, error) { - kpar, err := kpars.findParam(name) +func (pars Params) GetDouble(name string) (float64, error) { + par, err := pars.findParam(name) if err != nil { return float64(0), err } - v, ok := kpar.Value.(float64) + v, ok := par.Value.(float64) if !ok { return float64(0), fmt.Errorf("unable to type cast %q parameter to float64 value", name) } @@ -550,12 +549,12 @@ func (kpars Kparams) GetDouble(name string) (float64, error) { } // TryGetAddress attempts to convert the underlying type to address. -func (kpars Kparams) TryGetAddress(name string) va.Address { - kpar, err := kpars.findParam(name) +func (pars Params) TryGetAddress(name string) va.Address { + par, err := pars.findParam(name) if err != nil { return 0 } - v, ok := kpar.Value.(uint64) + v, ok := par.Value.(uint64) if !ok { return 0 } @@ -563,15 +562,15 @@ func (kpars Kparams) TryGetAddress(name string) va.Address { } // GetIPv4 returns the underlying IPv4 address from the parameter. -func (kpars Kparams) GetIPv4(name string) (net.IP, error) { - kpar, err := kpars.findParam(name) +func (pars Params) GetIPv4(name string) (net.IP, error) { + par, err := pars.findParam(name) if err != nil { return net.IP{}, err } - if kpar.Type != kparams.IPv4 { + if par.Type != params.IPv4 { return net.IP{}, fmt.Errorf("%q parameter is not an IPv4 address", name) } - v, ok := kpar.Value.(net.IP) + v, ok := par.Value.(net.IP) if !ok { return net.IP{}, fmt.Errorf("unable to type cast %q parameter to net.IP value", name) } @@ -579,15 +578,15 @@ func (kpars Kparams) GetIPv4(name string) (net.IP, error) { } // GetIPv6 returns the underlying IPv6 address from the parameter. -func (kpars Kparams) GetIPv6(name string) (net.IP, error) { - kpar, err := kpars.findParam(name) +func (pars Params) GetIPv6(name string) (net.IP, error) { + par, err := pars.findParam(name) if err != nil { return net.IP{}, err } - if kpar.Type != kparams.IPv6 { + if par.Type != params.IPv6 { return net.IP{}, fmt.Errorf("%q parameter is not an IPv6 address", name) } - v, ok := kpar.Value.(net.IP) + v, ok := par.Value.(net.IP) if !ok { return net.IP{}, fmt.Errorf("unable to type cast %q parameter to net.IP value", name) } @@ -595,15 +594,15 @@ func (kpars Kparams) GetIPv6(name string) (net.IP, error) { } // GetIP returns either the IPv4 or IPv6 address from the parameter. -func (kpars Kparams) GetIP(name string) (net.IP, error) { - kpar, err := kpars.findParam(name) +func (pars Params) GetIP(name string) (net.IP, error) { + par, err := pars.findParam(name) if err != nil { return net.IP{}, err } - if kpar.Type != kparams.IPv4 && kpar.Type != kparams.IPv6 { + if par.Type != params.IPv4 && par.Type != params.IPv6 { return net.IP{}, fmt.Errorf("%q parameter is not an IP address", name) } - v, ok := kpar.Value.(net.IP) + v, ok := par.Value.(net.IP) if !ok { return net.IP{}, fmt.Errorf("unable to type cast %q parameter to net.IP value", name) } @@ -611,8 +610,8 @@ func (kpars Kparams) GetIP(name string) (net.IP, error) { } // MustGetIP returns the IP address parameter or panics if an error occurs. -func (kpars Kparams) MustGetIP(name string) net.IP { - ip, err := kpars.GetIP(name) +func (pars Params) MustGetIP(name string) net.IP { + ip, err := pars.GetIP(name) if err != nil { panic(err) } @@ -620,12 +619,12 @@ func (kpars Kparams) MustGetIP(name string) net.IP { } // GetTime returns the underlying time structure from the parameter. -func (kpars Kparams) GetTime(name string) (time.Time, error) { - kpar, err := kpars.findParam(name) +func (pars Params) GetTime(name string) (time.Time, error) { + par, err := pars.findParam(name) if err != nil { return time.Unix(0, 0), err } - v, ok := kpar.Value.(time.Time) + v, ok := par.Value.(time.Time) if !ok { return time.Unix(0, 0), fmt.Errorf("unable to type cast %q parameter to Time value", name) } @@ -634,12 +633,12 @@ func (kpars Kparams) GetTime(name string) (time.Time, error) { // MustGetTime returns the underlying time structure from the parameter or panics // if any errors occur. -func (kpars Kparams) MustGetTime(name string) time.Time { - kpar, err := kpars.findParam(name) +func (pars Params) MustGetTime(name string) time.Time { + par, err := pars.findParam(name) if err != nil { panic(err) } - v, ok := kpar.Value.(time.Time) + v, ok := par.Value.(time.Time) if !ok { panic(fmt.Errorf("unable to type cast %q parameter to Time value", name)) } @@ -647,12 +646,12 @@ func (kpars Kparams) MustGetTime(name string) time.Time { } // GetStringSlice returns the string slice from the event parameter. -func (kpars Kparams) GetStringSlice(name string) ([]string, error) { - kpar, err := kpars.GetSlice(name) +func (pars Params) GetStringSlice(name string) ([]string, error) { + par, err := pars.GetSlice(name) if err != nil { return nil, err } - v, ok := kpar.([]string) + v, ok := par.([]string) if !ok { return nil, fmt.Errorf("unable to type cast %q parameter to string slice", name) } @@ -660,31 +659,31 @@ func (kpars Kparams) GetStringSlice(name string) ([]string, error) { } // GetSlice returns the slice of generic values from the parameter. -func (kpars Kparams) GetSlice(name string) (kparams.Value, error) { - kpar, err := kpars.findParam(name) +func (pars Params) GetSlice(name string) (params.Value, error) { + par, err := pars.findParam(name) if err != nil { return nil, err } - if reflect.TypeOf(kpar.Value).Kind() != reflect.Slice { + if reflect.TypeOf(par.Value).Kind() != reflect.Slice { return nil, fmt.Errorf("%q parameter is not a slice", name) } - return kpar.Value, nil + return par.Value, nil } // MustGetSlice returns the slice of generic values from the parameter or // panics if the parameter cannot be found. -func (kpars Kparams) MustGetSlice(name string) kparams.Value { - kpar, err := kpars.findParam(name) +func (pars Params) MustGetSlice(name string) params.Value { + par, err := pars.findParam(name) if err != nil { panic(err) } - return kpar.Value + return par.Value } // MustGetSliceAddrs returns the slice of addresses or panics if the parameter // is not found, or either a parameter is not a slice of addresses. -func (kpars Kparams) MustGetSliceAddrs(name string) []va.Address { - val := kpars.MustGetSlice(name) +func (pars Params) MustGetSliceAddrs(name string) []va.Address { + val := pars.MustGetSlice(name) addrs, ok := val.([]va.Address) if !ok { panic("must be a slice of addresses") @@ -694,22 +693,22 @@ func (kpars Kparams) MustGetSliceAddrs(name string) []va.Address { // String returns the string representation of the event parameters. Parameter names are rendered according // to the currently active parameter style case. -func (kpars Kparams) String() string { +func (pars Params) String() string { var sb strings.Builder // sort parameters by name - pars := make([]*Kparam, 0, len(kpars)) - for _, kpar := range kpars { - pars = append(pars, kpar) + s := make([]*Param, 0, len(pars)) + for _, par := range pars { + s = append(s, par) } - sort.Slice(pars, func(i, j int) bool { return pars[i].Name < pars[j].Name }) - for i, kpar := range pars { + sort.Slice(s, func(i, j int) bool { return s[i].Name < s[j].Name }) + for i, par := range s { switch ParamNameCaseStyle { case SnakeCase: - sb.WriteString(kpar.Name + ParamKVDelimiter + kpar.String()) + sb.WriteString(par.Name + ParamKVDelimiter + par.String()) case DotCase: - sb.WriteString(strings.Replace(kpar.Name, "_", ".", -1) + ParamKVDelimiter + kpar.String()) + sb.WriteString(strings.Replace(par.Name, "_", ".", -1) + ParamKVDelimiter + par.String()) case PascalCase: - sb.WriteString(strings.Replace(caser.String(strings.Replace(kpar.Name, "_", " ", -1)), " ", "", -1) + ParamKVDelimiter + kpar.String()) + sb.WriteString(strings.Replace(caser.String(strings.Replace(par.Name, "_", " ", -1)), " ", "", -1) + ParamKVDelimiter + par.String()) case CamelCase: } if i != len(pars)-1 { @@ -719,19 +718,19 @@ func (kpars Kparams) String() string { return sb.String() } -// Find returns the kparam with specified name. If it is not found, nil value is returned. -func (kpars Kparams) Find(name string) *Kparam { - kpar, err := kpars.findParam(name) +// Find returns the parameter with the specified name. If it is not found, nil value is returned. +func (pars Params) Find(name string) *Param { + par, err := pars.findParam(name) if err != nil { return nil } - return kpar + return par } // findParam lookups a parameter in the map and returns an error if it doesn't exist. -func (kpars Kparams) findParam(name string) (*Kparam, error) { - if _, ok := kpars[name]; !ok { - return nil, &kerrors.ErrKparamNotFound{Name: name} +func (pars Params) findParam(name string) (*Param, error) { + if _, ok := pars[name]; !ok { + return nil, &errors.ErrParamNotFound{Name: name} } - return kpars[name], nil + return pars[name], nil } diff --git a/pkg/event/param_test.go b/pkg/event/param_test.go new file mode 100644 index 000000000..a47b18dcb --- /dev/null +++ b/pkg/event/param_test.go @@ -0,0 +1,60 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package event + +import ( + "github.com/rabbitstack/fibratus/pkg/event/params" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestParams(t *testing.T) { + pars := Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(18446738026482168384)}, + params.ThreadID: {Name: params.ThreadID, Type: params.Uint32, Value: uint32(1484)}, + params.FileCreateOptions: {Name: params.FileCreateOptions, Type: params.Uint32, Value: uint32(1223456)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\kernel32.dll"}, + params.FileShareMask: {Name: params.FileShareMask, Type: params.Uint32, Value: uint32(5)}, + } + + assert.True(t, pars.Contains(params.FileObject)) + assert.False(t, pars.Contains(params.FileOffset)) + + filename, err := pars.GetString(params.FilePath) + require.NoError(t, err) + assert.Equal(t, "\\Device\\HarddiskVolume2\\Windows\\system32\\kernel32.dll", filename) + + _, err = pars.GetString(params.FileObject) + require.Error(t, err) + + assert.Equal(t, 5, pars.Len()) + + pars.Remove(params.ThreadID) + + assert.False(t, pars.Contains(params.ThreadID)) + assert.Equal(t, 4, pars.Len()) + + require.NoError(t, pars.Set(params.FileShareMask, uint32(5), params.Enum)) + + require.NoError(t, pars.SetValue(params.FilePath, "\\Device\\HarddiskVolume2\\Windows\\system32\\KERNEL32.dll")) + filename1, err := pars.GetString(params.FilePath) + require.NoError(t, err) + assert.Equal(t, "\\Device\\HarddiskVolume2\\Windows\\system32\\KERNEL32.dll", filename1) +} diff --git a/pkg/kevent/kparam_windows.go b/pkg/event/param_windows.go similarity index 50% rename from pkg/kevent/kparam_windows.go rename to pkg/event/param_windows.go index 5907d9224..021c04bb2 100644 --- a/pkg/kevent/kparam_windows.go +++ b/pkg/event/param_windows.go @@ -16,15 +16,14 @@ * limitations under the License. */ -package kevent +package event import ( "expvar" "fmt" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/fs" htypes "github.com/rabbitstack/fibratus/pkg/handle/types" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/rabbitstack/fibratus/pkg/sys/etw" "github.com/rabbitstack/fibratus/pkg/util/ip" "github.com/rabbitstack/fibratus/pkg/util/key" @@ -42,47 +41,47 @@ import ( // unknownKeysCount counts the number of times the registry key failed to convert from native format var unknownKeysCount = expvar.NewInt("registry.unknown.keys.count") -// NewKparam creates a new event parameter. Since the parameter type is already categorized, +// NewParam creates a new event parameter. Since the parameter type is already categorized, // we can coerce the value to the appropriate representation (e.g. hex, IP address) -func NewKparam(name string, typ kparams.Type, value kparams.Value, options ...ParamOption) *Kparam { +func NewParam(name string, typ params.Type, value params.Value, options ...ParamOption) *Param { var opts paramOpts for _, opt := range options { opt(&opts) } - var v kparams.Value + var v params.Value switch typ { - case kparams.IPv4: + case params.IPv4: v = ip.ToIPv4(value.(uint32)) - case kparams.IPv6: + case params.IPv6: v = ip.ToIPv6(value.([]byte)) - case kparams.Port: + case params.Port: v = windows.Ntohs(value.(uint16)) default: v = value } - return &Kparam{Name: name, Type: typ, Value: v, Flags: opts.flags, Enum: opts.enum} + return &Param{Name: name, Type: typ, Value: v, Flags: opts.flags, Enum: opts.enum} } var devMapper = fs.NewDevMapper() // String returns the string representation of the parameter value. -func (k Kparam) String() string { - if k.Value == nil { +func (p Param) String() string { + if p.Value == nil { return "" } - switch k.Type { - case kparams.UnicodeString, kparams.AnsiString, kparams.Path: - return k.Value.(string) - case kparams.SID, kparams.WbemSID: - sid, err := getSID(&k) + switch p.Type { + case params.UnicodeString, params.AnsiString, params.Path: + return p.Value.(string) + case params.SID, params.WbemSID: + sid, err := getSID(&p) if err != nil { return "" } return sid.String() - case kparams.DOSPath: - return devMapper.Convert(k.Value.(string)) - case kparams.Key: - rootKey, keyName := key.Format(k.Value.(string)) + case params.DOSPath: + return devMapper.Convert(p.Value.(string)) + case params.Key: + rootKey, keyName := key.Format(p.Value.(string)) if keyName != "" && rootKey != key.Invalid { return rootKey.String() + "\\" + keyName } @@ -91,100 +90,100 @@ func (k Kparam) String() string { } unknownKeysCount.Add(1) return keyName - case kparams.HandleType: - return htypes.ConvertTypeIDToName(k.Value.(uint16)) - case kparams.Status: - v, ok := k.Value.(uint32) + case params.HandleType: + return htypes.ConvertTypeIDToName(p.Value.(uint16)) + case params.Status: + v, ok := p.Value.(uint32) if !ok { return "" } return ntstatus.FormatMessage(v) - case kparams.Address: - v, ok := k.Value.(uint64) + case params.Address: + v, ok := p.Value.(uint64) if !ok { return "" } return va.Address(v).String() - case kparams.Int8: - return strconv.Itoa(int(k.Value.(int8))) - case kparams.Uint8: - return strconv.Itoa(int(k.Value.(uint8))) - case kparams.Int16: - return strconv.Itoa(int(k.Value.(int16))) - case kparams.Uint16, kparams.Port: - return strconv.Itoa(int(k.Value.(uint16))) - case kparams.Uint32, kparams.PID, kparams.TID: - return strconv.Itoa(int(k.Value.(uint32))) - case kparams.Int32: - return strconv.Itoa(int(k.Value.(int32))) - case kparams.Uint64: - return strconv.FormatUint(k.Value.(uint64), 10) - case kparams.Int64: - return strconv.Itoa(int(k.Value.(int64))) - case kparams.IPv4, kparams.IPv6: - return k.Value.(net.IP).String() - case kparams.Bool: - return strconv.FormatBool(k.Value.(bool)) - case kparams.Float: - return strconv.FormatFloat(float64(k.Value.(float32)), 'f', 6, 32) - case kparams.Double: - return strconv.FormatFloat(k.Value.(float64), 'f', 6, 64) - case kparams.Time: - return k.Value.(time.Time).String() - case kparams.Enum: - if k.Enum == nil { + case params.Int8: + return strconv.Itoa(int(p.Value.(int8))) + case params.Uint8: + return strconv.Itoa(int(p.Value.(uint8))) + case params.Int16: + return strconv.Itoa(int(p.Value.(int16))) + case params.Uint16, params.Port: + return strconv.Itoa(int(p.Value.(uint16))) + case params.Uint32, params.PID, params.TID: + return strconv.Itoa(int(p.Value.(uint32))) + case params.Int32: + return strconv.Itoa(int(p.Value.(int32))) + case params.Uint64: + return strconv.FormatUint(p.Value.(uint64), 10) + case params.Int64: + return strconv.Itoa(int(p.Value.(int64))) + case params.IPv4, params.IPv6: + return p.Value.(net.IP).String() + case params.Bool: + return strconv.FormatBool(p.Value.(bool)) + case params.Float: + return strconv.FormatFloat(float64(p.Value.(float32)), 'f', 6, 32) + case params.Double: + return strconv.FormatFloat(p.Value.(float64), 'f', 6, 64) + case params.Time: + return p.Value.(time.Time).String() + case params.Enum: + if p.Enum == nil { return "" } - e := k.Value + e := p.Value v, ok := e.(uint32) if !ok { return "" } - return k.Enum[v] - case kparams.Flags, kparams.Flags64: - if k.Flags == nil { + return p.Enum[v] + case params.Flags, params.Flags64: + if p.Flags == nil { return "" } - f := k.Value + f := p.Value switch v := f.(type) { case uint32: - return k.Flags.String(uint64(v)) + return p.Flags.String(uint64(v)) case uint64: - return k.Flags.String(v) + return p.Flags.String(v) default: return "" } - case kparams.Slice: - switch slice := k.Value.(type) { + case params.Slice: + switch slice := p.Value.(type) { case []string: return strings.Join(slice, ",") default: return fmt.Sprintf("%v", slice) } - case kparams.Binary: - return string(k.Value.([]byte)) + case params.Binary: + return string(p.Value.([]byte)) } - return fmt.Sprintf("%v", k.Value) + return fmt.Sprintf("%v", p.Value) } // GetSID returns the raw SID (Security Identifier) parameter as // typed representation on which various operations can be performed, // such as converting the SID to string or resolving username/domain. -func (kpars Kparams) GetSID() (*windows.SID, error) { - kpar, err := kpars.findParam(kparams.UserSID) +func (pars Params) GetSID() (*windows.SID, error) { + par, err := pars.findParam(params.UserSID) if err != nil { return nil, err } - return getSID(kpar) + return getSID(par) } -func getSID(kpar *Kparam) (*windows.SID, error) { - sid, ok := kpar.Value.([]byte) +func getSID(param *Param) (*windows.SID, error) { + sid, ok := param.Value.([]byte) if !ok { - return nil, fmt.Errorf("unable to type cast %q parameter to []byte value", kparams.UserSID) + return nil, fmt.Errorf("unable to type cast %q parameter to []byte value", params.UserSID) } b := uintptr(unsafe.Pointer(&sid[0])) - if kpar.Type == kparams.WbemSID { + if param.Type == params.WbemSID { // a WBEM SID is actually a TOKEN_USER structure followed // by the SID, so we have to double the pointer size b += uintptr(8 * 2) @@ -194,8 +193,8 @@ func getSID(kpar *Kparam) (*windows.SID, error) { // MustGetSID returns the SID (Security Identifier) event parameter // or panics if an error occurs. -func (kpars Kparams) MustGetSID() *windows.SID { - sid, err := kpars.GetSID() +func (pars Params) MustGetSID() *windows.SID { + sid, err := pars.GetSID() if err != nil { panic(err) } @@ -206,11 +205,11 @@ func (kpars Kparams) MustGetSID() *windows.SID { // the parameters. Each event is annotated with the schema // version number which helps us determine when the event // schema changes in order to parse new fields. -func (e *Kevent) produceParams(evt *etw.EventRecord) { +func (e *Event) produceParams(evt *etw.EventRecord) { switch e.Type { - case ktypes.ProcessRundown, - ktypes.CreateProcess, - ktypes.TerminateProcess: + case ProcessRundown, + CreateProcess, + TerminateProcess: var ( kproc uint64 pid, ppid uint32 @@ -251,35 +250,35 @@ func (e *Kevent) produceParams(evt *etw.EventRecord) { sid, soffset = evt.ReadSID(offset) name, noffset = evt.ReadAnsiString(soffset) cmdline, _ = evt.ReadUTF16String(soffset + noffset) - e.AppendParam(kparams.ProcessObject, kparams.Address, kproc) - e.AppendParam(kparams.ProcessID, kparams.PID, pid) - e.AppendParam(kparams.ProcessParentID, kparams.PID, ppid) - e.AppendParam(kparams.ProcessRealParentID, kparams.PID, evt.Header.ProcessID) - e.AppendParam(kparams.SessionID, kparams.Uint32, sessionID) - e.AppendParam(kparams.ExitStatus, kparams.Status, exitStatus) - e.AppendParam(kparams.DTB, kparams.Address, dtb) - e.AppendParam(kparams.ProcessFlags, kparams.Flags, flags, WithFlags(PsCreationFlags)) - e.AppendParam(kparams.UserSID, kparams.WbemSID, sid) - e.AppendParam(kparams.ProcessName, kparams.AnsiString, name) - e.AppendParam(kparams.Cmdline, kparams.UnicodeString, cmdline) - case ktypes.OpenProcess: + e.AppendParam(params.ProcessObject, params.Address, kproc) + e.AppendParam(params.ProcessID, params.PID, pid) + e.AppendParam(params.ProcessParentID, params.PID, ppid) + e.AppendParam(params.ProcessRealParentID, params.PID, evt.Header.ProcessID) + e.AppendParam(params.SessionID, params.Uint32, sessionID) + e.AppendParam(params.ExitStatus, params.Status, exitStatus) + e.AppendParam(params.DTB, params.Address, dtb) + e.AppendParam(params.ProcessFlags, params.Flags, flags, WithFlags(PsCreationFlags)) + e.AppendParam(params.UserSID, params.WbemSID, sid) + e.AppendParam(params.ProcessName, params.AnsiString, name) + e.AppendParam(params.Cmdline, params.UnicodeString, cmdline) + case OpenProcess: processID := evt.ReadUint32(0) desiredAccess := evt.ReadUint32(4) status := evt.ReadUint32(8) - e.AppendParam(kparams.ProcessID, kparams.PID, processID) - e.AppendParam(kparams.DesiredAccess, kparams.Flags, desiredAccess, WithFlags(PsAccessRightFlags)) - e.AppendParam(kparams.NTStatus, kparams.Status, status) + e.AppendParam(params.ProcessID, params.PID, processID) + e.AppendParam(params.DesiredAccess, params.Flags, desiredAccess, WithFlags(PsAccessRightFlags)) + e.AppendParam(params.NTStatus, params.Status, status) // append callstack for interested flags if desiredAccess == AllAccess || ((desiredAccess & windows.PROCESS_VM_READ) != 0) || ((desiredAccess & windows.PROCESS_VM_WRITE) != 0) || ((desiredAccess & windows.PROCESS_VM_OPERATION) != 0) || ((desiredAccess & windows.PROCESS_DUP_HANDLE) != 0) || ((desiredAccess & windows.PROCESS_TERMINATE) != 0) || ((desiredAccess & windows.PROCESS_CREATE_PROCESS) != 0) || ((desiredAccess & windows.PROCESS_CREATE_THREAD) != 0) || ((desiredAccess & windows.PROCESS_SET_INFORMATION) != 0) { - e.AppendParam(kparams.Callstack, kparams.Slice, evt.Callstack()) + e.AppendParam(params.Callstack, params.Slice, evt.Callstack()) } - case ktypes.CreateThread, - ktypes.TerminateThread, - ktypes.ThreadRundown: + case CreateThread, + TerminateThread, + ThreadRundown: var ( pid uint32 tid uint32 @@ -311,41 +310,41 @@ func (e *Kevent) produceParams(evt *etw.EventRecord) { pagePrio = evt.ReadByte(70) ioPrio = evt.ReadByte(71) } - e.AppendParam(kparams.ProcessID, kparams.PID, pid) - e.AppendParam(kparams.ThreadID, kparams.TID, tid) - e.AppendParam(kparams.KstackBase, kparams.Address, kstack) - e.AppendParam(kparams.KstackLimit, kparams.Address, klimit) - e.AppendParam(kparams.UstackBase, kparams.Address, ustack) - e.AppendParam(kparams.UstackLimit, kparams.Address, ulimit) - e.AppendParam(kparams.StartAddress, kparams.Address, startAddress) - e.AppendParam(kparams.TEB, kparams.Address, teb) - e.AppendParam(kparams.BasePrio, kparams.Uint8, basePrio) - e.AppendParam(kparams.PagePrio, kparams.Uint8, pagePrio) - e.AppendParam(kparams.IOPrio, kparams.Uint8, ioPrio) - case ktypes.OpenThread: + e.AppendParam(params.ProcessID, params.PID, pid) + e.AppendParam(params.ThreadID, params.TID, tid) + e.AppendParam(params.KstackBase, params.Address, kstack) + e.AppendParam(params.KstackLimit, params.Address, klimit) + e.AppendParam(params.UstackBase, params.Address, ustack) + e.AppendParam(params.UstackLimit, params.Address, ulimit) + e.AppendParam(params.StartAddress, params.Address, startAddress) + e.AppendParam(params.TEB, params.Address, teb) + e.AppendParam(params.BasePrio, params.Uint8, basePrio) + e.AppendParam(params.PagePrio, params.Uint8, pagePrio) + e.AppendParam(params.IOPrio, params.Uint8, ioPrio) + case OpenThread: processID := evt.ReadUint32(0) threadID := evt.ReadUint32(4) desiredAccess := evt.ReadUint32(8) status := evt.ReadUint32(12) - e.AppendParam(kparams.ProcessID, kparams.PID, processID) - e.AppendParam(kparams.ThreadID, kparams.TID, threadID) - e.AppendParam(kparams.DesiredAccess, kparams.Flags, desiredAccess, WithFlags(ThreadAccessRightFlags)) - e.AppendParam(kparams.NTStatus, kparams.Status, status) + e.AppendParam(params.ProcessID, params.PID, processID) + e.AppendParam(params.ThreadID, params.TID, threadID) + e.AppendParam(params.DesiredAccess, params.Flags, desiredAccess, WithFlags(ThreadAccessRightFlags)) + e.AppendParam(params.NTStatus, params.Status, status) // append callstack for interested flags if desiredAccess == AllAccess || ((desiredAccess & windows.THREAD_SET_CONTEXT) != 0) || ((desiredAccess & windows.THREAD_SET_THREAD_TOKEN) != 0) || ((desiredAccess & windows.THREAD_IMPERSONATE) != 0) || ((desiredAccess & windows.THREAD_DIRECT_IMPERSONATION) != 0) || ((desiredAccess & windows.THREAD_SUSPEND_RESUME) != 0) || ((desiredAccess & windows.THREAD_TERMINATE) != 0) || ((desiredAccess & windows.THREAD_SET_INFORMATION) != 0) { - e.AppendParam(kparams.Callstack, kparams.Slice, evt.Callstack()) + e.AppendParam(params.Callstack, params.Slice, evt.Callstack()) } - case ktypes.SetThreadContext: + case SetThreadContext: status := evt.ReadUint32(0) - e.AppendParam(kparams.NTStatus, kparams.Status, status) + e.AppendParam(params.NTStatus, params.Status, status) if evt.HasStackTrace() { - e.AppendParam(kparams.Callstack, kparams.Slice, evt.Callstack()) + e.AppendParam(params.Callstack, params.Slice, evt.Callstack()) } - case ktypes.CreateHandle, ktypes.CloseHandle: + case CreateHandle, CloseHandle: object := evt.ReadUint64(0) handleID := evt.ReadUint32(8) typeID := evt.ReadUint16(12) @@ -353,26 +352,26 @@ func (e *Kevent) produceParams(evt *etw.EventRecord) { if evt.BufferLen >= 16 { handleName = evt.ConsumeUTF16String(14) } - e.AppendParam(kparams.HandleObject, kparams.Address, object) - e.AppendParam(kparams.HandleID, kparams.Uint32, handleID) - e.AppendParam(kparams.HandleObjectTypeID, kparams.HandleType, typeID) - e.AppendParam(kparams.HandleObjectName, kparams.UnicodeString, handleName) - case ktypes.DuplicateHandle: + e.AppendParam(params.HandleObject, params.Address, object) + e.AppendParam(params.HandleID, params.Uint32, handleID) + e.AppendParam(params.HandleObjectTypeID, params.HandleType, typeID) + e.AppendParam(params.HandleObjectName, params.UnicodeString, handleName) + case DuplicateHandle: object := evt.ReadUint64(0) srcHandleID := evt.ReadUint32(8) dstHandleID := evt.ReadUint32(12) targetPID := evt.ReadUint32(16) typeID := evt.ReadUint16(20) sourcePID := evt.ReadUint32(22) - e.AppendParam(kparams.HandleObject, kparams.Address, object) - e.AppendParam(kparams.HandleID, kparams.Uint32, dstHandleID) - e.AppendParam(kparams.HandleSourceID, kparams.Uint32, srcHandleID) - e.AppendParam(kparams.HandleObjectTypeID, kparams.HandleType, typeID) - e.AppendParam(kparams.ProcessID, kparams.PID, sourcePID) - e.AppendParam(kparams.TargetProcessID, kparams.PID, targetPID) - case ktypes.LoadImage, - ktypes.UnloadImage, - ktypes.ImageRundown: + e.AppendParam(params.HandleObject, params.Address, object) + e.AppendParam(params.HandleID, params.Uint32, dstHandleID) + e.AppendParam(params.HandleSourceID, params.Uint32, srcHandleID) + e.AppendParam(params.HandleObjectTypeID, params.HandleType, typeID) + e.AppendParam(params.ProcessID, params.PID, sourcePID) + e.AppendParam(params.TargetProcessID, params.PID, targetPID) + case LoadImage, + UnloadImage, + ImageRundown: var ( pid uint32 checksum uint32 @@ -406,20 +405,20 @@ func (e *Kevent) produceParams(evt *etw.EventRecord) { offset = 16 } filename = evt.ConsumeUTF16String(offset) - e.AppendParam(kparams.ProcessID, kparams.PID, pid) - e.AppendParam(kparams.ImageCheckSum, kparams.Uint32, checksum) - e.AppendParam(kparams.ImageDefaultBase, kparams.Address, defaultBase) - e.AppendParam(kparams.ImageBase, kparams.Address, imageBase) - e.AppendParam(kparams.ImageSize, kparams.Uint64, imageSize) - e.AppendParam(kparams.ImagePath, kparams.DOSPath, filename) - e.AppendParam(kparams.ImageSignatureLevel, kparams.Enum, uint32(sigLevel), WithEnum(signature.Levels)) - e.AppendParam(kparams.ImageSignatureType, kparams.Enum, uint32(sigType), WithEnum(signature.Types)) - case ktypes.RegOpenKey, ktypes.RegCloseKey, - ktypes.RegCreateKCB, ktypes.RegDeleteKCB, - ktypes.RegKCBRundown, ktypes.RegCreateKey, - ktypes.RegDeleteKey, ktypes.RegDeleteValue, - ktypes.RegQueryKey, ktypes.RegQueryValue, - ktypes.RegSetValue: + e.AppendParam(params.ProcessID, params.PID, pid) + e.AppendParam(params.ImageCheckSum, params.Uint32, checksum) + e.AppendParam(params.ImageDefaultBase, params.Address, defaultBase) + e.AppendParam(params.ImageBase, params.Address, imageBase) + e.AppendParam(params.ImageSize, params.Uint64, imageSize) + e.AppendParam(params.ImagePath, params.DOSPath, filename) + e.AppendParam(params.ImageSignatureLevel, params.Enum, uint32(sigLevel), WithEnum(signature.Levels)) + e.AppendParam(params.ImageSignatureType, params.Enum, uint32(sigType), WithEnum(signature.Types)) + case RegOpenKey, RegCloseKey, + RegCreateKCB, RegDeleteKCB, + RegKCBRundown, RegCreateKey, + RegDeleteKey, RegDeleteValue, + RegQueryKey, RegQueryValue, + RegSetValue: var ( status uint32 keyHandle uint64 @@ -437,10 +436,10 @@ func (e *Kevent) produceParams(evt *etw.EventRecord) { } else { keyName = evt.ConsumeUTF16String(20) } - e.AppendParam(kparams.RegKeyHandle, kparams.Address, keyHandle) - e.AppendParam(kparams.RegPath, kparams.Key, keyName) - e.AppendParam(kparams.NTStatus, kparams.Status, status) - case ktypes.CreateFile: + e.AppendParam(params.RegKeyHandle, params.Address, keyHandle) + e.AppendParam(params.RegPath, params.Key, keyName) + e.AppendParam(params.NTStatus, params.Status, status) + case CreateFile: var ( irp uint64 fileObject uint64 @@ -462,14 +461,14 @@ func (e *Kevent) produceParams(evt *etw.EventRecord) { fileObject = evt.ReadUint64(0) filename = evt.ConsumeUTF16String(8) } - e.AppendParam(kparams.FileIrpPtr, kparams.Address, irp) - e.AppendParam(kparams.FileObject, kparams.Address, fileObject) - e.AppendParam(kparams.ThreadID, kparams.TID, tid) - e.AppendParam(kparams.FileShareMask, kparams.Flags, shareAccess, WithFlags(FileShareModeFlags)) - e.AppendParam(kparams.FileAttributes, kparams.Flags, fileAttributes, WithFlags(FileAttributeFlags)) - e.AppendParam(kparams.FileCreateOptions, kparams.Flags, createOptions, WithFlags(FileCreateOptionsFlags)) - e.AppendParam(kparams.FilePath, kparams.DOSPath, filename) - case ktypes.FileOpEnd: + e.AppendParam(params.FileIrpPtr, params.Address, irp) + e.AppendParam(params.FileObject, params.Address, fileObject) + e.AppendParam(params.ThreadID, params.TID, tid) + e.AppendParam(params.FileShareMask, params.Flags, shareAccess, WithFlags(FileShareModeFlags)) + e.AppendParam(params.FileAttributes, params.Flags, fileAttributes, WithFlags(FileAttributeFlags)) + e.AppendParam(params.FileCreateOptions, params.Flags, createOptions, WithFlags(FileCreateOptionsFlags)) + e.AppendParam(params.FilePath, params.DOSPath, filename) + case FileOpEnd: var ( irp uint64 extraInfo uint64 @@ -480,10 +479,10 @@ func (e *Kevent) produceParams(evt *etw.EventRecord) { extraInfo = evt.ReadUint64(8) status = evt.ReadUint32(16) } - e.AppendParam(kparams.FileIrpPtr, kparams.Address, irp) - e.AppendParam(kparams.FileExtraInfo, kparams.Address, extraInfo) - e.AppendParam(kparams.NTStatus, kparams.Status, status) - case ktypes.FileRundown: + e.AppendParam(params.FileIrpPtr, params.Address, irp) + e.AppendParam(params.FileExtraInfo, params.Address, extraInfo) + e.AppendParam(params.NTStatus, params.Status, status) + case FileRundown: var ( fileObject uint64 filename string @@ -492,9 +491,9 @@ func (e *Kevent) produceParams(evt *etw.EventRecord) { fileObject = evt.ReadUint64(0) filename = evt.ConsumeUTF16String(8) } - e.AppendParam(kparams.FileObject, kparams.Address, fileObject) - e.AppendParam(kparams.FilePath, kparams.DOSPath, filename) - case ktypes.ReleaseFile, ktypes.CloseFile: + e.AppendParam(params.FileObject, params.Address, fileObject) + e.AppendParam(params.FilePath, params.DOSPath, filename) + case ReleaseFile, CloseFile: var ( irp uint64 fileObject uint64 @@ -509,13 +508,13 @@ func (e *Kevent) produceParams(evt *etw.EventRecord) { fileKey = evt.ReadUint64(16) tid = evt.ReadUint32(24) } - e.AppendParam(kparams.FileIrpPtr, kparams.Address, irp) - e.AppendParam(kparams.FileObject, kparams.Address, fileObject) - e.AppendParam(kparams.FileKey, kparams.Address, fileKey) - e.AppendParam(kparams.ThreadID, kparams.TID, tid) - case ktypes.DeleteFile, - ktypes.RenameFile, - ktypes.SetFileInformation: + e.AppendParam(params.FileIrpPtr, params.Address, irp) + e.AppendParam(params.FileObject, params.Address, fileObject) + e.AppendParam(params.FileKey, params.Address, fileKey) + e.AppendParam(params.ThreadID, params.TID, tid) + case DeleteFile, + RenameFile, + SetFileInformation: var ( irp uint64 fileObject uint64 @@ -539,13 +538,13 @@ func (e *Kevent) produceParams(evt *etw.EventRecord) { fileKey = evt.ReadUint64(18) extraInfo = evt.ReadUint64(28) } - e.AppendParam(kparams.FileIrpPtr, kparams.Address, irp) - e.AppendParam(kparams.FileObject, kparams.Address, fileObject) - e.AppendParam(kparams.FileKey, kparams.Address, fileKey) - e.AppendParam(kparams.ThreadID, kparams.TID, tid) - e.AppendParam(kparams.FileExtraInfo, kparams.Uint64, extraInfo) - e.AppendParam(kparams.FileInfoClass, kparams.Enum, infoClass, WithEnum(fs.FileInfoClasses)) - case ktypes.ReadFile, ktypes.WriteFile: + e.AppendParam(params.FileIrpPtr, params.Address, irp) + e.AppendParam(params.FileObject, params.Address, fileObject) + e.AppendParam(params.FileKey, params.Address, fileKey) + e.AppendParam(params.ThreadID, params.TID, tid) + e.AppendParam(params.FileExtraInfo, params.Uint64, extraInfo) + e.AppendParam(params.FileInfoClass, params.Enum, infoClass, WithEnum(fs.FileInfoClasses)) + case ReadFile, WriteFile: var ( irp uint64 offset uint64 @@ -568,13 +567,13 @@ func (e *Kevent) produceParams(evt *etw.EventRecord) { fileKey = evt.ReadUint64(28) tid = evt.ReadUint32(16) } - e.AppendParam(kparams.FileIrpPtr, kparams.Address, irp) - e.AppendParam(kparams.FileObject, kparams.Address, fileObject) - e.AppendParam(kparams.FileKey, kparams.Address, fileKey) - e.AppendParam(kparams.ThreadID, kparams.TID, tid) - e.AppendParam(kparams.FileOffset, kparams.Uint64, offset) - e.AppendParam(kparams.FileIoSize, kparams.Uint32, size) - case ktypes.EnumDirectory: + e.AppendParam(params.FileIrpPtr, params.Address, irp) + e.AppendParam(params.FileObject, params.Address, fileObject) + e.AppendParam(params.FileKey, params.Address, fileKey) + e.AppendParam(params.ThreadID, params.TID, tid) + e.AppendParam(params.FileOffset, params.Uint64, offset) + e.AppendParam(params.FileIoSize, params.Uint32, size) + case EnumDirectory: var ( irp uint64 fileObject uint64 @@ -597,13 +596,13 @@ func (e *Kevent) produceParams(evt *etw.EventRecord) { fileObject = evt.ReadUint64(12) fileKey = evt.ReadUint64(20) } - e.AppendParam(kparams.FileIrpPtr, kparams.Address, irp) - e.AppendParam(kparams.FileObject, kparams.Address, fileObject) - e.AppendParam(kparams.ThreadID, kparams.TID, tid) - e.AppendParam(kparams.FileKey, kparams.Address, fileKey) - e.AppendParam(kparams.FilePath, kparams.UnicodeString, filename) - e.AppendParam(kparams.FileInfoClass, kparams.Enum, infoClass, WithEnum(fs.FileInfoClasses)) - case ktypes.MapViewFile, ktypes.UnmapViewFile, ktypes.MapFileRundown: + e.AppendParam(params.FileIrpPtr, params.Address, irp) + e.AppendParam(params.FileObject, params.Address, fileObject) + e.AppendParam(params.ThreadID, params.TID, tid) + e.AppendParam(params.FileKey, params.Address, fileKey) + e.AppendParam(params.FilePath, params.UnicodeString, filename) + e.AppendParam(params.FileInfoClass, params.Enum, infoClass, WithEnum(fs.FileInfoClasses)) + case MapViewFile, UnmapViewFile, MapFileRundown: var ( viewBase uint64 fileKey uint64 @@ -626,22 +625,22 @@ func (e *Kevent) produceParams(evt *etw.EventRecord) { } protect := uint32(extraInfo >> 32) section := uint32(extraInfo >> 52) - e.AppendParam(kparams.FileViewBase, kparams.Address, viewBase) - e.AppendParam(kparams.FileKey, kparams.Address, fileKey) - e.AppendParam(kparams.FileViewSize, kparams.Uint64, viewSize) - e.AppendParam(kparams.FileOffset, kparams.Uint64, offset) - e.AppendParam(kparams.ProcessID, kparams.PID, pid) - e.AppendParam(kparams.MemProtect, kparams.Flags, protect, WithFlags(ViewProtectionFlags)) - e.AppendParam(kparams.FileViewSectionType, kparams.Enum, section, WithEnum(ViewSectionTypes)) - case ktypes.SendTCPv4, - ktypes.SendUDPv4, - ktypes.RecvTCPv4, - ktypes.RecvUDPv4, - ktypes.DisconnectTCPv4, - ktypes.RetransmitTCPv4, - ktypes.ReconnectTCPv4, - ktypes.ConnectTCPv4, - ktypes.AcceptTCPv4: + e.AppendParam(params.FileViewBase, params.Address, viewBase) + e.AppendParam(params.FileKey, params.Address, fileKey) + e.AppendParam(params.FileViewSize, params.Uint64, viewSize) + e.AppendParam(params.FileOffset, params.Uint64, offset) + e.AppendParam(params.ProcessID, params.PID, pid) + e.AppendParam(params.MemProtect, params.Flags, protect, WithFlags(ViewProtectionFlags)) + e.AppendParam(params.FileViewSectionType, params.Enum, section, WithEnum(ViewSectionTypes)) + case SendTCPv4, + SendUDPv4, + RecvTCPv4, + RecvUDPv4, + DisconnectTCPv4, + RetransmitTCPv4, + ReconnectTCPv4, + ConnectTCPv4, + AcceptTCPv4: var ( pid uint32 size uint32 @@ -665,21 +664,21 @@ func (e *Kevent) produceParams(evt *etw.EventRecord) { size = evt.ReadUint32(12) pid = evt.ReadUint32(16) } - e.AppendParam(kparams.ProcessID, kparams.PID, pid) - e.AppendParam(kparams.NetSize, kparams.Uint32, size) - e.AppendParam(kparams.NetDIP, kparams.IPv4, dip) - e.AppendParam(kparams.NetSIP, kparams.IPv4, sip) - e.AppendParam(kparams.NetDport, kparams.Port, dport) - e.AppendParam(kparams.NetSport, kparams.Port, sport) - case ktypes.SendTCPv6, - ktypes.SendUDPv6, - ktypes.RecvTCPv6, - ktypes.RecvUDPv6, - ktypes.DisconnectTCPv6, - ktypes.RetransmitTCPv6, - ktypes.ReconnectTCPv6, - ktypes.ConnectTCPv6, - ktypes.AcceptTCPv6: + e.AppendParam(params.ProcessID, params.PID, pid) + e.AppendParam(params.NetSize, params.Uint32, size) + e.AppendParam(params.NetDIP, params.IPv4, dip) + e.AppendParam(params.NetSIP, params.IPv4, sip) + e.AppendParam(params.NetDport, params.Port, dport) + e.AppendParam(params.NetSport, params.Port, sport) + case SendTCPv6, + SendUDPv6, + RecvTCPv6, + RecvUDPv6, + DisconnectTCPv6, + RetransmitTCPv6, + ReconnectTCPv6, + ConnectTCPv6, + AcceptTCPv6: var ( pid uint32 size uint32 @@ -696,13 +695,13 @@ func (e *Kevent) produceParams(evt *etw.EventRecord) { dport = evt.ReadUint16(40) sport = evt.ReadUint16(42) } - e.AppendParam(kparams.ProcessID, kparams.PID, pid) - e.AppendParam(kparams.NetSize, kparams.Uint32, size) - e.AppendParam(kparams.NetDIP, kparams.IPv6, dip) - e.AppendParam(kparams.NetSIP, kparams.IPv6, sip) - e.AppendParam(kparams.NetDport, kparams.Port, dport) - e.AppendParam(kparams.NetSport, kparams.Port, sport) - case ktypes.VirtualAlloc, ktypes.VirtualFree: + e.AppendParam(params.ProcessID, params.PID, pid) + e.AppendParam(params.NetSize, params.Uint32, size) + e.AppendParam(params.NetDIP, params.IPv6, dip) + e.AppendParam(params.NetSIP, params.IPv6, sip) + e.AppendParam(params.NetDport, params.Port, dport) + e.AppendParam(params.NetSport, params.Port, sport) + case VirtualAlloc, VirtualFree: var ( baseAddress uint64 regionSize uint64 @@ -715,11 +714,11 @@ func (e *Kevent) produceParams(evt *etw.EventRecord) { pid = evt.ReadUint32(16) flags = evt.ReadUint32(20) } - e.AppendParam(kparams.MemBaseAddress, kparams.Address, baseAddress) - e.AppendParam(kparams.MemRegionSize, kparams.Uint64, regionSize) - e.AppendParam(kparams.ProcessID, kparams.PID, pid) - e.AppendParam(kparams.MemAllocType, kparams.Flags, flags, WithFlags(MemAllocationFlags)) - case ktypes.QueryDNS, ktypes.ReplyDNS: + e.AppendParam(params.MemBaseAddress, params.Address, baseAddress) + e.AppendParam(params.MemRegionSize, params.Uint64, regionSize) + e.AppendParam(params.ProcessID, params.PID, pid) + e.AppendParam(params.MemAllocType, params.Flags, flags, WithFlags(MemAllocationFlags)) + case QueryDNS, ReplyDNS: var ( name string rr uint32 @@ -729,18 +728,18 @@ func (e *Kevent) produceParams(evt *etw.EventRecord) { name, offset = evt.ReadUTF16String(0) rr = evt.ReadUint32(offset) opts = evt.ReadUint64(offset + 4) - e.AppendParam(kparams.DNSName, kparams.UnicodeString, name) - e.AppendParam(kparams.DNSRR, kparams.Enum, rr, WithEnum(DNSRecordTypes)) - e.AppendParam(kparams.DNSOpts, kparams.Flags64, opts, WithFlags(DNSOptsFlags)) - if e.Type == ktypes.ReplyDNS { + e.AppendParam(params.DNSName, params.UnicodeString, name) + e.AppendParam(params.DNSRR, params.Enum, rr, WithEnum(DNSRecordTypes)) + e.AppendParam(params.DNSOpts, params.Flags64, opts, WithFlags(DNSOptsFlags)) + if e.Type == ReplyDNS { rcode := evt.ReadUint32(offset + 12) answers := evt.ConsumeUTF16String(offset + 16) - e.AppendParam(kparams.DNSRcode, kparams.Enum, rcode, WithEnum(DNSResponseCodes)) - e.AppendParam(kparams.DNSAnswers, kparams.Slice, strings.Split(sanitizeDNSAnswers(answers), ";")) + e.AppendParam(params.DNSRcode, params.Enum, rcode, WithEnum(DNSResponseCodes)) + e.AppendParam(params.DNSAnswers, params.Slice, strings.Split(sanitizeDNSAnswers(answers), ";")) } - case ktypes.StackWalk: - e.AppendParam(kparams.ProcessID, kparams.PID, evt.ReadUint32(8)) - e.AppendParam(kparams.ThreadID, kparams.TID, evt.ReadUint32(12)) + case StackWalk: + e.AppendParam(params.ProcessID, params.PID, evt.ReadUint32(8)) + e.AppendParam(params.ThreadID, params.TID, evt.ReadUint32(12)) var n uint16 var offset uint16 = 16 frames := (evt.BufferLen - offset) / 8 @@ -750,43 +749,43 @@ func (e *Kevent) produceParams(evt *etw.EventRecord) { offset += 8 n++ } - e.AppendParam(kparams.Callstack, kparams.Slice, callstack) - case ktypes.CreateSymbolicLinkObject: + e.AppendParam(params.Callstack, params.Slice, callstack) + case CreateSymbolicLinkObject: source, offset := evt.ReadUTF16String(0) target, offset := evt.ReadUTF16String(offset) desiredAccess := evt.ReadUint32(offset) status := evt.ReadUint32(offset + 4) - e.AppendParam(kparams.LinkSource, kparams.UnicodeString, source) - e.AppendParam(kparams.LinkTarget, kparams.UnicodeString, target) - e.AppendParam(kparams.DesiredAccess, kparams.Flags, desiredAccess, WithFlags(AccessMaskFlags)) - e.AppendParam(kparams.NTStatus, kparams.Status, status) + e.AppendParam(params.LinkSource, params.UnicodeString, source) + e.AppendParam(params.LinkTarget, params.UnicodeString, target) + e.AppendParam(params.DesiredAccess, params.Flags, desiredAccess, WithFlags(AccessMaskFlags)) + e.AppendParam(params.NTStatus, params.Status, status) if evt.HasStackTrace() { - e.AppendParam(kparams.Callstack, kparams.Slice, evt.Callstack()) + e.AppendParam(params.Callstack, params.Slice, evt.Callstack()) } - case ktypes.SubmitThreadpoolWork, ktypes.SubmitThreadpoolCallback: + case SubmitThreadpoolWork, SubmitThreadpoolCallback: poolID := evt.ReadUint64(0) taskID := evt.ReadUint64(8) callback := evt.ReadUint64(16) ctx := evt.ReadUint64(24) tag := evt.ReadUint64(32) - e.AppendParam(kparams.ThreadpoolPoolID, kparams.Address, poolID) - e.AppendParam(kparams.ThreadpoolTaskID, kparams.Address, taskID) - e.AppendParam(kparams.ThreadpoolCallback, kparams.Address, callback) - e.AppendParam(kparams.ThreadpoolContext, kparams.Address, ctx) - e.AppendParam(kparams.ThreadpoolSubprocessTag, kparams.Address, tag) - case ktypes.SetThreadpoolTimer: + e.AppendParam(params.ThreadpoolPoolID, params.Address, poolID) + e.AppendParam(params.ThreadpoolTaskID, params.Address, taskID) + e.AppendParam(params.ThreadpoolCallback, params.Address, callback) + e.AppendParam(params.ThreadpoolContext, params.Address, ctx) + e.AppendParam(params.ThreadpoolSubprocessTag, params.Address, tag) + case SetThreadpoolTimer: duetime := evt.ReadUint64(0) subqueue := evt.ReadUint64(8) timer := evt.ReadUint64(16) period := evt.ReadUint32(24) window := evt.ReadUint32(28) absolute := evt.ReadUint32(32) - e.AppendParam(kparams.ThreadpoolTimerDuetime, kparams.Uint64, duetime) - e.AppendParam(kparams.ThreadpoolTimerSubqueue, kparams.Address, subqueue) - e.AppendParam(kparams.ThreadpoolTimer, kparams.Address, timer) - e.AppendParam(kparams.ThreadpoolTimerPeriod, kparams.Uint32, period) - e.AppendParam(kparams.ThreadpoolTimerWindow, kparams.Uint32, window) - e.AppendParam(kparams.ThreadpoolTimerAbsolute, kparams.Bool, absolute > 0) + e.AppendParam(params.ThreadpoolTimerDuetime, params.Uint64, duetime) + e.AppendParam(params.ThreadpoolTimerSubqueue, params.Address, subqueue) + e.AppendParam(params.ThreadpoolTimer, params.Address, timer) + e.AppendParam(params.ThreadpoolTimerPeriod, params.Uint32, period) + e.AppendParam(params.ThreadpoolTimerWindow, params.Uint32, window) + e.AppendParam(params.ThreadpoolTimerAbsolute, params.Bool, absolute > 0) } } diff --git a/pkg/kevent/kparams/fields_windows.go b/pkg/event/params/params_windows.go similarity index 99% rename from pkg/kevent/kparams/fields_windows.go rename to pkg/event/params/params_windows.go index e692394d1..afed096c0 100644 --- a/pkg/kevent/kparams/fields_windows.go +++ b/pkg/event/params/params_windows.go @@ -16,7 +16,7 @@ * limitations under the License. */ -package kparams +package params const ( // NTStatus is the parameter that identifies the NTSTATUS value. diff --git a/pkg/event/params/types.go b/pkg/event/params/types.go new file mode 100644 index 000000000..e492cb3e6 --- /dev/null +++ b/pkg/event/params/types.go @@ -0,0 +1,27 @@ +/* + * Copyright 2021-present by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package params + +const ( + // NA defines absent parameter's value + NA = "na" +) + +// Value defines the container for parameter values +type Value interface{} diff --git a/pkg/kevent/kparams/types_windows.go b/pkg/event/params/types_windows.go similarity index 93% rename from pkg/kevent/kparams/types_windows.go rename to pkg/event/params/types_windows.go index 441cfa1f6..aec8c363d 100644 --- a/pkg/kevent/kparams/types_windows.go +++ b/pkg/event/params/types_windows.go @@ -1,5 +1,5 @@ /* - * Copyright 2019-2020 by Nedim Sabic Sabic + * Copyright 2021-present by Nedim Sabic Sabic * https://www.fibratus.io * All Rights Reserved. * @@ -16,17 +16,9 @@ * limitations under the License. */ -package kparams +package params -const ( - // NA defines absent parameter's value - NA = "na" -) - -// Value defines the container for parameter values -type Value interface{} - -// Type defines kernel event parameter type +// Type defines event parameter type type Type uint16 const ( diff --git a/pkg/kevent/queue.go b/pkg/event/queue.go similarity index 90% rename from pkg/kevent/queue.go rename to pkg/event/queue.go index 9a9a801e4..cf185f4ee 100644 --- a/pkg/kevent/queue.go +++ b/pkg/event/queue.go @@ -16,7 +16,7 @@ * limitations under the License. */ -package kevent +package event import ( "expvar" @@ -38,7 +38,7 @@ type Listener interface { // indicating if the event should continue the processing journey. // In case any errors occur during processing, this method returns // the error and stops further event processing. - ProcessEvent(*Kevent) (bool, error) + ProcessEvent(*Event) (bool, error) // CanEnqueue indicates if the event listener is capable of // submitting the event to the output queue if the ProcessEvent // method returns true. In general, processors that merely @@ -50,7 +50,7 @@ type Listener interface { // Queue is the channel-backed data structure for // pushing captured events and invoking listeners. type Queue struct { - q chan *Kevent + q chan *Event listeners []Listener backlog *backlog decorator *StackwalkDecorator @@ -61,7 +61,7 @@ type Queue struct { // NewQueue constructs a new queue with the given channel size. func NewQueue(size int, stackEnrichment bool, enqueueAlways bool) *Queue { q := &Queue{ - q: make(chan *Kevent, size), + q: make(chan *Event, size), listeners: make([]Listener, 0), backlog: newBacklog(backlogCacheSize), stackEnrichment: stackEnrichment, @@ -72,7 +72,7 @@ func NewQueue(size int, stackEnrichment bool, enqueueAlways bool) *Queue { } // NewQueueWithChannel constructs a new queue with a custom channel. -func NewQueueWithChannel(ch chan *Kevent, stackEnrichment bool, enqueueAlways bool) *Queue { +func NewQueueWithChannel(ch chan *Event, stackEnrichment bool, enqueueAlways bool) *Queue { q := &Queue{ q: ch, listeners: make([]Listener, 0), @@ -91,7 +91,7 @@ func (q *Queue) RegisterListener(listener Listener) { } // Events returns the channel with all queued events. -func (q *Queue) Events() <-chan *Kevent { return q.q } +func (q *Queue) Events() <-chan *Event { return q.q } // Close closes the queue disposing allocated resources. func (q *Queue) Close() { q.decorator.Stop() } @@ -116,7 +116,7 @@ func (q *Queue) Close() { q.decorator.Stop() } // Then, the originating event is popped from the queue, // enriched with callstack parameter and forwarded to the // event queue. -func (q *Queue) Push(e *Kevent) error { +func (q *Queue) Push(e *Event) error { if q.stackEnrichment { // store pending event for callstack enrichment if e.Type.CanEnrichStack() { @@ -143,7 +143,7 @@ func (q *Queue) Push(e *Kevent) error { return q.push(e) } -func (q *Queue) push(e *Kevent) error { +func (q *Queue) push(e *Event) error { var enqueue bool if q.enqueueAlways { enqueue = true @@ -158,7 +158,7 @@ func (q *Queue) push(e *Kevent) error { } } if q.stackEnrichment && e.IsTerminateThread() { - id := uint64(e.Kparams.MustGetPid() + e.Kparams.MustGetTid()) + id := uint64(e.Params.MustGetPid() + e.Params.MustGetTid()) q.decorator.RemoveBucket(id) } if enqueue || len(q.listeners) == 0 { @@ -168,7 +168,7 @@ func (q *Queue) push(e *Kevent) error { return nil } -func isEventDelayed(e *Kevent) bool { +func isEventDelayed(e *Event) bool { return e.IsCreateHandle() } @@ -180,7 +180,7 @@ func newBacklog(size int) *backlog { return &backlog{cache: lru.New(size)} } -func (b *backlog) put(evt *Kevent) { +func (b *backlog) put(evt *Event) { if b.cache.Len() > backlogCacheSize { b.cache.RemoveOldest() } @@ -190,7 +190,7 @@ func (b *backlog) put(evt *Kevent) { } } -func (b *backlog) pop(evt *Kevent) *Kevent { +func (b *backlog) pop(evt *Event) *Event { key := evt.BacklogKey() if key == 0 { return nil @@ -200,7 +200,7 @@ func (b *backlog) pop(evt *Kevent) *Kevent { return nil } b.cache.Remove(key) - e := ev.(*Kevent) + e := ev.(*Event) e.CopyState(evt) return e } diff --git a/pkg/kevent/queue_test.go b/pkg/event/queue_test.go similarity index 51% rename from pkg/kevent/queue_test.go rename to pkg/event/queue_test.go index bc4ebf310..56dea2d83 100644 --- a/pkg/kevent/queue_test.go +++ b/pkg/event/queue_test.go @@ -16,13 +16,12 @@ * limitations under the License. */ -package kevent +package event import ( "errors" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/fs" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -38,9 +37,9 @@ type AddParamListener struct { func (l *AddParamListener) CanEnqueue() bool { return true } -func (l *AddParamListener) ProcessEvent(e *Kevent) (bool, error) { +func (l *AddParamListener) ProcessEvent(e *Event) (bool, error) { args := l.Called(e) - e.AppendParam(kparams.FileAttributes, kparams.AnsiString, "HIDDEN") + e.AppendParam(params.FileAttributes, params.AnsiString, "HIDDEN") return args.Bool(0), args.Error(1) } @@ -49,7 +48,7 @@ type DummyListener struct{} func (l *DummyListener) CanEnqueue() bool { return true } -func (l *DummyListener) ProcessEvent(e *Kevent) (bool, error) { +func (l *DummyListener) ProcessEvent(e *Event) (bool, error) { return true, nil } @@ -58,7 +57,7 @@ var ErrCantEnqueue = errors.New("cannot push event into the queue") func TestQueuePush(t *testing.T) { var tests = []struct { name string - e *Kevent + e *Event err error listeners func() []Listener enqueueAlways bool @@ -66,20 +65,20 @@ func TestQueuePush(t *testing.T) { }{ { "push event ok", - &Kevent{ - Type: ktypes.CreateFile, + &Event{ + Type: CreateFile, Tid: 2484, PID: 859, CPU: 1, Seq: 2, Name: "CreateFile", Timestamp: time.Now(), - Category: ktypes.File, - Kparams: Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.Enum, Value: uint32(1), Enum: fs.FileCreateDispositions}, + Category: File, + Params: Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.Enum, Value: uint32(1), Enum: fs.FileCreateDispositions}, }, }, nil, @@ -93,20 +92,20 @@ func TestQueuePush(t *testing.T) { }, { "push event listener error", - &Kevent{ - Type: ktypes.CreateFile, + &Event{ + Type: CreateFile, Tid: 2484, PID: 859, CPU: 1, Seq: 2, Name: "CreateFile", Timestamp: time.Now(), - Category: ktypes.File, - Kparams: Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.Enum, Value: uint32(1), Enum: fs.FileCreateDispositions}, + Category: File, + Params: Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.Enum, Value: uint32(1), Enum: fs.FileCreateDispositions}, }, }, ErrCantEnqueue, @@ -120,20 +119,20 @@ func TestQueuePush(t *testing.T) { }, { "push event one listener allows", - &Kevent{ - Type: ktypes.CreateFile, + &Event{ + Type: CreateFile, Tid: 2484, PID: 859, CPU: 1, Seq: 2, Name: "CreateFile", Timestamp: time.Now(), - Category: ktypes.File, - Kparams: Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.Enum, Value: uint32(1), Enum: fs.FileCreateDispositions}, + Category: File, + Params: Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.Enum, Value: uint32(1), Enum: fs.FileCreateDispositions}, }, }, nil, @@ -148,20 +147,20 @@ func TestQueuePush(t *testing.T) { }, { "push event listeners deny", - &Kevent{ - Type: ktypes.CreateFile, + &Event{ + Type: CreateFile, Tid: 2484, PID: 859, CPU: 1, Seq: 2, Name: "CreateFile", Timestamp: time.Now(), - Category: ktypes.File, - Kparams: Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.Enum, Value: uint32(1), Enum: fs.FileCreateDispositions}, + Category: File, + Params: Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.Enum, Value: uint32(1), Enum: fs.FileCreateDispositions}, }, }, nil, @@ -185,7 +184,7 @@ func TestQueuePush(t *testing.T) { err := q.Push(tt.e) assert.Equal(t, err, tt.err) if tt.isEnqueued { - assert.True(t, tt.e.Kparams.Contains(kparams.FileAttributes)) + assert.True(t, tt.e.Params.Contains(params.FileAttributes)) } assert.True(t, len(q.Events()) > 0 == tt.isEnqueued) }) @@ -193,16 +192,16 @@ func TestQueuePush(t *testing.T) { } func TestPushBacklog(t *testing.T) { - e := &Kevent{ - Type: ktypes.CreateHandle, + e := &Event{ + Type: CreateHandle, Tid: 2484, PID: 859, - Category: ktypes.Handle, - Kparams: Kparams{ - kparams.HandleID: {Name: kparams.HandleID, Type: kparams.Uint32, Value: uint32(21)}, - kparams.HandleObjectTypeID: {Name: kparams.HandleObjectTypeID, Type: kparams.AnsiString, Value: "Key"}, - kparams.HandleObject: {Name: kparams.HandleObject, Type: kparams.Uint64, Value: uint64(18446692422059208560)}, - kparams.HandleObjectName: {Name: kparams.HandleObjectName, Type: kparams.UnicodeString, Value: ""}, + Category: Handle, + Params: Params{ + params.HandleID: {Name: params.HandleID, Type: params.Uint32, Value: uint32(21)}, + params.HandleObjectTypeID: {Name: params.HandleObjectTypeID, Type: params.AnsiString, Value: "Key"}, + params.HandleObject: {Name: params.HandleObject, Type: params.Uint64, Value: uint64(18446692422059208560)}, + params.HandleObjectName: {Name: params.HandleObjectName, Type: params.UnicodeString, Value: ""}, }, Metadata: make(Metadata), } @@ -214,16 +213,16 @@ func TestPushBacklog(t *testing.T) { require.Len(t, q.Events(), 0) require.False(t, q.backlog.empty()) - e1 := &Kevent{ - Type: ktypes.CloseHandle, + e1 := &Event{ + Type: CloseHandle, Tid: 2484, PID: 859, - Category: ktypes.Handle, - Kparams: Kparams{ - kparams.HandleID: {Name: kparams.HandleID, Type: kparams.Uint32, Value: uint32(21)}, - kparams.HandleObjectTypeID: {Name: kparams.HandleObjectTypeID, Type: kparams.AnsiString, Value: "Key"}, - kparams.HandleObject: {Name: kparams.HandleObject, Type: kparams.Uint64, Value: uint64(18446692422059208560)}, - kparams.HandleObjectName: {Name: kparams.HandleObjectName, Type: kparams.UnicodeString, Value: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`}, + Category: Handle, + Params: Params{ + params.HandleID: {Name: params.HandleID, Type: params.Uint32, Value: uint32(21)}, + params.HandleObjectTypeID: {Name: params.HandleObjectTypeID, Type: params.AnsiString, Value: "Key"}, + params.HandleObject: {Name: params.HandleObject, Type: params.Uint64, Value: uint64(18446692422059208560)}, + params.HandleObjectName: {Name: params.HandleObjectName, Type: params.UnicodeString, Value: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`}, }, Metadata: make(Metadata), } @@ -233,7 +232,7 @@ func TestPushBacklog(t *testing.T) { ev := <-q.Events() require.NotNil(t, ev) - assert.Equal(t, `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`, ev.GetParamAsString(kparams.HandleObjectName)) + assert.Equal(t, `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`, ev.GetParamAsString(params.HandleObjectName)) require.True(t, reflect.DeepEqual(e1, <-q.Events())) } diff --git a/pkg/kevent/sequencer_windows.go b/pkg/event/sequencer_windows.go similarity index 92% rename from pkg/kevent/sequencer_windows.go rename to pkg/event/sequencer_windows.go index 4aa593fb6..faf0594b5 100644 --- a/pkg/kevent/sequencer_windows.go +++ b/pkg/event/sequencer_windows.go @@ -16,7 +16,7 @@ * limitations under the License. */ -package kevent +package event import ( "errors" @@ -36,18 +36,18 @@ const ( invalidKey = registry.Key(syscall.InvalidHandle) ) -var seqStoreErrors = expvar.NewInt("kevent.seq.store.errors") -var seqInitErrors = expvar.NewMap("kevent.seq.init.errors") +var seqStoreErrors = expvar.NewInt("event.seq.store.errors") +var seqInitErrors = expvar.NewMap("event.seq.init.errors") var errInvalidVolatileKey = errors.New("couldn't open HKCU/Volatile Environment key") -// Sequencer is responsible for incrementing, getting and persisting the kevent sequence number in the Windows registry. +// Sequencer is responsible for incrementing, getting and persisting the event sequence number in the Windows registry. type Sequencer struct { key registry.Key quit chan struct{} seq uint64 } -// NewSequencer creates a fresh kevent sequencer. If the `KeventSeq` value is present under the volatile key, the current +// NewSequencer creates a fresh event sequencer. If the `EventSequence` value is present under the volatile key, the current // sequence number is initialized to the last stored sequence. The sequencer schedules a ticker that periodically dumps // the current sequence number into the registry value. func NewSequencer() *Sequencer { diff --git a/pkg/kevent/sequencer_windows_test.go b/pkg/event/sequencer_windows_test.go similarity index 99% rename from pkg/kevent/sequencer_windows_test.go rename to pkg/event/sequencer_windows_test.go index bb2aff59b..f815b4767 100644 --- a/pkg/kevent/sequencer_windows_test.go +++ b/pkg/event/sequencer_windows_test.go @@ -16,7 +16,7 @@ * limitations under the License. */ -package kevent +package event import ( "github.com/stretchr/testify/assert" diff --git a/pkg/kevent/stackwalk.go b/pkg/event/stackwalk.go similarity index 92% rename from pkg/kevent/stackwalk.go rename to pkg/event/stackwalk.go index 6cc9a388d..bdf86795b 100644 --- a/pkg/kevent/stackwalk.go +++ b/pkg/event/stackwalk.go @@ -16,11 +16,11 @@ * limitations under the License. */ -package kevent +package event import ( "expvar" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/util/multierror" log "github.com/sirupsen/logrus" "sync" @@ -55,7 +55,7 @@ var stackwalkBuckets = expvar.NewInt("stackwalk.buckets") // popped from the queue and enriched with return addresses // which are later subject to symbolization. type StackwalkDecorator struct { - buckets map[uint64][]*Kevent + buckets map[uint64][]*Event q *Queue mux sync.Mutex @@ -69,7 +69,7 @@ type StackwalkDecorator struct { func NewStackwalkDecorator(q *Queue) *StackwalkDecorator { s := &StackwalkDecorator{ q: q, - buckets: make(map[uint64][]*Kevent), + buckets: make(map[uint64][]*Event), flusher: time.NewTicker(flusherInterval), quit: make(chan struct{}, 1), } @@ -80,7 +80,7 @@ func NewStackwalkDecorator(q *Queue) *StackwalkDecorator { } // Push pushes a new event to the queue. -func (s *StackwalkDecorator) Push(e *Kevent) { +func (s *StackwalkDecorator) Push(e *Event) { s.mux.Lock() defer s.mux.Unlock() @@ -88,7 +88,7 @@ func (s *StackwalkDecorator) Push(e *Kevent) { id := e.StackID() q, ok := s.buckets[id] if !ok { - s.buckets[id] = []*Kevent{e} + s.buckets[id] = []*Event{e} } else { s.buckets[id] = append(q, e) } @@ -101,7 +101,7 @@ func (s *StackwalkDecorator) Push(e *Kevent) { // originating event with the same pid,tid tuple formerly // coined as stack identifier. The originating event is then // decorated with callstack return addresses. -func (s *StackwalkDecorator) Pop(e *Kevent) *Kevent { +func (s *StackwalkDecorator) Pop(e *Event) *Event { s.mux.Lock() defer s.mux.Unlock() @@ -111,7 +111,7 @@ func (s *StackwalkDecorator) Pop(e *Kevent) *Kevent { return e } - var evt *Kevent + var evt *Event if len(q) > 0 { evt, s.buckets[id] = q[0], q[1:] stackwalkEnqueued.Add(-int64(len(s.buckets[id]))) @@ -121,8 +121,8 @@ func (s *StackwalkDecorator) Pop(e *Kevent) *Kevent { return e } - callstack := e.Kparams.MustGetSlice(kparams.Callstack) - evt.AppendParam(kparams.Callstack, kparams.Slice, callstack) + callstack := e.Params.MustGetSlice(params.Callstack) + evt.AppendParam(params.Callstack, params.Slice, callstack) return evt } diff --git a/pkg/event/stackwalk_test.go b/pkg/event/stackwalk_test.go new file mode 100644 index 000000000..2275b1005 --- /dev/null +++ b/pkg/event/stackwalk_test.go @@ -0,0 +1,129 @@ +/* + * Copyright 2021-present by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package event + +import ( + "github.com/rabbitstack/fibratus/pkg/event/params" + "github.com/rabbitstack/fibratus/pkg/fs" + "github.com/rabbitstack/fibratus/pkg/util/va" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestStackwalkDecorator(t *testing.T) { + q := NewQueue(50, false, true) + cd := NewStackwalkDecorator(q) + + e := &Event{ + Type: CreateFile, + Tid: 2484, + PID: 859, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Timestamp: time.Now(), + Category: File, + Params: Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.Enum, Value: uint32(1), Enum: fs.FileCreateDispositions}, + }, + } + + e1 := &Event{ + Type: CreateFile, + Tid: 2484, + PID: 859, + CPU: 1, + Seq: 3, + Name: "CreateFile", + Timestamp: time.Now(), + Category: File, + Params: Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\kernel32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.Enum, Value: uint32(1), Enum: fs.FileCreateDispositions}, + }, + } + + cd.Push(e) + cd.Push(e1) + + assert.Len(t, cd.buckets[e.StackID()], 2) + + sw := &Event{ + Type: StackWalk, + Tid: 2484, + PID: 859, + CPU: 1, + Seq: 4, + Name: "StackWalk", + Timestamp: time.Now(), + Params: Params{ + params.Callstack: {Name: params.Callstack, Type: params.Slice, Value: []va.Address{0x7ffb5eb70dc4, 0x7ffb5c191deb, 0x7ffb3138592e}}, + }, + } + + evt := cd.Pop(sw) + assert.Len(t, cd.buckets[e.StackID()], 1) + assert.Equal(t, CreateFile, evt.Type) + assert.True(t, evt.Params.Contains(params.Callstack)) + assert.Equal(t, "C:\\Windows\\system32\\user32.dll", evt.GetParamAsString(params.FilePath)) +} + +func init() { + maxQueueTTLPeriod = time.Second * 2 + flusherInterval = time.Second +} + +func TestStackwalkDecoratorFlush(t *testing.T) { + q := NewQueue(50, false, true) + q.RegisterListener(&DummyListener{}) + cd := NewStackwalkDecorator(q) + defer cd.Stop() + + e := &Event{ + Type: CreateFile, + Tid: 2484, + PID: 859, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Timestamp: time.Now(), + Category: File, + Params: Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.Enum, Value: uint32(1), Enum: fs.FileCreateDispositions}, + }, + } + + cd.Push(e) + assert.Len(t, cd.buckets[e.StackID()], 1) + time.Sleep(time.Millisecond * 3100) + + evt := <-q.Events() + assert.Len(t, cd.buckets[e.StackID()], 0) + assert.Equal(t, CreateFile, evt.Type) + assert.False(t, evt.Params.Contains(params.Callstack)) +} diff --git a/pkg/kevent/template.go b/pkg/event/template.go similarity index 59% rename from pkg/kevent/template.go rename to pkg/event/template.go index f487fe69a..8f349b653 100644 --- a/pkg/kevent/template.go +++ b/pkg/event/template.go @@ -16,7 +16,7 @@ * limitations under the License. */ -package kevent +package event import ( "bytes" @@ -25,28 +25,28 @@ import ( ) // Template is the default Go template used for formatting events in textual format. -var Template = `Name: {{ .Kevt.Name }} -Sequence: {{ .Kevt.Seq }} -Description: {{ .Kevt.Description }} -Process ID: {{ .Kevt.PID }} -Thread ID: {{ .Kevt.Tid }} -Params: {{ .Kevt.Kparams }} +var Template = `Name: {{ .Evt.Name }} +Sequence: {{ .Evt.Seq }} +Description: {{ .Evt.Description }} +Process ID: {{ .Evt.PID }} +Thread ID: {{ .Evt.Tid }} +Params: {{ .Evt.Params }} -{{- if .Kevt.PS }} +{{- if .Evt.PS }} -Process: {{ .Kevt.PS.Name }} -Exe: {{ .Kevt.PS.Exe }} -Pid: {{ .Kevt.PS.PID }} -Ppid: {{ .Kevt.PS.Ppid }} -Cmdline: {{ .Kevt.PS.Cmdline }} -Cwd: {{ .Kevt.PS.Cwd }} -SID: {{ .Kevt.PS.SID }} -User: {{ .Kevt.PS.Username }} -Domain: {{ .Kevt.PS.Domain }} -Session ID: {{ .Kevt.PS.SessionID }} -{{ if and (.SerializeEnvs) (.Kevt.PS.Envs) }} +Process: {{ .Evt.PS.Name }} +Exe: {{ .Evt.PS.Exe }} +Pid: {{ .Evt.PS.PID }} +Ppid: {{ .Evt.PS.Ppid }} +Cmdline: {{ .Evt.PS.Cmdline }} +Cwd: {{ .Evt.PS.Cwd }} +SID: {{ .Evt.PS.SID }} +User: {{ .Evt.PS.Username }} +Domain: {{ .Evt.PS.Domain }} +Session ID: {{ .Evt.PS.SessionID }} +{{ if and (.SerializeEnvs) (.Evt.PS.Envs) }} Env: - {{- with .Kevt.PS.Envs }} + {{- with .Evt.PS.Envs }} {{- range $k, $v := . }} {{ $k }}: {{ $v }} {{- end }} @@ -54,7 +54,7 @@ Env: {{ end }} {{ if .SerializeThreads }} Threads: - {{- with .Kevt.PS.Threads }} + {{- with .Evt.PS.Threads }} {{- range . }} {{ . }} {{- end }} @@ -62,54 +62,54 @@ Threads: {{ end }} {{ if .SerializeImages }} Modules: - {{- with .Kevt.PS.Modules }} + {{- with .Evt.PS.Modules }} {{- range . }} {{ . }} {{- end }} {{- end }} {{ end }} -{{ if and (.SerializeHandles) (.Kevt.PS.Handles) }} +{{ if and (.SerializeHandles) (.Evt.PS.Handles) }} Handles: - {{- with .Kevt.PS.Handles }} + {{- with .Evt.PS.Handles }} {{- range . }} {{ . }} {{- end }} {{- end }} {{ end }} -{{ if and (.SerializePE) (.Kevt.PS.PE) }} -Entrypoint: {{ .Kevt.PS.PE.EntryPoint }} -Image base: {{ .Kevt.PS.PE.ImageBase }} -Build date: {{ .Kevt.PS.PE.LinkTime }} +{{ if and (.SerializePE) (.Evt.PS.PE) }} +Entrypoint: {{ .Evt.PS.PE.EntryPoint }} +Image base: {{ .Evt.PS.PE.ImageBase }} +Build date: {{ .Evt.PS.PE.LinkTime }} -Number of symbols: {{ .Kevt.PS.PE.NumberOfSymbols }} -Number of sections: {{ .Kevt.PS.PE.NumberOfSections }} +Number of symbols: {{ .Evt.PS.PE.NumberOfSymbols }} +Number of sections: {{ .Evt.PS.PE.NumberOfSections }} Sections: - {{- with .Kevt.PS.PE.Sections }} + {{- with .Evt.PS.PE.Sections }} {{- range . }} {{ . }} {{- end }} {{- end }} -{{ if .Kevt.PS.PE.Symbols }} +{{ if .Evt.PS.PE.Symbols }} Symbols: - {{- with .Kevt.PS.PE.Symbols }} + {{- with .Evt.PS.PE.Symbols }} {{- range . }} {{ . }} {{- end }} {{- end }} {{ end }} -{{ if .Kevt.PS.PE.Imports }} +{{ if .Evt.PS.PE.Imports }} Imports: - {{- with .Kevt.PS.PE.Imports }} + {{- with .Evt.PS.PE.Imports }} {{- range . }} {{ . }} {{- end }} {{- end }} {{ end }} -{{ if .Kevt.PS.PE.VersionResources }} +{{ if .Evt.PS.PE.VersionResources }} Resources: - {{- with .Kevt.PS.PE.VersionResources }} + {{- with .Evt.PS.PE.VersionResources }} {{- range $k, $v := . }} {{ $k }}: {{ $v }} {{- end }} @@ -121,7 +121,7 @@ Resources: // RenderDefaultTemplate returns the event string representation // after applying the default Go template. -func (e *Kevent) RenderDefaultTemplate() ([]byte, error) { +func (e *Event) RenderDefaultTemplate() ([]byte, error) { tmpl, err := template.New("event").Parse(Template) if err != nil { return nil, err @@ -131,21 +131,21 @@ func (e *Kevent) RenderDefaultTemplate() ([]byte, error) { // RenderCustomTemplate returns the event string representation // after applying the given Go template. -func (e *Kevent) RenderCustomTemplate(tmpl *template.Template) ([]byte, error) { +func (e *Event) RenderCustomTemplate(tmpl *template.Template) ([]byte, error) { return renderTemplate(e, tmpl) } -func renderTemplate(kevt *Kevent, tmpl *template.Template) ([]byte, error) { +func renderTemplate(evt *Event, tmpl *template.Template) ([]byte, error) { var writer bytes.Buffer data := struct { - Kevt *Kevent + Evt *Event SerializeHandles bool SerializeThreads bool SerializeImages bool SerializeEnvs bool SerializePE bool }{ - kevt, + evt, SerializeHandles, SerializeThreads, SerializeImages, diff --git a/pkg/kevent/ktypes/ktypes_windows.go b/pkg/event/types_windows.go similarity index 92% rename from pkg/kevent/ktypes/ktypes_windows.go rename to pkg/event/types_windows.go index 1eb62e2b3..db44c512e 100644 --- a/pkg/kevent/ktypes/ktypes_windows.go +++ b/pkg/event/types_windows.go @@ -16,7 +16,7 @@ * limitations under the License. */ -package ktypes +package event import ( "encoding/binary" @@ -43,8 +43,8 @@ const ( ThreadpoolLogger ) -// Ktype identifies an event type. It comprises the event GUID + hook ID to uniquely identify the event -type Ktype [18]byte +// Type identifies an event type. It comprises the event GUID + hook ID to uniquely identify the event +type Type [18]byte var ( // ProcessEventGUID represents process provider event GUID @@ -221,19 +221,19 @@ var ( // SetThreadpoolTimer represents the event that sets the thread pool timer object SetThreadpoolTimer = pack(ThreadpoolGUID, 44) - // UnknownKtype designates unknown kernel event type - UnknownKtype = pack(windows.GUID{}, 0) + // UnknownType designates unknown event type + UnknownType = pack(windows.GUID{}, 0) ) // NewFromEventRecord creates a new event type from ETW event record. -func NewFromEventRecord(ev *etw.EventRecord) Ktype { +func NewFromEventRecord(ev *etw.EventRecord) Type { return pack(ev.Header.ProviderID, ev.HookID()) } // String returns the string representation of the event type. Returns an empty string // if the event type is not recognized. -func (k Ktype) String() string { - switch k { +func (t Type) String() string { + switch t { case CreateProcess: return "CreateProcess" case TerminateProcess: @@ -350,8 +350,8 @@ func (k Ktype) String() string { } // Category determines the category to which the event type pertains. -func (k Ktype) Category() Category { - switch k { +func (t Type) Category() Category { + switch t { case CreateProcess, TerminateProcess, OpenProcess, ProcessRundown: return Process case CreateThread, TerminateThread, OpenThread, SetThreadContext, ThreadRundown, StackWalk: @@ -387,8 +387,8 @@ func (k Ktype) Category() Category { } // Subcategory determines the event subcategory, if any. -func (k Ktype) Subcategory() Subcategory { - switch k { +func (t Type) Subcategory() Subcategory { + switch t { case QueryDNS, ReplyDNS: return DNS default: @@ -397,8 +397,8 @@ func (k Ktype) Subcategory() Subcategory { } // Description returns a brief description of the event type. -func (k Ktype) Description() string { - switch k { +func (t Type) Description() string { + switch t { case CreateProcess: return "Creates a new process and its primary thread" case TerminateProcess: @@ -495,21 +495,21 @@ func (k Ktype) Description() string { } // Hash calculates the hash number of the event type. -func (k Ktype) Hash() uint32 { - if k == UnknownKtype { +func (t Type) Hash() uint32 { + if t == UnknownType { return 0 } - return hashers.FnvUint32([]byte(k.String())) + return hashers.FnvUint32([]byte(t.String())) } // Exists determines whether particular event type exists. -func (k Ktype) Exists() bool { - return k.String() != "" +func (t Type) Exists() bool { + return t.String() != "" } // OnlyState determines whether the event type is solely used for state management. -func (k Ktype) OnlyState() bool { - switch k { +func (t Type) OnlyState() bool { + switch t { case ProcessRundown, ThreadRundown, ImageRundown, @@ -527,8 +527,8 @@ func (k Ktype) OnlyState() bool { } // CanEnrichStack determines if the event can be enriched with a callstack. -func (k Ktype) CanEnrichStack() bool { - switch k { +func (t Type) CanEnrichStack() bool { + switch t { case CreateProcess, CreateThread, TerminateThread, @@ -549,35 +549,35 @@ func (k Ktype) CanEnrichStack() bool { } } -// UnmarshalYAML converts the ktype name to ktype array type. -func (k *Ktype) UnmarshalYAML(unmarshal func(interface{}) error) error { - var ktyp string - err := unmarshal(&ktyp) +// UnmarshalYAML converts the Type name to Type array type. +func (t *Type) UnmarshalYAML(unmarshal func(interface{}) error) error { + var typ string + err := unmarshal(&typ) if err != nil { return err } - *k = KeventNameToKtype(ktyp) + *t = NameToType(typ) return nil } -// GUID returns the event GUID from the raw ktype. -func (k *Ktype) GUID() windows.GUID { +// GUID returns the event GUID from the raw event type. +func (t *Type) GUID() windows.GUID { return windows.GUID{ - Data1: binary.BigEndian.Uint32(k[0:4]), - Data2: binary.BigEndian.Uint16(k[4:6]), - Data3: binary.BigEndian.Uint16(k[6:8]), - Data4: [8]byte{k[8], k[9], k[10], k[11], k[12], k[13], k[14], k[15]}, + Data1: binary.BigEndian.Uint32(t[0:4]), + Data2: binary.BigEndian.Uint16(t[4:6]), + Data3: binary.BigEndian.Uint16(t[6:8]), + Data4: [8]byte{t[8], t[9], t[10], t[11], t[12], t[13], t[14], t[15]}, } } -// HookID returns the event operation code (hook ID) from the raw ktype. -func (k *Ktype) HookID() uint16 { - return binary.BigEndian.Uint16(k[16:]) +// HookID returns the event operation code (hook ID) from the raw event type. +func (t *Type) HookID() uint16 { + return binary.BigEndian.Uint16(t[16:]) } // Source designates the provenance of this event type. -func (k Ktype) Source() EventSource { - switch k { +func (t Type) Source() EventSource { + switch t { case OpenProcess, OpenThread, SetThreadContext, CreateSymbolicLinkObject: return AuditAPICallsLogger case QueryDNS, ReplyDNS: @@ -594,17 +594,17 @@ func (k Ktype) Source() EventSource { // its timestamp is perfectly aligned in relation to other // events, but it appears first on the consumer callback // before other events published before it. -func (k Ktype) CanArriveOutOfOrder() bool { - return k.Category() == Threadpool || k.Subcategory() == DNS || - k == OpenProcess || k == OpenThread || k == SetThreadContext || k == CreateSymbolicLinkObject +func (t Type) CanArriveOutOfOrder() bool { + return t.Category() == Threadpool || t.Subcategory() == DNS || + t == OpenProcess || t == OpenThread || t == SetThreadContext || t == CreateSymbolicLinkObject } -// FromParts builds ktype from provider GUID and hook ID. -func FromParts(g windows.GUID, id uint16) Ktype { return pack(g, id) } +// TypeFromParts builds the event type from provider GUID and hook ID. +func TypeFromParts(g windows.GUID, id uint16) Type { return pack(g, id) } -// pack merges event provider GUID and the hook ID into `Ktype` array. +// pack merges event provider GUID and the hook ID into `Type` array. // The type provides a convenient way for comparing event types. -func pack(g windows.GUID, id uint16) Ktype { +func pack(g windows.GUID, id uint16) Type { return [18]byte{ byte(g.Data1 >> 24), byte(g.Data1 >> 16), byte(g.Data1 >> 8), byte(g.Data1), byte(g.Data2 >> 8), byte(g.Data2), diff --git a/pkg/kevent/ktypes/ktypes_windows_test.go b/pkg/event/types_windows_test.go similarity index 89% rename from pkg/kevent/ktypes/ktypes_windows_test.go rename to pkg/event/types_windows_test.go index 2f6b1da56..a93c15b5a 100644 --- a/pkg/kevent/ktypes/ktypes_windows_test.go +++ b/pkg/event/types_windows_test.go @@ -16,7 +16,7 @@ * limitations under the License. */ -package ktypes +package event import ( "github.com/rabbitstack/fibratus/pkg/sys/etw" @@ -26,7 +26,7 @@ import ( "testing" ) -func TestPackAllBytes(t *testing.T) { +func TestEventTypePackAllBytes(t *testing.T) { assert.Equal(t, byte(0x3d), CreateProcess[0]) assert.Equal(t, byte(0x6f), CreateProcess[1]) assert.Equal(t, byte(0xa8), CreateProcess[2]) @@ -53,11 +53,11 @@ func TestPackAllBytes(t *testing.T) { assert.Equal(t, byte(0xbe), QueryDNS[17]) } -func TestKtypeComparision(t *testing.T) { +func TestEventTypeComparison(t *testing.T) { var tests = []struct { name string - ktyp Ktype - wants Ktype + ktyp Type + wants Type }{ { "equals CreateProcess", @@ -79,7 +79,7 @@ func TestKtypeComparision(t *testing.T) { } } -func TestNewFromEventRecord(t *testing.T) { +func TestNewEventTypeFromEventRecord(t *testing.T) { assert.Equal(t, CreateProcess, NewFromEventRecord(&etw.EventRecord{ Header: etw.EventHeader{ ProviderID: windows.GUID{Data1: 0x3d6fa8d0, Data2: 0xfe05, Data3: 0x11d0, Data4: [8]byte{0x9d, 0xda, 0x0, 0xc0, 0x4f, 0xd7, 0xba, 0x7c}}, @@ -98,14 +98,14 @@ func TestNewFromEventRecord(t *testing.T) { })) } -func TestKtypeExists(t *testing.T) { +func TestEventTypeExists(t *testing.T) { require.True(t, AcceptTCPv4.Exists()) require.True(t, AcceptTCPv6.Exists()) } -func TestGUIDAndHookIDFromKtype(t *testing.T) { +func TestGUIDAndHookIDFromEventType(t *testing.T) { var tests = []struct { - ktype Ktype + Type Type opcode uint16 guid windows.GUID }{ @@ -122,9 +122,9 @@ func TestGUIDAndHookIDFromKtype(t *testing.T) { } for _, tt := range tests { - t.Run(tt.ktype.String(), func(t *testing.T) { - assert.Equal(t, tt.guid.String(), tt.ktype.GUID().String()) - assert.Equal(t, tt.opcode, tt.ktype.HookID()) + t.Run(tt.Type.String(), func(t *testing.T) { + assert.Equal(t, tt.guid.String(), tt.Type.GUID().String()) + assert.Equal(t, tt.opcode, tt.Type.HookID()) }) } } diff --git a/pkg/filament/_fixtures/test_filter.py b/pkg/filament/_fixtures/test_filter.py index 33c1afbbb..2211180a9 100644 --- a/pkg/filament/_fixtures/test_filter.py +++ b/pkg/filament/_fixtures/test_filter.py @@ -23,5 +23,5 @@ def on_init(): kfilter('ps.name in (%s)' % ','.join(["'svchost.exe'", "'cmd.exe'", "'mimikatz.exe'"])) -def on_next_kevent(kevent): +def on_next_kevent(Event): pass \ No newline at end of file diff --git a/pkg/filament/_fixtures/test_on_next_kevent.py b/pkg/filament/_fixtures/test_on_next_kevent.py index 07b490f96..080ae9132 100644 --- a/pkg/filament/_fixtures/test_on_next_kevent.py +++ b/pkg/filament/_fixtures/test_on_next_kevent.py @@ -25,8 +25,8 @@ def on_init(): columns(['Key', '#Seq']) sort_by('#Seq') -def on_next_kevent(kevent): - kevents.append({'key_name': kevent['kparams']['key_name'], 'seq': kevent['seq'], 'dip': kevent['kparams']['dip']}) +def on_next_kevent(Event): + kevents.append({'key_name': Event['params']['key_name'], 'seq': Event['seq'], 'dip': Event['params']['dip']}) def on_interval(): for key in kevents: diff --git a/pkg/filament/_fixtures/top_hives_io.py b/pkg/filament/_fixtures/top_hives_io.py index b58fcc640..7855e5922 100644 --- a/pkg/filament/_fixtures/top_hives_io.py +++ b/pkg/filament/_fixtures/top_hives_io.py @@ -29,6 +29,6 @@ def on_init(): sort_by("1") -def on_next_kevent(kevent): +def on_next_kevent(Event): pass diff --git a/pkg/filament/_fixtures/top_keys_io_table.py b/pkg/filament/_fixtures/top_keys_io_table.py index 104e3cdb8..ae039b73f 100644 --- a/pkg/filament/_fixtures/top_keys_io_table.py +++ b/pkg/filament/_fixtures/top_keys_io_table.py @@ -29,7 +29,7 @@ def on_init(): sort_by('#Ops') -def on_next_kevent(kevent): +def on_next_kevent(Event): pass diff --git a/pkg/filament/cpython/_fixtures/top_hives_io.py b/pkg/filament/cpython/_fixtures/top_hives_io.py index 9695ab4fe..5621a4cb1 100644 --- a/pkg/filament/cpython/_fixtures/top_hives_io.py +++ b/pkg/filament/cpython/_fixtures/top_hives_io.py @@ -32,8 +32,8 @@ def on_init(): #set_interval(1) -def on_next_kevent(kevent): - print("KEVENT\n", kevent["kparams"]) +def on_next_kevent(Event): + print("Event\n", Event["params"]) #raise Exception('eggs', 'eggs') def on_interval(): diff --git a/pkg/filament/dict.go b/pkg/filament/dict.go new file mode 100644 index 000000000..307261e1c --- /dev/null +++ b/pkg/filament/dict.go @@ -0,0 +1,114 @@ +//go:build filament && windows +// +build filament,windows + +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package filament + +import ( + "errors" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" + "github.com/rabbitstack/fibratus/pkg/filament/cpython" +) + +var ( + seq = cpython.PyUnicodeFromString("seq") + pid = cpython.PyUnicodeFromString("pid") + ppid = cpython.PyUnicodeFromString("ppid") + cwd = cpython.PyUnicodeFromString("cwd") + exec = cpython.PyUnicodeFromString("exe") + comm = cpython.PyUnicodeFromString("comm") + sid = cpython.PyUnicodeFromString("sid") + tid = cpython.PyUnicodeFromString("tid") + cpu = cpython.PyUnicodeFromString("cpu") + name = cpython.PyUnicodeFromString("name") + cat = cpython.PyUnicodeFromString("category") + desc = cpython.PyUnicodeFromString("description") + host = cpython.PyUnicodeFromString("host") + ts = cpython.PyUnicodeFromString("timestamp") + parameters = cpython.PyUnicodeFromString("params") + + errDictAllocate = errors.New("couldn't allocate a new dict") +) + +// newEventDict constructs a Python dictionary object from event structure. This dictionary object is +// passed to the event dispatching function in the filament. +func newEventDict(evt *event.Event) (*cpython.Dict, error) { + dict := cpython.NewDict() + if dict.IsNull() { + return nil, errDictAllocate + } + + // insert canonical event fields + dict.Insert(seq, cpython.NewPyObjectFromValue(evt.Seq)) + dict.Insert(pid, cpython.NewPyObjectFromValue(evt.PID)) + dict.Insert(tid, cpython.NewPyObjectFromValue(evt.Tid)) + dict.Insert(cpu, cpython.NewPyObjectFromValue(evt.CPU)) + dict.Insert(name, cpython.NewPyObjectFromValue(evt.Name)) + dict.Insert(cat, cpython.NewPyObjectFromValue(string(evt.Category))) + dict.Insert(desc, cpython.NewPyObjectFromValue(evt.Description)) + dict.Insert(host, cpython.NewPyObjectFromValue(evt.Host)) + dict.Insert(ts, cpython.NewPyObjectFromValue(evt.Timestamp)) + + // insert process state fields + ps := evt.PS + if ps != nil { + dict.Insert(ppid, cpython.NewPyObjectFromValue(ps.Ppid)) + dict.Insert(cwd, cpython.NewPyObjectFromValue(ps.Cwd)) + dict.Insert(exec, cpython.NewPyObjectFromValue(ps.Name)) + dict.Insert(comm, cpython.NewPyObjectFromValue(ps.Cmdline)) + dict.Insert(sid, cpython.NewPyObjectFromValue(ps.SID)) + } + + // insert event parameters + pars := cpython.NewDict() + for _, par := range evt.Params { + var val interface{} + var err error + switch par.Type { + case params.Uint8: + val, err = evt.Params.GetUint8(par.Name) + case params.Uint16, params.Port: + val, err = evt.Params.GetUint16(par.Name) + case params.Uint32, params.PID, params.TID: + val, err = evt.Params.GetUint32(par.Name) + case params.Uint64: + val, err = evt.Params.GetUint64(par.Name) + case params.Time: + val, err = evt.Params.GetTime(par.Name) + case params.IP: + val, err = evt.Params.GetIP(par.Name) + default: + val = evt.GetParamAsString(par.Name) + } + if err != nil { + continue + } + param := cpython.NewPyObjectFromValue(val) + if param.IsNull() { + continue + } + pars.Insert(cpython.PyUnicodeFromString(par.Name), param) + } + + dict.Insert(parameters, pars.Object()) + + return dict, nil +} diff --git a/pkg/filament/kdict_test.go b/pkg/filament/dict_test.go similarity index 76% rename from pkg/filament/kdict_test.go rename to pkg/filament/dict_test.go index 22c184ca4..069f72c26 100644 --- a/pkg/filament/kdict_test.go +++ b/pkg/filament/dict_test.go @@ -22,10 +22,9 @@ package filament import ( + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/filament/cpython" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "net" @@ -33,7 +32,7 @@ import ( "time" ) -func TestProduceKdict(t *testing.T) { +func TestProduceEventDict(t *testing.T) { // this test crashes in the CI. Reenable once // we investigate why this happens t.SkipNow() @@ -41,18 +40,18 @@ func TestProduceKdict(t *testing.T) { require.NoError(t, err) defer cpython.Finalize() now := time.Now() - kevt := &kevent.Kevent{ + evt := &event.Event{ Seq: uint64(12456738026482168384), Tid: 2484, PID: 859, CPU: 1, Name: "CreateFile", Timestamp: now, - Category: ktypes.File, + Category: event.File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", } - dict, err := newKDict(kevt) + dict, err := newEventDict(evt) require.NoError(t, err) require.NotNil(t, dict) @@ -72,7 +71,7 @@ func TestProduceKdict(t *testing.T) { assert.Equal(t, timestamp.Second(), now.Second()) } -func TestProduceKdictWithIPAddresses(t *testing.T) { +func TestProduceEventDictWithIPAddresses(t *testing.T) { // this test crashes in the CI. Reenable once // we investigate why this happens t.SkipNow() @@ -80,30 +79,30 @@ func TestProduceKdictWithIPAddresses(t *testing.T) { require.NoError(t, err) defer cpython.Finalize() - kevt := &kevent.Kevent{ + evt := &event.Event{ Name: "Send", Tid: 2484, PID: 859, - Kparams: kevent.Kparams{ - kparams.NetDport: {Name: kparams.NetDport, Type: kparams.Uint16, Value: uint16(443)}, - kparams.NetSport: {Name: kparams.NetSport, Type: kparams.Uint16, Value: uint16(43123)}, - kparams.NetSIP: {Name: kparams.NetSIP, Type: kparams.IPv6, Value: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")}, - kparams.NetDIP: {Name: kparams.NetDIP, Type: kparams.IPv4, Value: net.ParseIP("216.58.201.174")}, + Params: event.Params{ + params.NetDport: {Name: params.NetDport, Type: params.Uint16, Value: uint16(443)}, + params.NetSport: {Name: params.NetSport, Type: params.Uint16, Value: uint16(43123)}, + params.NetSIP: {Name: params.NetSIP, Type: params.IPv6, Value: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")}, + params.NetDIP: {Name: params.NetDIP, Type: params.IPv4, Value: net.ParseIP("216.58.201.174")}, }, } - dict, err := newKDict(kevt) + dict, err := newEventDict(evt) require.NoError(t, err) require.NotNil(t, dict) - kpars := dict.Get(cpython.PyUnicodeFromString("kparams")) + kpars := dict.Get(cpython.PyUnicodeFromString("params")) kparamsDict := cpython.NewDictFromObject(kpars) assert.Equal(t, "216.58.201.174", kparamsDict.Get(cpython.PyUnicodeFromString("dip")).String()) assert.Equal(t, "2001:db8:85a3::8a2e:370:7334", kparamsDict.Get(cpython.PyUnicodeFromString("sip")).String()) } -func BenchmarkTestProduceKdict(b *testing.B) { +func BenchmarkTestProduceEventDict(b *testing.B) { // this crashes in the CI. Reenable once // we investigate why this happens b.SkipNow() @@ -112,20 +111,20 @@ func BenchmarkTestProduceKdict(b *testing.B) { require.NoError(b, err) defer cpython.Finalize() - kevt := &kevent.Kevent{ + evt := &event.Event{ Seq: uint64(12456738026482168384), Tid: 2484, PID: 859, CPU: 1, Name: "CreateFile", Timestamp: time.Now(), - Category: ktypes.File, + Category: event.File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", } for i := 0; i < b.N; i++ { - dict, err := newKDict(kevt) + dict, err := newEventDict(evt) if err != nil || dict.IsNull() { b.Fatal("invalid dict produced") } diff --git a/pkg/filament/filament.go b/pkg/filament/filament.go index 54bb978a6..2a2c5f7a8 100644 --- a/pkg/filament/filament.go +++ b/pkg/filament/filament.go @@ -27,10 +27,10 @@ import ( "fmt" "github.com/rabbitstack/fibratus/pkg/alertsender" "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/filament/cpython" "github.com/rabbitstack/fibratus/pkg/filter" "github.com/rabbitstack/fibratus/pkg/handle" - "github.com/rabbitstack/fibratus/pkg/kevent" "github.com/rabbitstack/fibratus/pkg/ps" "github.com/rabbitstack/fibratus/pkg/util/multierror" "github.com/rabbitstack/fibratus/pkg/util/term" @@ -55,7 +55,7 @@ const ( intervalFn = "interval" columnsFn = "columns" sortbyFn = "sort_by" - kfilterFn = "kfilter" + setFilterFn = "set_filter" addRowFn = "add_row" maxRowsFn = "max_rows" titleFn = "title" @@ -66,37 +66,37 @@ const ( findProcessesFn = "find_processes" emitAlertFn = "emit_alert" - onInitFn = "on_init" - onStopFn = "on_stop" - onNextKeventFn = "on_next_kevent" - onIntervalFn = "on_interval" - doc = "__doc__" + onInitFn = "on_init" + onStopFn = "on_stop" + onNextEventFn = "on_next_event" + onIntervalFn = "on_interval" + doc = "__doc__" ) var ( - keventErrors = expvar.NewMap("filament.kevent.errors") - keventProcessErrors = expvar.NewInt("filament.kevent.process.errors") + keventErrors = expvar.NewMap("filament.event.errors") + keventProcessErrors = expvar.NewInt("filament.event.process.errors") kdictErrors = expvar.NewInt("filament.kdict.errors") - batchFlushes = expvar.NewInt("filament.kevent.batch.flushes") + batchFlushes = expvar.NewInt("filament.event.batch.flushes") errFilamentsDir = func(path string) error { return fmt.Errorf("%s does not exist or is not a directory", path) } - errNoDoc = errors.New("filament description is required") - errNoOnNextKevent = errors.New("required on_next_kevent function is not defined") - errOnNextKeventNotCallable = errors.New("on_next_kevent is not callable") - errOnNextKeventMismatchArgs = func(c uint32) error { return fmt.Errorf("expected 1 argument for on_next_kevent but found %d args", c) } - errEmptyName = errors.New("filament name is empty") + errNoDoc = errors.New("filament description is required") + errNoOnNextEvent = errors.New("required on_next_event function is not defined") + errOnNextEventNotCallable = errors.New("on_next_event is not callable") + errOnNextEventMismatchArgs = func(c uint32) error { return fmt.Errorf("expected 1 argument for on_next_event but found %d args", c) } + errEmptyName = errors.New("filament name is empty") tableOutput io.Writer ) -type kbatch []*kevent.Kevent +type kbatch []*event.Event -func (k *kbatch) append(kevt *kevent.Kevent) { +func (k *kbatch) append(evt *event.Event) { if *k == nil { - *k = make([]*kevent.Kevent, 0) + *k = make([]*event.Event, 0) } - *k = append(*k, kevt) + *k = append(*k, evt) } func (k *kbatch) reset() { *k = nil } @@ -205,26 +205,26 @@ func New( // ensure required attributes are present before proceeding with // further initialization. For instance, if the documentation - // string is not provided, on_next_kevent function is missing + // string is not provided, on_next_event function is missing // or has a wrong signature we won't run the filament doc, err := mod.GetAttrString(doc) if err != nil || doc.IsNull() { return nil, errNoDoc } defer doc.DecRef() - if !mod.HasAttr(onNextKeventFn) { - return nil, errNoOnNextKevent + if !mod.HasAttr(onNextEventFn) { + return nil, errNoOnNextEvent } - onNextKevent, err := mod.GetAttrString(onNextKeventFn) - if err != nil || onNextKevent.IsNull() { - return nil, errNoOnNextKevent + onNextEvent, err := mod.GetAttrString(onNextEventFn) + if err != nil || onNextEvent.IsNull() { + return nil, errNoOnNextEvent } - if !onNextKevent.IsCallable() { - return nil, errOnNextKeventNotCallable + if !onNextEvent.IsCallable() { + return nil, errOnNextEventNotCallable } - argCount := onNextKevent.CallableArgCount() + argCount := onNextEvent.CallableArgCount() if argCount != 1 { - return nil, errOnNextKeventMismatchArgs(argCount) + return nil, errOnNextEventMismatchArgs(argCount) } f := &filament{ @@ -237,7 +237,7 @@ func New( fnerrs: make(chan error, 100), gil: cpython.NewGIL(), columns: make([]string, 0), - onNextKevent: onNextKevent, + onNextKevent: onNextEvent, interval: time.Second, initErrors: make([]error, 0), table: newTable(), @@ -272,7 +272,7 @@ func New( if err != nil { return nil, err } - err = f.mod.RegisterFn(kfilterFn, f.kfilterFn, cpython.DefaultMethFlags) + err = f.mod.RegisterFn(setFilterFn, f.setFilterFn, cpython.DefaultMethFlags) if err != nil { return nil, err } @@ -368,7 +368,7 @@ func New( return f, nil } -func (f *filament) Run(kevents <-chan *kevent.Kevent, errs <-chan error) error { +func (f *filament) Run(kevents <-chan *event.Event, errs <-chan error) error { var batch kbatch var flusher = time.NewTicker(time.Second) for { @@ -380,14 +380,14 @@ func (f *filament) Run(kevents <-chan *kevent.Kevent, errs <-chan error) error { } select { - case kevt := <-kevents: - batch.append(kevt) + case evt := <-kevents: + batch.append(evt) case err := <-errs: keventErrors.Add(err.Error(), 1) case <-flusher.C: batchFlushes.Add(1) if batch.len() > 0 { - err := f.pushKevents(batch) + err := f.pushEvents(batch) if err != nil { log.Warnf("on_next_kevent failed: %v", err) keventProcessErrors.Add(1) @@ -403,21 +403,21 @@ func (f *filament) Run(kevents <-chan *kevent.Kevent, errs <-chan error) error { } } -func (f *filament) pushKevents(b kbatch) error { +func (f *filament) pushEvents(b kbatch) error { f.gil.Lock() defer f.gil.Unlock() - for _, kevt := range b { - kdict, err := newKDict(kevt) + for _, evt := range b { + dict, err := newEventDict(evt) if err != nil { - kdict.DecRef() + dict.DecRef() kdictErrors.Add(1) continue } - r := f.onNextKevent.Call(kdict.Object()) + r := f.onNextKevent.Call(dict.Object()) if r != nil { r.DecRef() } - kdict.DecRef() + dict.DecRef() if err := cpython.FetchErr(); err != nil { return err } @@ -472,7 +472,7 @@ func (f *filament) columnsFn(_, args cpython.PyArgs) cpython.PyRawObject { return cpython.NewPyNone() } -func (f *filament) kfilterFn(_, args cpython.PyArgs) cpython.PyRawObject { +func (f *filament) setFilterFn(_, args cpython.PyArgs) cpython.PyRawObject { f.fexpr = args.GetString(1) return cpython.NewPyNone() } diff --git a/pkg/filament/filament_test.go b/pkg/filament/filament_test.go index c61c9751f..862238fd6 100644 --- a/pkg/filament/filament_test.go +++ b/pkg/filament/filament_test.go @@ -25,9 +25,8 @@ import ( "bufio" "bytes" "github.com/rabbitstack/fibratus/pkg/config" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "net" @@ -65,26 +64,26 @@ func TestOnNextKevent(t *testing.T) { filament.Close() }) - kevents := make(chan *kevent.Kevent, 100) + kevents := make(chan *event.Event, 100) errs := make(chan error, 10) for i := 1; i <= 100; i++ { - kevt := &kevent.Kevent{ - Type: ktypes.RegCreateKey, + evt := &event.Event{ + Type: event.RegCreateKey, Tid: 2484, PID: 859, Name: "RegCreateKey", Host: "archrabbit", CPU: uint8(i / 2), - Category: ktypes.Registry, + Category: event.Registry, Seq: uint64(i), Timestamp: time.Now(), - Kparams: kevent.Kparams{ - kparams.RegPath: {Name: kparams.RegPath, Type: kparams.UnicodeString, Value: `HKEY_LOCAL_MACHINE\SYSTEM\Setup`}, - kparams.RegKeyHandle: {Name: kparams.RegKeyHandle, Type: kparams.Address, Value: uint64(18446666033449935464)}, - kparams.NetDIP: {Name: kparams.NetDIP, Type: kparams.IPv4, Value: net.ParseIP("216.58.201.174")}, + Params: event.Params{ + params.RegPath: {Name: params.RegPath, Type: params.UnicodeString, Value: `HKEY_LOCAL_MACHINE\SYSTEM\Setup`}, + params.RegKeyHandle: {Name: params.RegKeyHandle, Type: params.Address, Value: uint64(18446666033449935464)}, + params.NetDIP: {Name: params.NetDIP, Type: params.IPv4, Value: net.ParseIP("216.58.201.174")}, }, } - kevents <- kevt + kevents <- evt } err = filament.Run(kevents, errs) require.Nil(t, err) @@ -105,18 +104,18 @@ func TestFilamentFilter(t *testing.T) { require.NotNil(t, filament) defer filament.Close() require.NotNil(t, filament.Filter()) - kpars := kevent.Kparams{ - kparams.Cmdline: {Name: kparams.Cmdline, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\svchost.exe -k RPCSS"}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.AnsiString, Value: "svchost.exe"}, - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.Uint32, Value: uint32(1234)}, - kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.Uint32, Value: uint32(345)}, + kpars := event.Params{ + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\svchost.exe -k RPCSS"}, + params.ProcessName: {Name: params.ProcessName, Type: params.AnsiString, Value: "svchost.exe"}, + params.ProcessID: {Name: params.ProcessID, Type: params.Uint32, Value: uint32(1234)}, + params.ProcessParentID: {Name: params.ProcessParentID, Type: params.Uint32, Value: uint32(345)}, } - kevt := &kevent.Kevent{ - Type: ktypes.CreateProcess, - Kparams: kpars, - Name: "CreateProcess", + evt := &event.Event{ + Type: event.CreateProcess, + Params: kpars, + Name: "CreateProcess", } - require.True(t, filament.Filter().Run(kevt)) + require.True(t, filament.Filter().Run(evt)) } diff --git a/pkg/filament/filament_unsupported.go b/pkg/filament/filament_unsupported.go index fd5f83b0d..24fadfc11 100644 --- a/pkg/filament/filament_unsupported.go +++ b/pkg/filament/filament_unsupported.go @@ -23,7 +23,7 @@ package filament import ( "github.com/rabbitstack/fibratus/pkg/config" - kerrors "github.com/rabbitstack/fibratus/pkg/errors" + errs "github.com/rabbitstack/fibratus/pkg/errors" "github.com/rabbitstack/fibratus/pkg/handle" "github.com/rabbitstack/fibratus/pkg/ps" ) @@ -35,5 +35,5 @@ func New( hsnap handle.Snapshotter, config *config.Config, ) (Filament, error) { - return nil, kerrors.ErrFeatureUnsupported("filament") + return nil, errs.ErrFeatureUnsupported("filament") } diff --git a/pkg/filament/kdict.go b/pkg/filament/kdict.go deleted file mode 100644 index fd39f108a..000000000 --- a/pkg/filament/kdict.go +++ /dev/null @@ -1,114 +0,0 @@ -//go:build filament && windows -// +build filament,windows - -/* - * Copyright 2019-2020 by Nedim Sabic Sabic - * https://www.fibratus.io - * All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package filament - -import ( - "errors" - "github.com/rabbitstack/fibratus/pkg/filament/cpython" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" -) - -var ( - seq = cpython.PyUnicodeFromString("seq") - pid = cpython.PyUnicodeFromString("pid") - ppid = cpython.PyUnicodeFromString("ppid") - cwd = cpython.PyUnicodeFromString("cwd") - exec = cpython.PyUnicodeFromString("exe") - comm = cpython.PyUnicodeFromString("comm") - sid = cpython.PyUnicodeFromString("sid") - tid = cpython.PyUnicodeFromString("tid") - cpu = cpython.PyUnicodeFromString("cpu") - name = cpython.PyUnicodeFromString("name") - cat = cpython.PyUnicodeFromString("category") - desc = cpython.PyUnicodeFromString("description") - host = cpython.PyUnicodeFromString("host") - ts = cpython.PyUnicodeFromString("timestamp") - kparamsk = cpython.PyUnicodeFromString("kparams") - - errDictAllocate = errors.New("couldn't allocate a new dict") -) - -// newKDict constructs a Python dictionary object from the kernel event structure. This dictionary object is -// passed to the event dispatching function in the filament. -func newKDict(kevt *kevent.Kevent) (*cpython.Dict, error) { - kdict := cpython.NewDict() - if kdict.IsNull() { - return nil, errDictAllocate - } - - // insert canonical kevent fields - kdict.Insert(seq, cpython.NewPyObjectFromValue(kevt.Seq)) - kdict.Insert(pid, cpython.NewPyObjectFromValue(kevt.PID)) - kdict.Insert(tid, cpython.NewPyObjectFromValue(kevt.Tid)) - kdict.Insert(cpu, cpython.NewPyObjectFromValue(kevt.CPU)) - kdict.Insert(name, cpython.NewPyObjectFromValue(kevt.Name)) - kdict.Insert(cat, cpython.NewPyObjectFromValue(string(kevt.Category))) - kdict.Insert(desc, cpython.NewPyObjectFromValue(kevt.Description)) - kdict.Insert(host, cpython.NewPyObjectFromValue(kevt.Host)) - kdict.Insert(ts, cpython.NewPyObjectFromValue(kevt.Timestamp)) - - // insert process state fields - ps := kevt.PS - if ps != nil { - kdict.Insert(ppid, cpython.NewPyObjectFromValue(ps.Ppid)) - kdict.Insert(cwd, cpython.NewPyObjectFromValue(ps.Cwd)) - kdict.Insert(exec, cpython.NewPyObjectFromValue(ps.Name)) - kdict.Insert(comm, cpython.NewPyObjectFromValue(ps.Cmdline)) - kdict.Insert(sid, cpython.NewPyObjectFromValue(ps.SID)) - } - - // insert kevent parameters - kpars := cpython.NewDict() - for _, kpar := range kevt.Kparams { - var val interface{} - var err error - switch kpar.Type { - case kparams.Uint8: - val, err = kevt.Kparams.GetUint8(kpar.Name) - case kparams.Uint16, kparams.Port: - val, err = kevt.Kparams.GetUint16(kpar.Name) - case kparams.Uint32, kparams.PID, kparams.TID: - val, err = kevt.Kparams.GetUint32(kpar.Name) - case kparams.Uint64: - val, err = kevt.Kparams.GetUint64(kpar.Name) - case kparams.Time: - val, err = kevt.Kparams.GetTime(kpar.Name) - case kparams.IP: - val, err = kevt.Kparams.GetIP(kpar.Name) - default: - val = kevt.GetParamAsString(kpar.Name) - } - if err != nil { - continue - } - kparam := cpython.NewPyObjectFromValue(val) - if kparam.IsNull() { - continue - } - kpars.Insert(cpython.PyUnicodeFromString(kpar.Name), kparam) - } - - kdict.Insert(kparamsk, kpars.Object()) - - return kdict, nil -} diff --git a/pkg/filament/types.go b/pkg/filament/types.go index b23a5f8a5..8e2087b1b 100644 --- a/pkg/filament/types.go +++ b/pkg/filament/types.go @@ -19,15 +19,15 @@ package filament import ( + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/filter" - "github.com/rabbitstack/fibratus/pkg/kevent" ) // Filament defines the set of operations all filaments have to satisfy. Filament represents a full-fledged // Python interpreter that runs the modules given by users. type Filament interface { // Run consumes all events from the kernel event stream and dispatches them to the filament. - Run(<-chan *kevent.Kevent, <-chan error) error + Run(<-chan *event.Event, <-chan error) error // Close shutdowns the filament by releasing all allocated resources. Close() error // Filter returns the filter compiled from filament. diff --git a/pkg/filter/accessor.go b/pkg/filter/accessor.go index 49ea069bd..27124c495 100644 --- a/pkg/filter/accessor.go +++ b/pkg/filter/accessor.go @@ -20,9 +20,9 @@ package filter import ( "errors" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/filter/fields" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" "reflect" ) @@ -36,7 +36,7 @@ var ( // from the non-params constructs such as process' state or PE metadata. type Accessor interface { // Get fetches the parameter value for the specified filter field. - Get(f Field, evt *kevent.Kevent) (kparams.Value, error) + Get(f Field, evt *event.Event) (params.Value, error) // SetFields sets all fields declared in the expression. SetFields(fields []Field) // SetSegments sets all segments utilized in the function predicate expression. @@ -45,94 +45,94 @@ type Accessor interface { // given event. The condition is usually based on the event category, // but it can also include different circumstances, like the presence // of the process state or callstacks. - IsFieldAccessible(evt *kevent.Kevent) bool + IsFieldAccessible(evt *event.Event) bool } -// kevtAccessor extracts generic event values. -type kevtAccessor struct{} +// evtAccessor extracts generic event values. +type evtAccessor struct{} -func (kevtAccessor) SetFields([]Field) {} -func (kevtAccessor) SetSegments([]fields.Segment) {} -func (kevtAccessor) IsFieldAccessible(*kevent.Kevent) bool { return true } +func (evtAccessor) SetFields([]Field) {} +func (evtAccessor) SetSegments([]fields.Segment) {} +func (evtAccessor) IsFieldAccessible(*event.Event) bool { return true } -func newKevtAccessor() Accessor { - return &kevtAccessor{} +func newEventAccessor() Accessor { + return &evtAccessor{} } const timeFmt = "15:04:05" const dateFmt = "2006-01-02" -func (k *kevtAccessor) Get(f Field, kevt *kevent.Kevent) (kparams.Value, error) { +func (k *evtAccessor) Get(f Field, evt *event.Event) (params.Value, error) { switch f.Name { case fields.KevtSeq: - return kevt.Seq, nil + return evt.Seq, nil case fields.KevtPID: - return kevt.PID, nil + return evt.PID, nil case fields.KevtTID: - return kevt.Tid, nil + return evt.Tid, nil case fields.KevtCPU: - return kevt.CPU, nil + return evt.CPU, nil case fields.KevtName: - return kevt.Name, nil + return evt.Name, nil case fields.KevtCategory: - return string(kevt.Category), nil + return string(evt.Category), nil case fields.KevtDesc: - return kevt.Description, nil + return evt.Description, nil case fields.KevtHost: - return kevt.Host, nil + return evt.Host, nil case fields.KevtTime: - return kevt.Timestamp.Format(timeFmt), nil + return evt.Timestamp.Format(timeFmt), nil case fields.KevtTimeHour: - return uint8(kevt.Timestamp.Hour()), nil + return uint8(evt.Timestamp.Hour()), nil case fields.KevtTimeMin: - return uint8(kevt.Timestamp.Minute()), nil + return uint8(evt.Timestamp.Minute()), nil case fields.KevtTimeSec: - return uint8(kevt.Timestamp.Second()), nil + return uint8(evt.Timestamp.Second()), nil case fields.KevtTimeNs: - return kevt.Timestamp.UnixNano(), nil + return evt.Timestamp.UnixNano(), nil case fields.KevtDate: - return kevt.Timestamp.Format(dateFmt), nil + return evt.Timestamp.Format(dateFmt), nil case fields.KevtDateDay: - return uint8(kevt.Timestamp.Day()), nil + return uint8(evt.Timestamp.Day()), nil case fields.KevtDateMonth: - return uint8(kevt.Timestamp.Month()), nil + return uint8(evt.Timestamp.Month()), nil case fields.KevtDateTz: - tz, _ := kevt.Timestamp.Zone() + tz, _ := evt.Timestamp.Zone() return tz, nil case fields.KevtDateYear: - return uint32(kevt.Timestamp.Year()), nil + return uint32(evt.Timestamp.Year()), nil case fields.KevtDateWeek: - _, week := kevt.Timestamp.ISOWeek() + _, week := evt.Timestamp.ISOWeek() return uint8(week), nil case fields.KevtDateWeekday: - return kevt.Timestamp.Weekday().String(), nil + return evt.Timestamp.Weekday().String(), nil case fields.KevtNparams: - return uint64(kevt.Kparams.Len()), nil + return uint64(evt.Params.Len()), nil case fields.KevtArg: // lookup the parameter from the field argument // and depending on the parameter type, return // the respective value. The field format is - // expressed as kevt.arg[cmdline] where the string + // expressed as evt.arg[cmdline] where the string // inside brackets represents the parameter name name := f.Arg - par, err := kevt.Kparams.Get(name) + par, err := evt.Params.Get(name) if err != nil { return nil, err } switch par.Type { - case kparams.Uint8: - return kevt.Kparams.GetUint8(name) - case kparams.Uint16, kparams.Port: - return kevt.Kparams.GetUint16(name) - case kparams.Uint32, kparams.PID, kparams.TID: - return kevt.Kparams.GetUint32(name) - case kparams.Uint64: - return kevt.Kparams.GetUint64(name) - case kparams.Time: - return kevt.Kparams.GetTime(name) + case params.Uint8: + return evt.Params.GetUint8(name) + case params.Uint16, params.Port: + return evt.Params.GetUint16(name) + case params.Uint32, params.PID, params.TID: + return evt.Params.GetUint32(name) + case params.Uint64: + return evt.Params.GetUint64(name) + case params.Time: + return evt.Params.GetTime(name) default: - return kevt.GetParamAsString(name), nil + return evt.GetParamAsString(name), nil } } @@ -189,7 +189,7 @@ func (f *filter) narrowAccessors() { } if removeKevtAccessor { - f.removeAccessor(&kevtAccessor{}) + f.removeAccessor(&evtAccessor{}) } if removePsAccessor { f.removeAccessor(&psAccessor{}) diff --git a/pkg/filter/accessor_windows.go b/pkg/filter/accessor_windows.go index 5ad8d5969..e3069ee59 100644 --- a/pkg/filter/accessor_windows.go +++ b/pkg/filter/accessor_windows.go @@ -22,7 +22,6 @@ import ( "errors" "expvar" "github.com/rabbitstack/fibratus/pkg/fs" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/rabbitstack/fibratus/pkg/network" psnap "github.com/rabbitstack/fibratus/pkg/ps" "github.com/rabbitstack/fibratus/pkg/util/cmdline" @@ -33,9 +32,9 @@ import ( "strings" "time" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/filter/fields" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" "github.com/rabbitstack/fibratus/pkg/pe" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" ) @@ -59,7 +58,7 @@ func GetAccessors() []Accessor { newMemAccessor(), newDNSAccessor(), newFileAccessor(), - newKevtAccessor(), + newEventAccessor(), newImageAccessor(), newThreadAccessor(), newHandleAccessor(), @@ -69,11 +68,11 @@ func GetAccessors() []Accessor { } } -func getParentPs(kevt *kevent.Kevent) *pstypes.PS { - if kevt.PS == nil { +func getParentPs(e *event.Event) *pstypes.PS { + if e.PS == nil { return nil } - return kevt.PS.Parent + return e.PS.Parent } // psAccessor extracts process's state or event specific values. @@ -83,181 +82,181 @@ type psAccessor struct { func (psAccessor) SetFields([]Field) {} func (psAccessor) SetSegments([]fields.Segment) {} -func (psAccessor) IsFieldAccessible(kevt *kevent.Kevent) bool { - return kevt.PS != nil || kevt.Category == ktypes.Process +func (psAccessor) IsFieldAccessible(e *event.Event) bool { + return e.PS != nil || e.Category == event.Process } func newPSAccessor(psnap psnap.Snapshotter) Accessor { return &psAccessor{psnap: psnap} } -func (ps *psAccessor) Get(f Field, kevt *kevent.Kevent) (kparams.Value, error) { +func (ps *psAccessor) Get(f Field, e *event.Event) (params.Value, error) { switch f.Name { case fields.PsPid: // identifier of the process that is generating the event - return kevt.PID, nil + return e.PID, nil case fields.PsSiblingPid, fields.PsChildPid: - if kevt.Category != ktypes.Process { + if e.Category != event.Process { return nil, nil } - // the id of a created child process. `kevt.PID` is the parent process id - return kevt.Kparams.GetPid() + // the id of a created child process. `e.PID` is the parent process id + return e.Params.GetPid() case fields.PsPpid: - ps := kevt.PS + ps := e.PS if ps == nil { return nil, ErrPsNil } return ps.Ppid, nil case fields.PsName: - ps := kevt.PS + ps := e.PS if ps == nil { return nil, ErrPsNil } return ps.Name, nil case fields.PsSiblingName, fields.PsChildName: - if kevt.Category != ktypes.Process { + if e.Category != event.Process { return nil, nil } - return kevt.Kparams.GetString(kparams.ProcessName) + return e.Params.GetString(params.ProcessName) case fields.PsComm, fields.PsCmdline: - ps := kevt.PS + ps := e.PS if ps == nil { return nil, ErrPsNil } return ps.Cmdline, nil case fields.PsSiblingComm, fields.PsChildCmdline: - if kevt.Category != ktypes.Process { + if e.Category != event.Process { return nil, nil } - return kevt.Kparams.GetString(kparams.Cmdline) + return e.Params.GetString(params.Cmdline) case fields.PsExe: - ps := kevt.PS + ps := e.PS if ps == nil { return nil, ErrPsNil } return ps.Exe, nil case fields.PsSiblingExe, fields.PsChildExe: - if kevt.Category != ktypes.Process { + if e.Category != event.Process { return nil, nil } - return kevt.Kparams.GetString(kparams.Exe) + return e.Params.GetString(params.Exe) case fields.PsArgs: - ps := kevt.PS + ps := e.PS if ps == nil { return nil, ErrPsNil } return ps.Args, nil case fields.PsSiblingArgs, fields.PsChildArgs: - if kevt.Category != ktypes.Process { + if e.Category != event.Process { return nil, nil } - cmndline, err := kevt.Kparams.GetString(kparams.Cmdline) + cmndline, err := e.Params.GetString(params.Cmdline) if err != nil { return nil, err } return cmdline.Split(cmndline), nil case fields.PsCwd: - ps := kevt.PS + ps := e.PS if ps == nil { return nil, ErrPsNil } return ps.Cwd, nil case fields.PsSID: - ps := kevt.PS + ps := e.PS if ps == nil { return nil, ErrPsNil } return ps.SID, nil case fields.PsSiblingSID, fields.PsChildSID: - if kevt.Category != ktypes.Process { + if e.Category != event.Process { return nil, nil } - sid, err := kevt.Kparams.GetSID() + sid, err := e.Params.GetSID() if err != nil { return nil, err } return sid.String(), nil case fields.PsSiblingDomain, fields.PsChildDomain: - if kevt.Category != ktypes.Process { + if e.Category != event.Process { return nil, nil } - return kevt.Kparams.GetString(kparams.Domain) + return e.Params.GetString(params.Domain) case fields.PsSiblingUsername, fields.PsChildUsername: - if kevt.Category != ktypes.Process { + if e.Category != event.Process { return nil, nil } - return kevt.Kparams.GetString(kparams.Username) + return e.Params.GetString(params.Username) case fields.PsChildIsWOW64Field: - if kevt.Category != ktypes.Process { + if e.Category != event.Process { return nil, nil } - return (kevt.Kparams.MustGetUint32(kparams.ProcessFlags) & kevent.PsWOW64) != 0, nil + return (e.Params.MustGetUint32(params.ProcessFlags) & event.PsWOW64) != 0, nil case fields.PsChildIsPackagedField: - if kevt.Category != ktypes.Process { + if e.Category != event.Process { return nil, nil } - return (kevt.Kparams.MustGetUint32(kparams.ProcessFlags) & kevent.PsPackaged) != 0, nil + return (e.Params.MustGetUint32(params.ProcessFlags) & event.PsPackaged) != 0, nil case fields.PsChildIsProtectedField: - if kevt.Category != ktypes.Process { + if e.Category != event.Process { return nil, nil } - return (kevt.Kparams.MustGetUint32(kparams.ProcessFlags) & kevent.PsProtected) != 0, nil + return (e.Params.MustGetUint32(params.ProcessFlags) & event.PsProtected) != 0, nil case fields.PsIsWOW64Field: - ps := kevt.PS + ps := e.PS if ps == nil { return nil, ErrPsNil } return ps.IsWOW64, nil case fields.PsIsPackagedField: - ps := kevt.PS + ps := e.PS if ps == nil { return nil, ErrPsNil } return ps.IsPackaged, nil case fields.PsIsProtectedField: - ps := kevt.PS + ps := e.PS if ps == nil { return nil, ErrPsNil } return ps.IsProtected, nil case fields.PsDomain: - ps := kevt.PS + ps := e.PS if ps == nil { return nil, ErrPsNil } return ps.Domain, nil case fields.PsUsername: - ps := kevt.PS + ps := e.PS if ps == nil { return nil, ErrPsNil } return ps.Username, nil case fields.PsSessionID: - ps := kevt.PS + ps := e.PS if ps == nil { return nil, nil } return ps.SessionID, nil case fields.PsAccessMask: - if kevt.Type != ktypes.OpenProcess { + if e.Type != event.OpenProcess { return nil, nil } - return kevt.Kparams.GetString(kparams.DesiredAccess) + return e.Params.GetString(params.DesiredAccess) case fields.PsAccessMaskNames: - if kevt.Type != ktypes.OpenProcess { + if e.Type != event.OpenProcess { return nil, nil } - return kevt.GetFlagsAsSlice(kparams.DesiredAccess), nil + return e.GetFlagsAsSlice(params.DesiredAccess), nil case fields.PsAccessStatus: - if kevt.Type != ktypes.OpenProcess { + if e.Type != event.OpenProcess { return nil, nil } - return kevt.GetParamAsString(kparams.NTStatus), nil + return e.GetParamAsString(params.NTStatus), nil case fields.PsSiblingSessionID, fields.PsChildSessionID: - if kevt.Category != ktypes.Process { + if e.Category != event.Process { return nil, nil } - return kevt.Kparams.GetUint32(kparams.SessionID) + return e.Params.GetUint32(params.SessionID) case fields.PsModuleNames: - ps := kevt.PS + ps := e.PS if ps == nil { return nil, ErrPsNil } @@ -267,23 +266,23 @@ func (ps *psAccessor) Get(f Field, kevt *kevent.Kevent) (kparams.Value, error) { } return mods, nil case fields.PsUUID: - ps := kevt.PS + ps := e.PS if ps == nil { return nil, ErrPsNil } return ps.UUID(), nil case fields.PsParentUUID: - ps := getParentPs(kevt) + ps := getParentPs(e) if ps == nil { return nil, ErrPsNil } return ps.UUID(), nil case fields.PsChildUUID: - if kevt.Category != ktypes.Process { + if e.Category != event.Process { return nil, nil } - pid, err := kevt.Kparams.GetPid() + pid, err := e.Params.GetPid() if err != nil { return nil, err } @@ -298,7 +297,7 @@ func (ps *psAccessor) Get(f Field, kevt *kevent.Kevent) (kparams.Value, error) { return proc.UUID(), nil case fields.PsHandleNames: - ps := kevt.PS + ps := e.PS if ps == nil { return nil, ErrPsNil } @@ -308,7 +307,7 @@ func (ps *psAccessor) Get(f Field, kevt *kevent.Kevent) (kparams.Value, error) { } return handles, nil case fields.PsHandleTypes: - ps := kevt.PS + ps := e.PS if ps == nil { return nil, ErrPsNil } @@ -321,67 +320,67 @@ func (ps *psAccessor) Get(f Field, kevt *kevent.Kevent) (kparams.Value, error) { } return types, nil case fields.PsParentPid: - parent := getParentPs(kevt) + parent := getParentPs(e) if parent == nil { return nil, ErrPsNil } return parent.PID, nil case fields.PsParentName: - parent := getParentPs(kevt) + parent := getParentPs(e) if parent == nil { return nil, ErrPsNil } return parent.Name, nil case fields.PsParentComm, fields.PsParentCmdline: - parent := getParentPs(kevt) + parent := getParentPs(e) if parent == nil { return nil, ErrPsNil } return parent.Cmdline, nil case fields.PsParentExe: - parent := getParentPs(kevt) + parent := getParentPs(e) if parent == nil { return nil, ErrPsNil } return parent.Exe, nil case fields.PsParentArgs: - parent := getParentPs(kevt) + parent := getParentPs(e) if parent == nil { return nil, ErrPsNil } return parent.Args, nil case fields.PsParentCwd: - parent := getParentPs(kevt) + parent := getParentPs(e) if parent == nil { return nil, ErrPsNil } return parent.Cwd, nil case fields.PsParentSID: - parent := getParentPs(kevt) + parent := getParentPs(e) if parent == nil { return nil, ErrPsNil } return parent.SID, nil case fields.PsParentDomain: - ps := getParentPs(kevt) + ps := getParentPs(e) if ps == nil { return nil, ErrPsNil } return ps.Domain, nil case fields.PsParentUsername: - ps := getParentPs(kevt) + ps := getParentPs(e) if ps == nil { return nil, ErrPsNil } return ps.Username, nil case fields.PsParentSessionID: - ps := getParentPs(kevt) + ps := getParentPs(e) if ps == nil { return nil, ErrPsNil } return ps.SessionID, nil case fields.PsParentEnvs: - ps := getParentPs(kevt) + ps := getParentPs(e) if ps == nil { return nil, ErrPsNil } @@ -391,7 +390,7 @@ func (ps *psAccessor) Get(f Field, kevt *kevent.Kevent) (kparams.Value, error) { } return envs, nil case fields.PsParentHandles: - ps := getParentPs(kevt) + ps := getParentPs(e) if ps == nil { return nil, ErrPsNil } @@ -401,7 +400,7 @@ func (ps *psAccessor) Get(f Field, kevt *kevent.Kevent) (kparams.Value, error) { } return handles, nil case fields.PsParentHandleTypes: - ps := getParentPs(kevt) + ps := getParentPs(e) if ps == nil { return nil, ErrPsNil } @@ -414,51 +413,51 @@ func (ps *psAccessor) Get(f Field, kevt *kevent.Kevent) (kparams.Value, error) { } return types, nil case fields.PsParentIsWOW64Field: - ps := getParentPs(kevt) + ps := getParentPs(e) if ps == nil { return nil, ErrPsNil } return ps.IsWOW64, nil case fields.PsParentIsPackagedField: - ps := getParentPs(kevt) + ps := getParentPs(e) if ps == nil { return nil, ErrPsNil } return ps.IsPackaged, nil case fields.PsParentIsProtectedField: - ps := getParentPs(kevt) + ps := getParentPs(e) if ps == nil { return nil, ErrPsNil } return ps.IsProtected, nil case fields.PsAncestors: - if kevt.PS != nil { + if e.PS != nil { ancestors := make([]*pstypes.PS, 0) walk := func(proc *pstypes.PS) { ancestors = append(ancestors, proc) } - pstypes.Walk(walk, kevt.PS) + pstypes.Walk(walk, e.PS) return ancestors, nil } return nil, ErrPsNil case fields.PsModules: - if kevt.PS != nil { - return kevt.PS.Modules, nil + if e.PS != nil { + return e.PS.Modules, nil } return nil, ErrPsNil case fields.PsThreads: - if kevt.PS != nil { - return kevt.PS.Threads, nil + if e.PS != nil { + return e.PS.Threads, nil } return nil, ErrPsNil case fields.PsMmaps: - if kevt.PS != nil { - return kevt.PS.Mmaps, nil + if e.PS != nil { + return e.PS.Mmaps, nil } return nil, ErrPsNil case fields.PsAncestor: - if kevt.PS != nil { + if e.PS != nil { n := -1 // if the index is given try to parse it // to access the ancestor at the given level. @@ -477,7 +476,7 @@ func (ps *psAccessor) Get(f Field, kevt *kevent.Kevent) (kparams.Value, error) { walk := func(proc *pstypes.PS) { ancestors = append(ancestors, proc.Name) } - pstypes.Walk(walk, kevt.PS) + pstypes.Walk(walk, e.PS) if n >= 0 { // return a single ancestor indicated by the index @@ -493,7 +492,7 @@ func (ps *psAccessor) Get(f Field, kevt *kevent.Kevent) (kparams.Value, error) { } return nil, ErrPsNil case fields.PsEnvs: - ps := kevt.PS + ps := e.PS if ps == nil { return nil, ErrPsNil } @@ -531,51 +530,51 @@ type threadAccessor struct{} func (threadAccessor) SetFields([]Field) {} func (threadAccessor) SetSegments([]fields.Segment) {} -func (threadAccessor) IsFieldAccessible(kevt *kevent.Kevent) bool { - return !kevt.Callstack.IsEmpty() || kevt.Category == ktypes.Thread +func (threadAccessor) IsFieldAccessible(e *event.Event) bool { + return !e.Callstack.IsEmpty() || e.Category == event.Thread } func newThreadAccessor() Accessor { return &threadAccessor{} } -func (t *threadAccessor) Get(f Field, e *kevent.Kevent) (kparams.Value, error) { +func (t *threadAccessor) Get(f Field, e *event.Event) (params.Value, error) { switch f.Name { case fields.ThreadBasePrio: - return e.Kparams.GetUint8(kparams.BasePrio) + return e.Params.GetUint8(params.BasePrio) case fields.ThreadIOPrio: - return e.Kparams.GetUint8(kparams.IOPrio) + return e.Params.GetUint8(params.IOPrio) case fields.ThreadPagePrio: - return e.Kparams.GetUint8(kparams.PagePrio) + return e.Params.GetUint8(params.PagePrio) case fields.ThreadKstackBase: - return e.GetParamAsString(kparams.KstackBase), nil + return e.GetParamAsString(params.KstackBase), nil case fields.ThreadKstackLimit: - return e.GetParamAsString(kparams.KstackLimit), nil + return e.GetParamAsString(params.KstackLimit), nil case fields.ThreadUstackBase: - return e.GetParamAsString(kparams.UstackBase), nil + return e.GetParamAsString(params.UstackBase), nil case fields.ThreadUstackLimit: - return e.GetParamAsString(kparams.UstackLimit), nil + return e.GetParamAsString(params.UstackLimit), nil case fields.ThreadEntrypoint, fields.ThreadStartAddress: - return e.GetParamAsString(kparams.StartAddress), nil + return e.GetParamAsString(params.StartAddress), nil case fields.ThreadPID: - return e.Kparams.GetUint32(kparams.ProcessID) + return e.Params.GetUint32(params.ProcessID) case fields.ThreadTEB: - return e.GetParamAsString(kparams.TEB), nil + return e.GetParamAsString(params.TEB), nil case fields.ThreadAccessMask: - if e.Type != ktypes.OpenThread { + if e.Type != event.OpenThread { return nil, nil } - return e.Kparams.GetString(kparams.DesiredAccess) + return e.Params.GetString(params.DesiredAccess) case fields.ThreadAccessMaskNames: - if e.Type != ktypes.OpenThread { + if e.Type != event.OpenThread { return nil, nil } - return e.GetFlagsAsSlice(kparams.DesiredAccess), nil + return e.GetFlagsAsSlice(params.DesiredAccess), nil case fields.ThreadAccessStatus: - if e.Type != ktypes.OpenThread { + if e.Type != event.OpenThread { return nil, nil } - return e.GetParamAsString(kparams.NTStatus), nil + return e.GetParamAsString(params.NTStatus), nil case fields.ThreadCallstackSummary: return e.Callstack.Summary(), nil case fields.ThreadCallstackDetail: @@ -625,15 +624,15 @@ func (t *threadAccessor) Get(f Field, e *kevent.Kevent) (kparams.Value, error) { case fields.ThreadCallstack: return e.Callstack, nil case fields.ThreadStartAddressSymbol: - if e.Type != ktypes.CreateThread { + if e.Type != event.CreateThread { return nil, nil } - return e.GetParamAsString(kparams.StartAddressSymbol), nil + return e.GetParamAsString(params.StartAddressSymbol), nil case fields.ThreadStartAddressModule: - if e.Type != ktypes.CreateThread { + if e.Type != event.CreateThread { return nil, nil } - return e.GetParamAsString(kparams.StartAddressModule), nil + return e.GetParamAsString(params.StartAddressModule), nil case fields.ThreadCallstackAddresses: return e.Callstack.Addresses(), nil case fields.ThreadCallstackFinalUserModuleName, fields.ThreadCallstackFinalUserModulePath: @@ -713,75 +712,75 @@ func (fileAccessor) SetFields(fields []Field) { } func (fileAccessor) SetSegments([]fields.Segment) {} -func (fileAccessor) IsFieldAccessible(kevt *kevent.Kevent) bool { return kevt.Category == ktypes.File } +func (fileAccessor) IsFieldAccessible(e *event.Event) bool { return e.Category == event.File } func newFileAccessor() Accessor { return &fileAccessor{} } -func (l *fileAccessor) Get(f Field, kevt *kevent.Kevent) (kparams.Value, error) { +func (l *fileAccessor) Get(f Field, e *event.Event) (params.Value, error) { switch f.Name { case fields.FilePath: - return kevt.GetParamAsString(kparams.FilePath), nil + return e.GetParamAsString(params.FilePath), nil case fields.FileName: - return filepath.Base(kevt.GetParamAsString(kparams.FilePath)), nil + return filepath.Base(e.GetParamAsString(params.FilePath)), nil case fields.FileExtension: - return filepath.Ext(kevt.GetParamAsString(kparams.FilePath)), nil + return filepath.Ext(e.GetParamAsString(params.FilePath)), nil case fields.FileOffset: - return kevt.Kparams.GetUint64(kparams.FileOffset) + return e.Params.GetUint64(params.FileOffset) case fields.FileIOSize: - return kevt.Kparams.GetUint32(kparams.FileIoSize) + return e.Params.GetUint32(params.FileIoSize) case fields.FileShareMask: - return kevt.GetParamAsString(kparams.FileShareMask), nil + return e.GetParamAsString(params.FileShareMask), nil case fields.FileOperation: - return kevt.GetParamAsString(kparams.FileOperation), nil + return e.GetParamAsString(params.FileOperation), nil case fields.FileObject: - return kevt.Kparams.GetUint64(kparams.FileObject) + return e.Params.GetUint64(params.FileObject) case fields.FileType: - return kevt.GetParamAsString(kparams.FileType), nil + return e.GetParamAsString(params.FileType), nil case fields.FileAttributes: - return kevt.GetFlagsAsSlice(kparams.FileAttributes), nil + return e.GetFlagsAsSlice(params.FileAttributes), nil case fields.FileStatus: - if kevt.Type != ktypes.CreateFile { + if e.Type != event.CreateFile { return nil, nil } - return kevt.GetParamAsString(kparams.NTStatus), nil + return e.GetParamAsString(params.NTStatus), nil case fields.FileViewBase: - return kevt.GetParamAsString(kparams.FileViewBase), nil + return e.GetParamAsString(params.FileViewBase), nil case fields.FileViewSize: - return kevt.Kparams.GetUint64(kparams.FileViewSize) + return e.Params.GetUint64(params.FileViewSize) case fields.FileViewType: - return kevt.GetParamAsString(kparams.FileViewSectionType), nil + return e.GetParamAsString(params.FileViewSectionType), nil case fields.FileViewProtection: - return kevt.GetParamAsString(kparams.MemProtect), nil + return e.GetParamAsString(params.MemProtect), nil case fields.FileIsDriverVulnerable, fields.FileIsDriverMalicious: - if kevt.IsCreateDisposition() && kevt.IsSuccess() { - return isLOLDriver(f.Name, kevt) + if e.IsCreateDisposition() && e.IsSuccess() { + return isLOLDriver(f.Name, e) } return false, nil case fields.FileIsDLL: - return kevt.Kparams.GetBool(kparams.FileIsDLL) + return e.Params.GetBool(params.FileIsDLL) case fields.FileIsDriver: - return kevt.Kparams.GetBool(kparams.FileIsDriver) + return e.Params.GetBool(params.FileIsDriver) case fields.FileIsExecutable: - return kevt.Kparams.GetBool(kparams.FileIsExecutable) + return e.Params.GetBool(params.FileIsExecutable) case fields.FilePID: - return kevt.Kparams.GetPid() + return e.Params.GetPid() case fields.FileKey: - return kevt.Kparams.GetUint64(kparams.FileKey) + return e.Params.GetUint64(params.FileKey) case fields.FileInfoClass: - return kevt.GetParamAsString(kparams.FileInfoClass), nil + return e.GetParamAsString(params.FileInfoClass), nil case fields.FileInfoAllocationSize: - if kevt.Kparams.TryGetUint32(kparams.FileInfoClass) == fs.AllocationClass { - return kevt.Kparams.GetUint64(kparams.FileExtraInfo) + if e.Params.TryGetUint32(params.FileInfoClass) == fs.AllocationClass { + return e.Params.GetUint64(params.FileExtraInfo) } case fields.FileInfoEOFSize: - if kevt.Kparams.TryGetUint32(kparams.FileInfoClass) == fs.EOFClass { - return kevt.Kparams.GetUint64(kparams.FileExtraInfo) + if e.Params.TryGetUint32(params.FileInfoClass) == fs.EOFClass { + return e.Params.GetUint64(params.FileExtraInfo) } case fields.FileInfoIsDispositionDeleteFile: - return kevt.Kparams.TryGetUint32(kparams.FileInfoClass) == fs.DispositionClass && - kevt.Kparams.TryGetUint64(kparams.FileExtraInfo) > 0, nil + return e.Params.TryGetUint32(params.FileInfoClass) == fs.DispositionClass && + e.Params.TryGetUint64(params.FileExtraInfo) > 0, nil } return nil, nil @@ -795,20 +794,20 @@ func (imageAccessor) SetFields(fields []Field) { } func (imageAccessor) SetSegments([]fields.Segment) {} -func (imageAccessor) IsFieldAccessible(kevt *kevent.Kevent) bool { - return kevt.Category == ktypes.Image +func (imageAccessor) IsFieldAccessible(e *event.Event) bool { + return e.Category == event.Image } func newImageAccessor() Accessor { return &imageAccessor{} } -func (i *imageAccessor) Get(f Field, kevt *kevent.Kevent) (kparams.Value, error) { - if kevt.IsLoadImage() && (f.Name == fields.ImageSignatureType || f.Name == fields.ImageSignatureLevel || f.Name.IsImageCert()) { - filename := kevt.GetParamAsString(kparams.ImagePath) - addr := kevt.Kparams.MustGetUint64(kparams.ImageBase) - typ := kevt.Kparams.MustGetUint32(kparams.ImageSignatureType) - level := kevt.Kparams.MustGetUint32(kparams.ImageSignatureLevel) +func (i *imageAccessor) Get(f Field, e *event.Event) (params.Value, error) { + if e.IsLoadImage() && (f.Name == fields.ImageSignatureType || f.Name == fields.ImageSignatureLevel || f.Name.IsImageCert()) { + filename := e.GetParamAsString(params.ImagePath) + addr := e.Params.MustGetUint64(params.ImageBase) + typ := e.Params.MustGetUint32(params.ImageSignatureType) + level := e.Params.MustGetUint32(params.ImageSignatureLevel) sign := signature.GetSignatures().GetSignature(addr) @@ -854,62 +853,62 @@ func (i *imageAccessor) Get(f Field, kevt *kevent.Kevent) (kparams.Value, error) signature.GetSignatures().PutSignature(addr, sign) } // reset signature type/level parameters - _ = kevt.Kparams.SetValue(kparams.ImageSignatureType, sign.Type) - _ = kevt.Kparams.SetValue(kparams.ImageSignatureLevel, sign.Level) + _ = e.Params.SetValue(params.ImageSignatureType, sign.Type) + _ = e.Params.SetValue(params.ImageSignatureLevel, sign.Level) } // append certificate parameters if sign.HasCertificate() { - kevt.AppendParam(kparams.ImageCertIssuer, kparams.UnicodeString, sign.Cert.Issuer) - kevt.AppendParam(kparams.ImageCertSubject, kparams.UnicodeString, sign.Cert.Subject) - kevt.AppendParam(kparams.ImageCertSerial, kparams.UnicodeString, sign.Cert.SerialNumber) - kevt.AppendParam(kparams.ImageCertNotAfter, kparams.Time, sign.Cert.NotAfter) - kevt.AppendParam(kparams.ImageCertNotBefore, kparams.Time, sign.Cert.NotBefore) + e.AppendParam(params.ImageCertIssuer, params.UnicodeString, sign.Cert.Issuer) + e.AppendParam(params.ImageCertSubject, params.UnicodeString, sign.Cert.Subject) + e.AppendParam(params.ImageCertSerial, params.UnicodeString, sign.Cert.SerialNumber) + e.AppendParam(params.ImageCertNotAfter, params.Time, sign.Cert.NotAfter) + e.AppendParam(params.ImageCertNotBefore, params.Time, sign.Cert.NotBefore) } } switch f.Name { case fields.ImagePath: - return kevt.GetParamAsString(kparams.ImagePath), nil + return e.GetParamAsString(params.ImagePath), nil case fields.ImageName: - return filepath.Base(kevt.GetParamAsString(kparams.ImagePath)), nil + return filepath.Base(e.GetParamAsString(params.ImagePath)), nil case fields.ImageDefaultAddress: - return kevt.GetParamAsString(kparams.ImageDefaultBase), nil + return e.GetParamAsString(params.ImageDefaultBase), nil case fields.ImageBase: - return kevt.GetParamAsString(kparams.ImageBase), nil + return e.GetParamAsString(params.ImageBase), nil case fields.ImageSize: - return kevt.Kparams.GetUint64(kparams.ImageSize) + return e.Params.GetUint64(params.ImageSize) case fields.ImageChecksum: - return kevt.Kparams.GetUint32(kparams.ImageCheckSum) + return e.Params.GetUint32(params.ImageCheckSum) case fields.ImagePID: - return kevt.Kparams.GetPid() + return e.Params.GetPid() case fields.ImageSignatureType: - return kevt.GetParamAsString(kparams.ImageSignatureType), nil + return e.GetParamAsString(params.ImageSignatureType), nil case fields.ImageSignatureLevel: - return kevt.GetParamAsString(kparams.ImageSignatureLevel), nil + return e.GetParamAsString(params.ImageSignatureLevel), nil case fields.ImageCertSubject: - return kevt.GetParamAsString(kparams.ImageCertSubject), nil + return e.GetParamAsString(params.ImageCertSubject), nil case fields.ImageCertIssuer: - return kevt.GetParamAsString(kparams.ImageCertIssuer), nil + return e.GetParamAsString(params.ImageCertIssuer), nil case fields.ImageCertSerial: - return kevt.GetParamAsString(kparams.ImageCertSerial), nil + return e.GetParamAsString(params.ImageCertSerial), nil case fields.ImageCertBefore: - return kevt.Kparams.GetTime(kparams.ImageCertNotBefore) + return e.Params.GetTime(params.ImageCertNotBefore) case fields.ImageCertAfter: - return kevt.Kparams.GetTime(kparams.ImageCertNotAfter) + return e.Params.GetTime(params.ImageCertNotAfter) case fields.ImageIsDriverVulnerable, fields.ImageIsDriverMalicious: - if kevt.IsLoadImage() { - return isLOLDriver(f.Name, kevt) + if e.IsLoadImage() { + return isLOLDriver(f.Name, e) } return false, nil case fields.ImageIsDLL: - return kevt.Kparams.GetBool(kparams.FileIsDLL) + return e.Params.GetBool(params.FileIsDLL) case fields.ImageIsDriver: - return kevt.Kparams.GetBool(kparams.FileIsDriver) + return e.Params.GetBool(params.FileIsDriver) case fields.ImageIsExecutable: - return kevt.Kparams.GetBool(kparams.FileIsExecutable) + return e.Params.GetBool(params.FileIsExecutable) case fields.ImageIsDotnet: - return kevt.Kparams.GetBool(kparams.FileIsDotnet) + return e.Params.GetBool(params.FileIsDotnet) } return nil, nil @@ -920,32 +919,32 @@ type registryAccessor struct{} func (registryAccessor) SetFields([]Field) {} func (registryAccessor) SetSegments([]fields.Segment) {} -func (registryAccessor) IsFieldAccessible(kevt *kevent.Kevent) bool { - return kevt.Category == ktypes.Registry +func (registryAccessor) IsFieldAccessible(e *event.Event) bool { + return e.Category == event.Registry } func newRegistryAccessor() Accessor { return ®istryAccessor{} } -func (r *registryAccessor) Get(f Field, kevt *kevent.Kevent) (kparams.Value, error) { +func (r *registryAccessor) Get(f Field, e *event.Event) (params.Value, error) { switch f.Name { case fields.RegistryPath: - return kevt.GetParamAsString(kparams.RegPath), nil + return e.GetParamAsString(params.RegPath), nil case fields.RegistryKeyName: - if kevt.IsRegSetValue() { - return filepath.Base(filepath.Dir(kevt.GetParamAsString(kparams.RegPath))), nil + if e.IsRegSetValue() { + return filepath.Base(filepath.Dir(e.GetParamAsString(params.RegPath))), nil } else { - return filepath.Base(kevt.GetParamAsString(kparams.RegPath)), nil + return filepath.Base(e.GetParamAsString(params.RegPath)), nil } case fields.RegistryKeyHandle: - return kevt.GetParamAsString(kparams.RegKeyHandle), nil + return e.GetParamAsString(params.RegKeyHandle), nil case fields.RegistryValue: - return kevt.Kparams.GetRaw(kparams.RegValue) + return e.Params.GetRaw(params.RegValue) case fields.RegistryValueType: - return kevt.Kparams.GetString(kparams.RegValueType) + return e.Params.GetString(params.RegValueType) case fields.RegistryStatus: - return kevt.GetParamAsString(kparams.NTStatus), nil + return e.GetParamAsString(params.NTStatus), nil } return nil, nil @@ -967,34 +966,34 @@ func (n *networkAccessor) SetFields(flds []Field) { func (networkAccessor) SetSegments([]fields.Segment) {} -func (networkAccessor) IsFieldAccessible(kevt *kevent.Kevent) bool { - return kevt.Category == ktypes.Net +func (networkAccessor) IsFieldAccessible(e *event.Event) bool { + return e.Category == event.Net } func newNetworkAccessor() Accessor { return &networkAccessor{} } -func (n *networkAccessor) Get(f Field, kevt *kevent.Kevent) (kparams.Value, error) { +func (n *networkAccessor) Get(f Field, e *event.Event) (params.Value, error) { switch f.Name { case fields.NetDIP: - return kevt.Kparams.GetIP(kparams.NetDIP) + return e.Params.GetIP(params.NetDIP) case fields.NetSIP: - return kevt.Kparams.GetIP(kparams.NetSIP) + return e.Params.GetIP(params.NetSIP) case fields.NetDport: - return kevt.Kparams.GetUint16(kparams.NetDport) + return e.Params.GetUint16(params.NetDport) case fields.NetSport: - return kevt.Kparams.GetUint16(kparams.NetSport) + return e.Params.GetUint16(params.NetSport) case fields.NetDportName: - return kevt.Kparams.GetString(kparams.NetDportName) + return e.Params.GetString(params.NetDportName) case fields.NetSportName: - return kevt.Kparams.GetString(kparams.NetSportName) + return e.Params.GetString(params.NetSportName) case fields.NetL4Proto: - return kevt.GetParamAsString(kparams.NetL4Proto), nil + return e.GetParamAsString(params.NetL4Proto), nil case fields.NetPacketSize: - return kevt.Kparams.GetUint32(kparams.NetSize) + return e.Params.GetUint32(params.NetSize) case fields.NetDIPNames: - return n.resolveNamesForIP(kevt.Kparams.MustGetIP(kparams.NetDIP)) + return n.resolveNamesForIP(e.Params.MustGetIP(params.NetDIP)) case fields.NetSIPNames: - return n.resolveNamesForIP(kevt.Kparams.MustGetIP(kparams.NetSIP)) + return n.resolveNamesForIP(e.Params.MustGetIP(params.NetSIP)) } return nil, nil @@ -1016,22 +1015,22 @@ type handleAccessor struct{} func (handleAccessor) SetFields([]Field) {} func (handleAccessor) SetSegments([]fields.Segment) {} -func (handleAccessor) IsFieldAccessible(kevt *kevent.Kevent) bool { - return kevt.Category == ktypes.Handle +func (handleAccessor) IsFieldAccessible(e *event.Event) bool { + return e.Category == event.Handle } func newHandleAccessor() Accessor { return &handleAccessor{} } -func (h *handleAccessor) Get(f Field, kevt *kevent.Kevent) (kparams.Value, error) { +func (h *handleAccessor) Get(f Field, e *event.Event) (params.Value, error) { switch f.Name { case fields.HandleID: - return kevt.Kparams.GetUint32(kparams.HandleID) + return e.Params.GetUint32(params.HandleID) case fields.HandleType: - return kevt.GetParamAsString(kparams.HandleObjectTypeID), nil + return e.GetParamAsString(params.HandleObjectTypeID), nil case fields.HandleName: - return kevt.Kparams.GetString(kparams.HandleObjectName) + return e.Params.GetString(params.HandleObjectName) case fields.HandleObject: - return kevt.Kparams.GetUint64(kparams.HandleObject) + return e.Params.GetUint64(params.HandleObject) } return nil, nil @@ -1050,8 +1049,8 @@ func (pa *peAccessor) SetSegments(segments []fields.Segment) { pa.segments = segments } -func (peAccessor) IsFieldAccessible(kevt *kevent.Kevent) bool { - return kevt.PS != nil || kevt.IsLoadImage() +func (peAccessor) IsFieldAccessible(e *event.Event) bool { + return e.PS != nil || e.IsLoadImage() } // parserOpts traverses all fields/segments declared in the expression and @@ -1103,10 +1102,10 @@ func newPEAccessor() Accessor { return &peAccessor{} } -func (pa *peAccessor) Get(f Field, kevt *kevent.Kevent) (kparams.Value, error) { +func (pa *peAccessor) Get(f Field, e *event.Event) (params.Value, error) { var p *pe.PE - if kevt.PS != nil && kevt.PS.PE != nil { - p = kevt.PS.PE + if e.PS != nil && e.PS.PE != nil { + p = e.PS.PE } // PE enrichment is likely disabled. Load PE data lazily @@ -1116,13 +1115,13 @@ func (pa *peAccessor) Get(f Field, kevt *kevent.Kevent) (kparams.Value, error) { // original file name as part of the CreateProcess event, // then the parser obtains the PE metadata for the executable // path parameter - if (kevt.PS != nil && kevt.PS.Exe != "" && p == nil) || f.Name == fields.PePsChildFileName || f.Name == fields.PsChildPeFilename { + if (e.PS != nil && e.PS.Exe != "" && p == nil) || f.Name == fields.PePsChildFileName || f.Name == fields.PsChildPeFilename { var err error var exe string - if (f.Name == fields.PePsChildFileName || f.Name == fields.PsChildPeFilename) && kevt.IsCreateProcess() { - exe = kevt.GetParamAsString(kparams.Exe) + if (f.Name == fields.PePsChildFileName || f.Name == fields.PsChildPeFilename) && e.IsCreateProcess() { + exe = e.GetParamAsString(params.Exe) } else { - exe = kevt.PS.Exe + exe = e.PS.Exe } p, err = pe.ParseFile(exe, pa.parserOpts()...) if err != nil { @@ -1135,15 +1134,15 @@ func (pa *peAccessor) Get(f Field, kevt *kevent.Kevent) (kparams.Value, error) { // PE for loaded executables followed by fetching the PE // from process' memory at the base address of the loaded // executable image - if kevt.IsLoadImage() && f.Name.IsPeModified() { - filename := kevt.GetParamAsString(kparams.ImagePath) - isExecutable := filepath.Ext(filename) == ".exe" || kevt.Kparams.TryGetBool(kparams.FileIsExecutable) + if e.IsLoadImage() && f.Name.IsPeModified() { + filename := e.GetParamAsString(params.ImagePath) + isExecutable := filepath.Ext(filename) == ".exe" || e.Params.TryGetBool(params.FileIsExecutable) if !isExecutable { return nil, nil } - pid := kevt.Kparams.MustGetPid() - addr := kevt.Kparams.MustGetUint64(kparams.ImageBase) + pid := e.Params.MustGetPid() + addr := e.Params.MustGetUint64(params.ImageBase) file, err := pe.ParseFile(filename, pa.parserOpts()...) if err != nil { @@ -1170,7 +1169,7 @@ func (pa *peAccessor) Get(f Field, kevt *kevent.Kevent) (kparams.Value, error) { } if f.Name != fields.PePsChildFileName { - kevt.PS.PE = p + e.PS.PE = p } switch f.Name { @@ -1224,11 +1223,11 @@ func (pa *peAccessor) Get(f Field, kevt *kevent.Kevent) (kparams.Value, error) { } return p.Cert.NotBefore, nil case fields.PeIsDLL: - return kevt.Kparams.GetBool(kparams.FileIsDLL) + return e.Params.GetBool(params.FileIsDLL) case fields.PeIsDriver: - return kevt.Kparams.GetBool(kparams.FileIsDriver) + return e.Params.GetBool(params.FileIsDriver) case fields.PeIsExecutable: - return kevt.Kparams.GetBool(kparams.FileIsExecutable) + return e.Params.GetBool(params.FileIsExecutable) case fields.PeCompany: return p.VersionResources[pe.Company], nil case fields.PeCopyright: @@ -1278,28 +1277,28 @@ func (pa *peAccessor) Get(f Field, kevt *kevent.Kevent) (kparams.Value, error) { // memAccessor extracts parameters from memory alloc/free events. type memAccessor struct{} -func (memAccessor) SetFields([]Field) {} -func (memAccessor) SetSegments([]fields.Segment) {} -func (memAccessor) IsFieldAccessible(kevt *kevent.Kevent) bool { return kevt.Category == ktypes.Mem } +func (memAccessor) SetFields([]Field) {} +func (memAccessor) SetSegments([]fields.Segment) {} +func (memAccessor) IsFieldAccessible(e *event.Event) bool { return e.Category == event.Mem } func newMemAccessor() Accessor { return &memAccessor{} } -func (*memAccessor) Get(f Field, kevt *kevent.Kevent) (kparams.Value, error) { +func (*memAccessor) Get(f Field, e *event.Event) (params.Value, error) { switch f.Name { case fields.MemPageType: - return kevt.GetParamAsString(kparams.MemPageType), nil + return e.GetParamAsString(params.MemPageType), nil case fields.MemAllocType: - return kevt.GetParamAsString(kparams.MemAllocType), nil + return e.GetParamAsString(params.MemAllocType), nil case fields.MemProtection: - return kevt.GetParamAsString(kparams.MemProtect), nil + return e.GetParamAsString(params.MemProtect), nil case fields.MemBaseAddress: - return kevt.Kparams.GetUint64(kparams.MemBaseAddress) + return e.Params.GetUint64(params.MemBaseAddress) case fields.MemRegionSize: - return kevt.Kparams.GetUint64(kparams.MemRegionSize) + return e.Params.GetUint64(params.MemRegionSize) case fields.MemProtectionMask: - return kevt.Kparams.GetString(kparams.MemProtectMask) + return e.Params.GetString(params.MemProtectMask) } return nil, nil @@ -1310,26 +1309,26 @@ type dnsAccessor struct{} func (dnsAccessor) SetFields([]Field) {} func (dnsAccessor) SetSegments([]fields.Segment) {} -func (dnsAccessor) IsFieldAccessible(kevt *kevent.Kevent) bool { - return kevt.Type.Subcategory() == ktypes.DNS +func (dnsAccessor) IsFieldAccessible(e *event.Event) bool { + return e.Type.Subcategory() == event.DNS } func newDNSAccessor() Accessor { return &dnsAccessor{} } -func (*dnsAccessor) Get(f Field, kevt *kevent.Kevent) (kparams.Value, error) { +func (*dnsAccessor) Get(f Field, e *event.Event) (params.Value, error) { switch f.Name { case fields.DNSName: - return kevt.GetParamAsString(kparams.DNSName), nil + return e.GetParamAsString(params.DNSName), nil case fields.DNSRR: - return kevt.GetParamAsString(kparams.DNSRR), nil + return e.GetParamAsString(params.DNSRR), nil case fields.DNSRcode: - return kevt.GetParamAsString(kparams.DNSRcode), nil + return e.GetParamAsString(params.DNSRcode), nil case fields.DNSOptions: - return kevt.GetFlagsAsSlice(kparams.DNSOpts), nil + return e.GetFlagsAsSlice(params.DNSOpts), nil case fields.DNSAnswers: - return kevt.Kparams.GetSlice(kparams.DNSAnswers) + return e.Params.GetSlice(params.DNSAnswers) } return nil, nil @@ -1340,48 +1339,48 @@ type threadpoolAccessor struct{} func (threadpoolAccessor) SetFields([]Field) {} func (threadpoolAccessor) SetSegments([]fields.Segment) {} -func (threadpoolAccessor) IsFieldAccessible(e *kevent.Kevent) bool { - return e.Category == ktypes.Threadpool +func (threadpoolAccessor) IsFieldAccessible(e *event.Event) bool { + return e.Category == event.Threadpool } func newThreadpoolAccessor() Accessor { return &threadpoolAccessor{} } -func (*threadpoolAccessor) Get(f Field, e *kevent.Kevent) (kparams.Value, error) { +func (*threadpoolAccessor) Get(f Field, e *event.Event) (params.Value, error) { switch f.Name { case fields.ThreadpoolPoolID: - return e.GetParamAsString(kparams.ThreadpoolPoolID), nil + return e.GetParamAsString(params.ThreadpoolPoolID), nil case fields.ThreadpoolTaskID: - return e.GetParamAsString(kparams.ThreadpoolTaskID), nil + return e.GetParamAsString(params.ThreadpoolTaskID), nil case fields.ThreadpoolCallbackAddress: - return e.GetParamAsString(kparams.ThreadpoolCallback), nil + return e.GetParamAsString(params.ThreadpoolCallback), nil case fields.ThreadpoolCallbackSymbol: - return e.GetParamAsString(kparams.ThreadpoolCallbackSymbol), nil + return e.GetParamAsString(params.ThreadpoolCallbackSymbol), nil case fields.ThreadpoolCallbackModule: - return e.GetParamAsString(kparams.ThreadpoolCallbackModule), nil + return e.GetParamAsString(params.ThreadpoolCallbackModule), nil case fields.ThreadpoolCallbackContext: - return e.GetParamAsString(kparams.ThreadpoolContext), nil + return e.GetParamAsString(params.ThreadpoolContext), nil case fields.ThreadpoolCallbackContextRip: - return e.GetParamAsString(kparams.ThreadpoolContextRip), nil + return e.GetParamAsString(params.ThreadpoolContextRip), nil case fields.ThreadpoolCallbackContextRipSymbol: - return e.GetParamAsString(kparams.ThreadpoolContextRipSymbol), nil + return e.GetParamAsString(params.ThreadpoolContextRipSymbol), nil case fields.ThreadpoolCallbackContextRipModule: - return e.GetParamAsString(kparams.ThreadpoolContextRipModule), nil + return e.GetParamAsString(params.ThreadpoolContextRipModule), nil case fields.ThreadpoolSubprocessTag: - return e.GetParamAsString(kparams.ThreadpoolSubprocessTag), nil + return e.GetParamAsString(params.ThreadpoolSubprocessTag), nil case fields.ThreadpoolTimer: - return e.GetParamAsString(kparams.ThreadpoolTimer), nil + return e.GetParamAsString(params.ThreadpoolTimer), nil case fields.ThreadpoolTimerSubqueue: - return e.GetParamAsString(kparams.ThreadpoolTimerSubqueue), nil + return e.GetParamAsString(params.ThreadpoolTimerSubqueue), nil case fields.ThreadpoolTimerDuetime: - return e.Kparams.GetUint64(kparams.ThreadpoolTimerDuetime) + return e.Params.GetUint64(params.ThreadpoolTimerDuetime) case fields.ThreadpoolTimerPeriod: - return e.Kparams.GetUint32(kparams.ThreadpoolTimerPeriod) + return e.Params.GetUint32(params.ThreadpoolTimerPeriod) case fields.ThreadpoolTimerWindow: - return e.Kparams.GetUint32(kparams.ThreadpoolTimerWindow) + return e.Params.GetUint32(params.ThreadpoolTimerWindow) case fields.ThreadpoolTimerAbsolute: - return e.Kparams.GetBool(kparams.ThreadpoolTimerAbsolute) + return e.Params.GetBool(params.ThreadpoolTimerAbsolute) } return nil, nil diff --git a/pkg/filter/accessor_windows_test.go b/pkg/filter/accessor_windows_test.go index bf8bb7bba..949077f27 100644 --- a/pkg/filter/accessor_windows_test.go +++ b/pkg/filter/accessor_windows_test.go @@ -20,8 +20,7 @@ package filter import ( "github.com/rabbitstack/fibratus/pkg/callstack" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/event" ptypes "github.com/rabbitstack/fibratus/pkg/ps/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -35,7 +34,7 @@ func TestNarrowAccessors(t *testing.T) { expectedAccessors int }{ { - New(`ps.name = 'cmd.exe' and kevt.name = 'CreateProcess' or kevt.name in ('TerminateProcess', 'CreateFile')`, cfg), + New(`ps.name = 'cmd.exe' and evt.name = 'CreateProcess' or evt.name in ('TerminateProcess', 'CreateFile')`, cfg), 2, }, { @@ -43,11 +42,11 @@ func TestNarrowAccessors(t *testing.T) { 1, }, { - New(`handle.type = 'Section' and pe.nsections > 1 and kevt.name = 'CreateHandle'`, cfg), + New(`handle.type = 'Section' and pe.nsections > 1 and evt.name = 'CreateHandle'`, cfg), 3, }, { - New(`sequence |kevt.name = 'CreateProcess'| as e1 |kevt.name = 'CreateFile' and file.name = $e1.ps.exe |`, cfg), + New(`sequence |evt.name = 'CreateProcess'| as e1 |evt.name = 'CreateFile' and file.name = $e1.ps.exe |`, cfg), 3, }, { @@ -78,72 +77,72 @@ func TestNarrowAccessors(t *testing.T) { func TestIsFieldAccessible(t *testing.T) { var tests = []struct { a Accessor - e *kevent.Kevent + e *event.Event isAccessible bool }{ { - newKevtAccessor(), - &kevent.Kevent{Type: ktypes.QueryDNS, Category: ktypes.Net}, + newEventAccessor(), + &event.Event{Type: event.QueryDNS, Category: event.Net}, true, }, { newPSAccessor(nil), - &kevent.Kevent{Type: ktypes.CreateProcess, Category: ktypes.Process}, + &event.Event{Type: event.CreateProcess, Category: event.Process}, true, }, { newPSAccessor(nil), - &kevent.Kevent{PS: &ptypes.PS{}, Type: ktypes.CreateFile, Category: ktypes.File}, + &event.Event{PS: &ptypes.PS{}, Type: event.CreateFile, Category: event.File}, true, }, { newPSAccessor(nil), - &kevent.Kevent{Type: ktypes.SetThreadContext, Category: ktypes.Thread}, + &event.Event{Type: event.SetThreadContext, Category: event.Thread}, false, }, { newThreadAccessor(), - &kevent.Kevent{Type: ktypes.SetThreadContext, Category: ktypes.Thread}, + &event.Event{Type: event.SetThreadContext, Category: event.Thread}, true, }, { newThreadAccessor(), - &kevent.Kevent{Type: ktypes.CreateProcess, Category: ktypes.Process, Callstack: []callstack.Frame{{Addr: 0x7ffb5c1d0396, Offset: 0x61, Symbol: "CreateProcessW", Module: "C:\\WINDOWS\\System32\\KERNELBASE.dll"}}}, + &event.Event{Type: event.CreateProcess, Category: event.Process, Callstack: []callstack.Frame{{Addr: 0x7ffb5c1d0396, Offset: 0x61, Symbol: "CreateProcessW", Module: "C:\\WINDOWS\\System32\\KERNELBASE.dll"}}}, true, }, { newThreadAccessor(), - &kevent.Kevent{Type: ktypes.RegSetValue, Category: ktypes.Registry, Callstack: []callstack.Frame{{Addr: 0x7ffb5c1d0396, Offset: 0x61, Symbol: "CreateProcessW", Module: "C:\\WINDOWS\\System32\\KERNELBASE.dll"}}}, + &event.Event{Type: event.RegSetValue, Category: event.Registry, Callstack: []callstack.Frame{{Addr: 0x7ffb5c1d0396, Offset: 0x61, Symbol: "CreateProcessW", Module: "C:\\WINDOWS\\System32\\KERNELBASE.dll"}}}, true, }, { newRegistryAccessor(), - &kevent.Kevent{Type: ktypes.RegSetValue, Category: ktypes.Registry, Callstack: []callstack.Frame{{Addr: 0x7ffb5c1d0396, Offset: 0x61, Symbol: "CreateProcessW", Module: "C:\\WINDOWS\\System32\\KERNELBASE.dll"}}}, + &event.Event{Type: event.RegSetValue, Category: event.Registry, Callstack: []callstack.Frame{{Addr: 0x7ffb5c1d0396, Offset: 0x61, Symbol: "CreateProcessW", Module: "C:\\WINDOWS\\System32\\KERNELBASE.dll"}}}, true, }, { newNetworkAccessor(), - &kevent.Kevent{Type: ktypes.RegSetValue, Category: ktypes.Registry, Callstack: []callstack.Frame{{Addr: 0x7ffb5c1d0396, Offset: 0x61, Symbol: "CreateProcessW", Module: "C:\\WINDOWS\\System32\\KERNELBASE.dll"}}}, + &event.Event{Type: event.RegSetValue, Category: event.Registry, Callstack: []callstack.Frame{{Addr: 0x7ffb5c1d0396, Offset: 0x61, Symbol: "CreateProcessW", Module: "C:\\WINDOWS\\System32\\KERNELBASE.dll"}}}, false, }, { newNetworkAccessor(), - &kevent.Kevent{Type: ktypes.ConnectTCPv6, Category: ktypes.Net}, + &event.Event{Type: event.ConnectTCPv6, Category: event.Net}, true, }, { newDNSAccessor(), - &kevent.Kevent{Type: ktypes.ReplyDNS, Category: ktypes.Net}, + &event.Event{Type: event.ReplyDNS, Category: event.Net}, true, }, { newImageAccessor(), - &kevent.Kevent{Type: ktypes.LoadImage, Category: ktypes.Image}, + &event.Event{Type: event.LoadImage, Category: event.Image}, true, }, { newMemAccessor(), - &kevent.Kevent{Type: ktypes.VirtualAlloc, Category: ktypes.Mem}, + &event.Event{Type: event.VirtualAlloc, Category: event.Mem}, true, }, } diff --git a/pkg/filter/fields/fields.go b/pkg/filter/fields/fields.go index 717f7525a..56c920a89 100644 --- a/pkg/filter/fields/fields.go +++ b/pkg/filter/fields/fields.go @@ -19,7 +19,7 @@ package fields import ( - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/event/params" "sort" "unicode" ) @@ -28,7 +28,7 @@ import ( type FieldInfo struct { Field Field Desc string - Type kparams.Type + Type params.Type Examples []string Deprecation *Deprecation Argument *Argument @@ -99,5 +99,5 @@ func IsDeprecated(f Field) (bool, *Deprecation) { // IsBoolean determines if the given field has the bool type. func IsBoolean(f Field) bool { - return fields[f].Type == kparams.Bool + return fields[f].Type == params.Bool } diff --git a/pkg/filter/fields/fields_windows.go b/pkg/filter/fields/fields_windows.go index 3cee4fe7a..4970397cb 100644 --- a/pkg/filter/fields/fields_windows.go +++ b/pkg/filter/fields/fields_windows.go @@ -22,7 +22,7 @@ import ( "strings" "unicode" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/event/params" ) // Field represents the type alias for the field @@ -301,51 +301,51 @@ const ( PePsChildFileName Field = "pe.ps.child.file.name" // KevtSeq is the event sequence number - KevtSeq Field = "kevt.seq" + KevtSeq Field = "evt.seq" // KevtPID is the process identifier that generated the event - KevtPID Field = "kevt.pid" + KevtPID Field = "evt.pid" // KevtTID is the thread identifier that generated the event - KevtTID Field = "kevt.tid" + KevtTID Field = "evt.tid" // KevtCPU is the CPU core where the event was generated - KevtCPU Field = "kevt.cpu" + KevtCPU Field = "evt.cpu" // KevtDesc represents the event description - KevtDesc Field = "kevt.desc" + KevtDesc Field = "evt.desc" // KevtHost represents the host where the event was produced - KevtHost Field = "kevt.host" + KevtHost Field = "evt.host" // KevtTime is the event time - KevtTime Field = "kevt.time" + KevtTime Field = "evt.time" // KevtTimeHour is the hour part of the event time - KevtTimeHour Field = "kevt.time.h" + KevtTimeHour Field = "evt.time.h" // KevtTimeMin is the minute part of the event time - KevtTimeMin Field = "kevt.time.m" + KevtTimeMin Field = "evt.time.m" // KevtTimeSec is the second part of the event time - KevtTimeSec Field = "kevt.time.s" + KevtTimeSec Field = "evt.time.s" // KevtTimeNs is the nanosecond part of the event time - KevtTimeNs Field = "kevt.time.ns" + KevtTimeNs Field = "evt.time.ns" // KevtDate is the event date - KevtDate Field = "kevt.date" + KevtDate Field = "evt.date" // KevtDateDay is the day of event date - KevtDateDay Field = "kevt.date.d" + KevtDateDay Field = "evt.date.d" // KevtDateMonth is the month of event date - KevtDateMonth Field = "kevt.date.m" + KevtDateMonth Field = "evt.date.m" // KevtDateYear is the year of event date - KevtDateYear Field = "kevt.date.y" + KevtDateYear Field = "evt.date.y" // KevtDateTz is the time zone of event timestamp - KevtDateTz Field = "kevt.date.tz" + KevtDateTz Field = "evt.date.tz" // KevtDateWeek is the event week number - KevtDateWeek Field = "kevt.date.week" + KevtDateWeek Field = "evt.date.week" // KevtDateWeekday is the event week day - KevtDateWeekday Field = "kevt.date.weekday" + KevtDateWeekday Field = "evt.date.weekday" // KevtName is the event name - KevtName Field = "kevt.name" + KevtName Field = "evt.name" // KevtCategory is the event category - KevtCategory Field = "kevt.category" + KevtCategory Field = "evt.category" // KevtMeta is the event metadata - KevtMeta Field = "kevt.meta" + KevtMeta Field = "evt.meta" // KevtNparams is the number of event parameters - KevtNparams Field = "kevt.nparams" + KevtNparams Field = "evt.nparams" // KevtArg represents the field sequence for generic argument access - KevtArg Field = "kevt.arg" + KevtArg Field = "evt.arg" // HandleID represents the handle identifier within the process address space HandleID Field = "handle.id" @@ -547,7 +547,7 @@ const ( func (f Field) String() string { return string(f) } func (f Field) IsPsField() bool { return strings.HasPrefix(string(f), "ps.") } -func (f Field) IsKevtField() bool { return strings.HasPrefix(string(f), "kevt.") } +func (f Field) IsKevtField() bool { return strings.HasPrefix(string(f), "evt.") } func (f Field) IsThreadField() bool { return strings.HasPrefix(string(f), "thread.") } func (f Field) IsImageField() bool { return strings.HasPrefix(string(f), "image.") } func (f Field) IsFileField() bool { return strings.HasPrefix(string(f), "file.") } @@ -734,28 +734,28 @@ func IsPseudoField(f Field) bool { func (f Field) IsPeSectionsPseudo() bool { return f == PeSections } var fields = map[Field]FieldInfo{ - KevtSeq: {KevtSeq, "event sequence number", kparams.Uint64, []string{"kevt.seq > 666"}, nil, nil}, - KevtPID: {KevtPID, "process identifier generating the kernel event", kparams.Uint32, []string{"kevt.pid = 6"}, nil, nil}, - KevtTID: {KevtTID, "thread identifier generating the kernel event", kparams.Uint32, []string{"kevt.tid = 1024"}, nil, nil}, - KevtCPU: {KevtCPU, "logical processor core where the event was generated", kparams.Uint8, []string{"kevt.cpu = 2"}, nil, nil}, - KevtName: {KevtName, "symbolical kernel event name", kparams.AnsiString, []string{"kevt.name = 'CreateThread'"}, nil, nil}, - KevtCategory: {KevtCategory, "event category", kparams.AnsiString, []string{"kevt.category = 'registry'"}, nil, nil}, - KevtDesc: {KevtDesc, "event description", kparams.AnsiString, []string{"kevt.desc contains 'Creates a new process'"}, nil, nil}, - KevtHost: {KevtHost, "host name on which the event was produced", kparams.UnicodeString, []string{"kevt.host contains 'kitty'"}, nil, nil}, - KevtTime: {KevtTime, "event timestamp as a time string", kparams.Time, []string{"kevt.time = '17:05:32'"}, nil, nil}, - KevtTimeHour: {KevtTimeHour, "hour within the day on which the event occurred", kparams.Time, []string{"kevt.time.h = 23"}, nil, nil}, - KevtTimeMin: {KevtTimeMin, "minute offset within the hour on which the event occurred", kparams.Time, []string{"kevt.time.m = 54"}, nil, nil}, - KevtTimeSec: {KevtTimeSec, "second offset within the minute on which the event occurred", kparams.Time, []string{"kevt.time.s = 0"}, nil, nil}, - KevtTimeNs: {KevtTimeNs, "nanoseconds specified by event timestamp", kparams.Int64, []string{"kevt.time.ns > 1591191629102337000"}, nil, nil}, - KevtDate: {KevtDate, "event timestamp as a date string", kparams.Time, []string{"kevt.date = '2018-03-03'"}, nil, nil}, - KevtDateDay: {KevtDateDay, "day of the month on which the event occurred", kparams.Time, []string{"kevt.date.d = 12"}, nil, nil}, - KevtDateMonth: {KevtDateMonth, "month of the year on which the event occurred", kparams.Time, []string{"kevt.date.m = 11"}, nil, nil}, - KevtDateYear: {KevtDateYear, "year on which the event occurred", kparams.Uint32, []string{"kevt.date.y = 2020"}, nil, nil}, - KevtDateTz: {KevtDateTz, "time zone associated with the event timestamp", kparams.AnsiString, []string{"kevt.date.tz = 'UTC'"}, nil, nil}, - KevtDateWeek: {KevtDateWeek, "week number within the year on which the event occurred", kparams.Uint8, []string{"kevt.date.week = 2"}, nil, nil}, - KevtDateWeekday: {KevtDateWeekday, "week day on which the event occurred", kparams.AnsiString, []string{"kevt.date.weekday = 'Monday'"}, nil, nil}, - KevtNparams: {KevtNparams, "number of parameters", kparams.Int8, []string{"kevt.nparams > 2"}, nil, nil}, - KevtArg: {KevtArg, "event parameter", kparams.Object, []string{"kevt.arg[cmdline] istartswith 'C:\\Windows'"}, nil, &Argument{Optional: false, Pattern: "[a-z0-9_]+", ValidationFunc: func(s string) bool { + KevtSeq: {KevtSeq, "event sequence number", params.Uint64, []string{"evt.seq > 666"}, nil, nil}, + KevtPID: {KevtPID, "process identifier generating the kernel event", params.Uint32, []string{"evt.pid = 6"}, nil, nil}, + KevtTID: {KevtTID, "thread identifier generating the kernel event", params.Uint32, []string{"evt.tid = 1024"}, nil, nil}, + KevtCPU: {KevtCPU, "logical processor core where the event was generated", params.Uint8, []string{"evt.cpu = 2"}, nil, nil}, + KevtName: {KevtName, "symbolical kernel event name", params.AnsiString, []string{"evt.name = 'CreateThread'"}, nil, nil}, + KevtCategory: {KevtCategory, "event category", params.AnsiString, []string{"evt.category = 'registry'"}, nil, nil}, + KevtDesc: {KevtDesc, "event description", params.AnsiString, []string{"evt.desc contains 'Creates a new process'"}, nil, nil}, + KevtHost: {KevtHost, "host name on which the event was produced", params.UnicodeString, []string{"evt.host contains 'kitty'"}, nil, nil}, + KevtTime: {KevtTime, "event timestamp as a time string", params.Time, []string{"evt.time = '17:05:32'"}, nil, nil}, + KevtTimeHour: {KevtTimeHour, "hour within the day on which the event occurred", params.Time, []string{"evt.time.h = 23"}, nil, nil}, + KevtTimeMin: {KevtTimeMin, "minute offset within the hour on which the event occurred", params.Time, []string{"evt.time.m = 54"}, nil, nil}, + KevtTimeSec: {KevtTimeSec, "second offset within the minute on which the event occurred", params.Time, []string{"evt.time.s = 0"}, nil, nil}, + KevtTimeNs: {KevtTimeNs, "nanoseconds specified by event timestamp", params.Int64, []string{"evt.time.ns > 1591191629102337000"}, nil, nil}, + KevtDate: {KevtDate, "event timestamp as a date string", params.Time, []string{"evt.date = '2018-03-03'"}, nil, nil}, + KevtDateDay: {KevtDateDay, "day of the month on which the event occurred", params.Time, []string{"evt.date.d = 12"}, nil, nil}, + KevtDateMonth: {KevtDateMonth, "month of the year on which the event occurred", params.Time, []string{"evt.date.m = 11"}, nil, nil}, + KevtDateYear: {KevtDateYear, "year on which the event occurred", params.Uint32, []string{"evt.date.y = 2020"}, nil, nil}, + KevtDateTz: {KevtDateTz, "time zone associated with the event timestamp", params.AnsiString, []string{"evt.date.tz = 'UTC'"}, nil, nil}, + KevtDateWeek: {KevtDateWeek, "week number within the year on which the event occurred", params.Uint8, []string{"evt.date.week = 2"}, nil, nil}, + KevtDateWeekday: {KevtDateWeekday, "week day on which the event occurred", params.AnsiString, []string{"evt.date.weekday = 'Monday'"}, nil, nil}, + KevtNparams: {KevtNparams, "number of parameters", params.Int8, []string{"evt.nparams > 2"}, nil, nil}, + KevtArg: {KevtArg, "event parameter", params.Object, []string{"evt.arg[cmdline] istartswith 'C:\\Windows'"}, nil, &Argument{Optional: false, Pattern: "[a-z0-9_]+", ValidationFunc: func(s string) bool { for _, c := range s { switch { case unicode.IsLower(c): @@ -768,190 +768,190 @@ var fields = map[Field]FieldInfo{ return true }}}, - PsPid: {PsPid, "process identifier", kparams.PID, []string{"ps.pid = 1024"}, nil, nil}, - PsPpid: {PsPpid, "parent process identifier", kparams.PID, []string{"ps.ppid = 45"}, nil, nil}, - PsName: {PsName, "process image name including the file extension", kparams.UnicodeString, []string{"ps.name contains 'firefox'"}, nil, nil}, - PsComm: {PsComm, "process command line", kparams.UnicodeString, []string{"ps.comm contains 'java'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsCmdline}}, nil}, - PsCmdline: {PsCmdline, "process command line", kparams.UnicodeString, []string{"ps.cmdline contains 'java'"}, nil, nil}, - PsExe: {PsExe, "full name of the process' executable", kparams.UnicodeString, []string{"ps.exe = 'C:\\Windows\\system32\\cmd.exe'"}, nil, nil}, - PsArgs: {PsArgs, "process command line arguments", kparams.Slice, []string{"ps.args in ('/cdir', '/-C')"}, nil, nil}, - PsCwd: {PsCwd, "process current working directory", kparams.UnicodeString, []string{"ps.cwd = 'C:\\Users\\Default'"}, nil, nil}, - PsSID: {PsSID, "security identifier under which this process is run", kparams.UnicodeString, []string{"ps.sid contains 'SYSTEM'"}, nil, nil}, - PsSessionID: {PsSessionID, "unique identifier for the current session", kparams.Int16, []string{"ps.sessionid = 1"}, nil, nil}, - PsDomain: {PsDomain, "process domain", kparams.UnicodeString, []string{"ps.domain contains 'SERVICE'"}, nil, nil}, - PsUsername: {PsUsername, "process username", kparams.UnicodeString, []string{"ps.username contains 'system'"}, nil, nil}, - PsEnvs: {PsEnvs, "process environment variables", kparams.Slice, []string{"ps.envs in ('SystemRoot:C:\\WINDOWS')", "ps.envs[windir] = 'C:\\WINDOWS'"}, nil, &Argument{Optional: true, ValidationFunc: func(arg string) bool { return true }}}, - PsHandleNames: {PsHandleNames, "allocated process handle names", kparams.Slice, []string{"ps.handles in ('\\BaseNamedObjects\\__ComCatalogCache__')"}, nil, nil}, - PsHandleTypes: {PsHandleTypes, "allocated process handle types", kparams.Slice, []string{"ps.handle.types in ('Key', 'Mutant', 'Section')"}, nil, nil}, - PsDTB: {PsDTB, "process directory table base address", kparams.Address, []string{"ps.dtb = '7ffe0000'"}, nil, nil}, - PsModuleNames: {PsModuleNames, "modules loaded by the process", kparams.Slice, []string{"ps.modules in ('crypt32.dll', 'xul.dll')"}, nil, nil}, - PsParentName: {PsParentName, "parent process image name including the file extension", kparams.UnicodeString, []string{"ps.parent.name contains 'cmd.exe'"}, nil, nil}, - PsParentPid: {PsParentPid, "parent process id", kparams.Uint32, []string{"ps.parent.pid = 4"}, nil, nil}, - PsParentComm: {PsParentComm, "parent process command line", kparams.UnicodeString, []string{"ps.parent.comm contains 'java'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsParentCmdline}}, nil}, - PsParentCmdline: {PsParentCmdline, "parent process command line", kparams.UnicodeString, []string{"ps.parent.cmdline contains 'java'"}, nil, nil}, - PsParentExe: {PsParentExe, "full name of the parent process' executable", kparams.UnicodeString, []string{"ps.parent.exe = 'C:\\Windows\\system32\\explorer.exe'"}, nil, nil}, - PsParentArgs: {PsParentArgs, "parent process command line arguments", kparams.Slice, []string{"ps.parent.args in ('/cdir', '/-C')"}, nil, nil}, - PsParentCwd: {PsParentCwd, "parent process current working directory", kparams.UnicodeString, []string{"ps.parent.cwd = 'C:\\Temp'"}, nil, nil}, - PsParentSID: {PsParentSID, "security identifier under which the parent process is run", kparams.UnicodeString, []string{"ps.parent.sid contains 'SYSTEM'"}, nil, nil}, - PsParentDomain: {PsParentDomain, "parent process domain", kparams.UnicodeString, []string{"ps.parent.domain contains 'SERVICE'"}, nil, nil}, - PsParentUsername: {PsParentUsername, "parent process username", kparams.UnicodeString, []string{"ps.parent.username contains 'system'"}, nil, nil}, - PsParentSessionID: {PsParentSessionID, "unique identifier for the current session of parent process", kparams.Int16, []string{"ps.parent.sessionid = 1"}, nil, nil}, - PsParentEnvs: {PsParentEnvs, "parent process environment variables", kparams.Slice, []string{"ps.parent.envs in ('MOZ_CRASHREPORTER_DATA_DIRECTORY')"}, nil, nil}, - PsParentHandles: {PsParentHandles, "allocated parent process handle names", kparams.Slice, []string{"ps.parent.handles in ('\\BaseNamedObjects\\__ComCatalogCache__')"}, nil, nil}, - PsParentHandleTypes: {PsParentHandleTypes, "allocated parent process handle types", kparams.Slice, []string{"ps.parent.handle.types in ('File', 'SymbolicLink')"}, nil, nil}, - PsParentDTB: {PsParentDTB, "parent process directory table base address", kparams.Address, []string{"ps.parent.dtb = '7ffe0000'"}, nil, nil}, - PsAccessMask: {PsAccessMask, "process desired access rights", kparams.AnsiString, []string{"ps.access.mask = '0x1400'"}, nil, nil}, - PsAccessMaskNames: {PsAccessMaskNames, "process desired access rights as a string list", kparams.Slice, []string{"ps.access.mask.names in ('SUSPEND_RESUME')"}, nil, nil}, - PsAccessStatus: {PsAccessStatus, "process access status", kparams.UnicodeString, []string{"ps.access.status = 'access is denied.'"}, nil, nil}, - PsSiblingPid: {PsSiblingPid, "created or terminated process identifier", kparams.PID, []string{"ps.sibling.pid = 320"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildPid}}, nil}, - PsChildPid: {PsChildPid, "created or terminated process identifier", kparams.PID, []string{"ps.child.pid = 320"}, nil, nil}, - PsSiblingName: {PsSiblingName, "created or terminated process name", kparams.UnicodeString, []string{"ps.sibling.name = 'notepad.exe'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildName}}, nil}, - PsChildName: {PsChildName, "created or terminated process name", kparams.UnicodeString, []string{"ps.child.name = 'notepad.exe'"}, nil, nil}, - PsSiblingComm: {PsSiblingComm, "created or terminated process command line", kparams.UnicodeString, []string{"ps.sibling.comm contains '\\k \\v'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildCmdline}}, nil}, - PsChildCmdline: {PsChildCmdline, "created or terminated process command line", kparams.UnicodeString, []string{"ps.child.cmdline contains '\\k \\v'"}, nil, nil}, - PsSiblingArgs: {PsSiblingArgs, "created process command line arguments", kparams.Slice, []string{"ps.sibling.args in ('/cdir', '/-C')"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildArgs}}, nil}, - PsChildArgs: {PsChildArgs, "created process command line arguments", kparams.Slice, []string{"ps.child.args in ('/cdir', '/-C')"}, nil, nil}, - PsSiblingExe: {PsSiblingExe, "created, terminated, or opened process id", kparams.UnicodeString, []string{"ps.sibling.exe contains '\\Windows\\cmd.exe'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildExe}}, nil}, - PsChildExe: {PsChildExe, "created, terminated, or opened process id", kparams.UnicodeString, []string{"ps.child.exe contains '\\Windows\\cmd.exe'"}, nil, nil}, - PsSiblingSID: {PsSiblingSID, "created or terminated process security identifier", kparams.UnicodeString, []string{"ps.sibling.sid contains 'SERVICE'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildSID}}, nil}, - PsChildSID: {PsChildSID, "created or terminated process security identifier", kparams.UnicodeString, []string{"ps.child.sid contains 'SERVICE'"}, nil, nil}, - PsSiblingSessionID: {PsSiblingSessionID, "created or terminated process session identifier", kparams.Int16, []string{"ps.sibling.sessionid == 1"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildSessionID}}, nil}, - PsChildSessionID: {PsChildSessionID, "created or terminated process session identifier", kparams.Int16, []string{"ps.child.sessionid == 1"}, nil, nil}, - PsSiblingDomain: {PsSiblingDomain, "created or terminated process domain", kparams.UnicodeString, []string{"ps.sibling.domain contains 'SERVICE'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildDomain}}, nil}, - PsChildDomain: {PsChildDomain, "created or terminated process domain", kparams.UnicodeString, []string{"ps.child.domain contains 'SERVICE'"}, nil, nil}, - PsSiblingUsername: {PsSiblingUsername, "created or terminated process username", kparams.UnicodeString, []string{"ps.sibling.username contains 'system'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildUsername}}, nil}, - PsChildUsername: {PsChildUsername, "created or terminated process username", kparams.UnicodeString, []string{"ps.child.username contains 'system'"}, nil, nil}, - PsUUID: {PsUUID, "unique process identifier", kparams.Uint64, []string{"ps.uuid > 6000054355"}, nil, nil}, - PsParentUUID: {PsParentUUID, "unique parent process identifier", kparams.Uint64, []string{"ps.parent.uuid > 6000054355"}, nil, nil}, - PsChildUUID: {PsChildUUID, "unique child process identifier", kparams.Uint64, []string{"ps.child.uuid > 6000054355"}, nil, nil}, - PsChildPeFilename: {PsChildPeFilename, "original file name of the child process executable supplied at compile-time", kparams.UnicodeString, []string{"ps.child.pe.file.name = 'NOTEPAD.EXE'"}, nil, nil}, - PsChildIsWOW64Field: {PsChildIsWOW64Field, "indicates if the 32-bit child process is created in 64-bit Windows system", kparams.Bool, []string{"ps.child.is_wow64"}, nil, nil}, - PsChildIsPackagedField: {PsChildIsPackagedField, "indicates if the child process is packaged with the MSIX technology", kparams.Bool, []string{"ps.child.is_packaged"}, nil, nil}, - PsChildIsProtectedField: {PsChildIsProtectedField, "indicates if the child process is a protected process", kparams.Bool, []string{"ps.child.is_protected"}, nil, nil}, - PsIsWOW64Field: {PsIsWOW64Field, "indicates if the process generating the event is a 32-bit process created in 64-bit Windows system", kparams.Bool, []string{"ps.is_wow64"}, nil, nil}, - PsIsPackagedField: {PsIsPackagedField, "indicates if the process generating the event is packaged with the MSIX technology", kparams.Bool, []string{"ps.is_packaged"}, nil, nil}, - PsIsProtectedField: {PsIsProtectedField, "indicates if the process generating the event is a protected process", kparams.Bool, []string{"ps.is_protected"}, nil, nil}, - PsParentIsWOW64Field: {PsParentIsWOW64Field, "indicates if the parent process generating the event is a 32-bit process created in 64-bit Windows system", kparams.Bool, []string{"ps.parent.is_wow64"}, nil, nil}, - PsParentIsPackagedField: {PsParentIsPackagedField, "indicates if the parent process generating the event is packaged with the MSIX technology", kparams.Bool, []string{"ps.parent.is_packaged"}, nil, nil}, - PsParentIsProtectedField: {PsParentIsProtectedField, "indicates if the the parent process generating the event is a protected process", kparams.Bool, []string{"ps.parent.is_protected"}, nil, nil}, - PsAncestor: {PsAncestor, "the process ancestor name", kparams.UnicodeString, []string{"ps.ancestor[1] = 'svchost.exe'", "ps.ancestor in ('winword.exe')"}, nil, &Argument{Optional: true, Pattern: "[0-9]+", ValidationFunc: isNumber}}, + PsPid: {PsPid, "process identifier", params.PID, []string{"ps.pid = 1024"}, nil, nil}, + PsPpid: {PsPpid, "parent process identifier", params.PID, []string{"ps.ppid = 45"}, nil, nil}, + PsName: {PsName, "process image name including the file extension", params.UnicodeString, []string{"ps.name contains 'firefox'"}, nil, nil}, + PsComm: {PsComm, "process command line", params.UnicodeString, []string{"ps.comm contains 'java'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsCmdline}}, nil}, + PsCmdline: {PsCmdline, "process command line", params.UnicodeString, []string{"ps.cmdline contains 'java'"}, nil, nil}, + PsExe: {PsExe, "full name of the process' executable", params.UnicodeString, []string{"ps.exe = 'C:\\Windows\\system32\\cmd.exe'"}, nil, nil}, + PsArgs: {PsArgs, "process command line arguments", params.Slice, []string{"ps.args in ('/cdir', '/-C')"}, nil, nil}, + PsCwd: {PsCwd, "process current working directory", params.UnicodeString, []string{"ps.cwd = 'C:\\Users\\Default'"}, nil, nil}, + PsSID: {PsSID, "security identifier under which this process is run", params.UnicodeString, []string{"ps.sid contains 'SYSTEM'"}, nil, nil}, + PsSessionID: {PsSessionID, "unique identifier for the current session", params.Int16, []string{"ps.sessionid = 1"}, nil, nil}, + PsDomain: {PsDomain, "process domain", params.UnicodeString, []string{"ps.domain contains 'SERVICE'"}, nil, nil}, + PsUsername: {PsUsername, "process username", params.UnicodeString, []string{"ps.username contains 'system'"}, nil, nil}, + PsEnvs: {PsEnvs, "process environment variables", params.Slice, []string{"ps.envs in ('SystemRoot:C:\\WINDOWS')", "ps.envs[windir] = 'C:\\WINDOWS'"}, nil, &Argument{Optional: true, ValidationFunc: func(arg string) bool { return true }}}, + PsHandleNames: {PsHandleNames, "allocated process handle names", params.Slice, []string{"ps.handles in ('\\BaseNamedObjects\\__ComCatalogCache__')"}, nil, nil}, + PsHandleTypes: {PsHandleTypes, "allocated process handle types", params.Slice, []string{"ps.handle.types in ('Key', 'Mutant', 'Section')"}, nil, nil}, + PsDTB: {PsDTB, "process directory table base address", params.Address, []string{"ps.dtb = '7ffe0000'"}, nil, nil}, + PsModuleNames: {PsModuleNames, "modules loaded by the process", params.Slice, []string{"ps.modules in ('crypt32.dll', 'xul.dll')"}, nil, nil}, + PsParentName: {PsParentName, "parent process image name including the file extension", params.UnicodeString, []string{"ps.parent.name contains 'cmd.exe'"}, nil, nil}, + PsParentPid: {PsParentPid, "parent process id", params.Uint32, []string{"ps.parent.pid = 4"}, nil, nil}, + PsParentComm: {PsParentComm, "parent process command line", params.UnicodeString, []string{"ps.parent.comm contains 'java'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsParentCmdline}}, nil}, + PsParentCmdline: {PsParentCmdline, "parent process command line", params.UnicodeString, []string{"ps.parent.cmdline contains 'java'"}, nil, nil}, + PsParentExe: {PsParentExe, "full name of the parent process' executable", params.UnicodeString, []string{"ps.parent.exe = 'C:\\Windows\\system32\\explorer.exe'"}, nil, nil}, + PsParentArgs: {PsParentArgs, "parent process command line arguments", params.Slice, []string{"ps.parent.args in ('/cdir', '/-C')"}, nil, nil}, + PsParentCwd: {PsParentCwd, "parent process current working directory", params.UnicodeString, []string{"ps.parent.cwd = 'C:\\Temp'"}, nil, nil}, + PsParentSID: {PsParentSID, "security identifier under which the parent process is run", params.UnicodeString, []string{"ps.parent.sid contains 'SYSTEM'"}, nil, nil}, + PsParentDomain: {PsParentDomain, "parent process domain", params.UnicodeString, []string{"ps.parent.domain contains 'SERVICE'"}, nil, nil}, + PsParentUsername: {PsParentUsername, "parent process username", params.UnicodeString, []string{"ps.parent.username contains 'system'"}, nil, nil}, + PsParentSessionID: {PsParentSessionID, "unique identifier for the current session of parent process", params.Int16, []string{"ps.parent.sessionid = 1"}, nil, nil}, + PsParentEnvs: {PsParentEnvs, "parent process environment variables", params.Slice, []string{"ps.parent.envs in ('MOZ_CRASHREPORTER_DATA_DIRECTORY')"}, nil, nil}, + PsParentHandles: {PsParentHandles, "allocated parent process handle names", params.Slice, []string{"ps.parent.handles in ('\\BaseNamedObjects\\__ComCatalogCache__')"}, nil, nil}, + PsParentHandleTypes: {PsParentHandleTypes, "allocated parent process handle types", params.Slice, []string{"ps.parent.handle.types in ('File', 'SymbolicLink')"}, nil, nil}, + PsParentDTB: {PsParentDTB, "parent process directory table base address", params.Address, []string{"ps.parent.dtb = '7ffe0000'"}, nil, nil}, + PsAccessMask: {PsAccessMask, "process desired access rights", params.AnsiString, []string{"ps.access.mask = '0x1400'"}, nil, nil}, + PsAccessMaskNames: {PsAccessMaskNames, "process desired access rights as a string list", params.Slice, []string{"ps.access.mask.names in ('SUSPEND_RESUME')"}, nil, nil}, + PsAccessStatus: {PsAccessStatus, "process access status", params.UnicodeString, []string{"ps.access.status = 'access is denied.'"}, nil, nil}, + PsSiblingPid: {PsSiblingPid, "created or terminated process identifier", params.PID, []string{"ps.sibling.pid = 320"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildPid}}, nil}, + PsChildPid: {PsChildPid, "created or terminated process identifier", params.PID, []string{"ps.child.pid = 320"}, nil, nil}, + PsSiblingName: {PsSiblingName, "created or terminated process name", params.UnicodeString, []string{"ps.sibling.name = 'notepad.exe'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildName}}, nil}, + PsChildName: {PsChildName, "created or terminated process name", params.UnicodeString, []string{"ps.child.name = 'notepad.exe'"}, nil, nil}, + PsSiblingComm: {PsSiblingComm, "created or terminated process command line", params.UnicodeString, []string{"ps.sibling.comm contains '\\k \\v'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildCmdline}}, nil}, + PsChildCmdline: {PsChildCmdline, "created or terminated process command line", params.UnicodeString, []string{"ps.child.cmdline contains '\\k \\v'"}, nil, nil}, + PsSiblingArgs: {PsSiblingArgs, "created process command line arguments", params.Slice, []string{"ps.sibling.args in ('/cdir', '/-C')"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildArgs}}, nil}, + PsChildArgs: {PsChildArgs, "created process command line arguments", params.Slice, []string{"ps.child.args in ('/cdir', '/-C')"}, nil, nil}, + PsSiblingExe: {PsSiblingExe, "created, terminated, or opened process id", params.UnicodeString, []string{"ps.sibling.exe contains '\\Windows\\cmd.exe'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildExe}}, nil}, + PsChildExe: {PsChildExe, "created, terminated, or opened process id", params.UnicodeString, []string{"ps.child.exe contains '\\Windows\\cmd.exe'"}, nil, nil}, + PsSiblingSID: {PsSiblingSID, "created or terminated process security identifier", params.UnicodeString, []string{"ps.sibling.sid contains 'SERVICE'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildSID}}, nil}, + PsChildSID: {PsChildSID, "created or terminated process security identifier", params.UnicodeString, []string{"ps.child.sid contains 'SERVICE'"}, nil, nil}, + PsSiblingSessionID: {PsSiblingSessionID, "created or terminated process session identifier", params.Int16, []string{"ps.sibling.sessionid == 1"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildSessionID}}, nil}, + PsChildSessionID: {PsChildSessionID, "created or terminated process session identifier", params.Int16, []string{"ps.child.sessionid == 1"}, nil, nil}, + PsSiblingDomain: {PsSiblingDomain, "created or terminated process domain", params.UnicodeString, []string{"ps.sibling.domain contains 'SERVICE'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildDomain}}, nil}, + PsChildDomain: {PsChildDomain, "created or terminated process domain", params.UnicodeString, []string{"ps.child.domain contains 'SERVICE'"}, nil, nil}, + PsSiblingUsername: {PsSiblingUsername, "created or terminated process username", params.UnicodeString, []string{"ps.sibling.username contains 'system'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildUsername}}, nil}, + PsChildUsername: {PsChildUsername, "created or terminated process username", params.UnicodeString, []string{"ps.child.username contains 'system'"}, nil, nil}, + PsUUID: {PsUUID, "unique process identifier", params.Uint64, []string{"ps.uuid > 6000054355"}, nil, nil}, + PsParentUUID: {PsParentUUID, "unique parent process identifier", params.Uint64, []string{"ps.parent.uuid > 6000054355"}, nil, nil}, + PsChildUUID: {PsChildUUID, "unique child process identifier", params.Uint64, []string{"ps.child.uuid > 6000054355"}, nil, nil}, + PsChildPeFilename: {PsChildPeFilename, "original file name of the child process executable supplied at compile-time", params.UnicodeString, []string{"ps.child.pe.file.name = 'NOTEPAD.EXE'"}, nil, nil}, + PsChildIsWOW64Field: {PsChildIsWOW64Field, "indicates if the 32-bit child process is created in 64-bit Windows system", params.Bool, []string{"ps.child.is_wow64"}, nil, nil}, + PsChildIsPackagedField: {PsChildIsPackagedField, "indicates if the child process is packaged with the MSIX technology", params.Bool, []string{"ps.child.is_packaged"}, nil, nil}, + PsChildIsProtectedField: {PsChildIsProtectedField, "indicates if the child process is a protected process", params.Bool, []string{"ps.child.is_protected"}, nil, nil}, + PsIsWOW64Field: {PsIsWOW64Field, "indicates if the process generating the event is a 32-bit process created in 64-bit Windows system", params.Bool, []string{"ps.is_wow64"}, nil, nil}, + PsIsPackagedField: {PsIsPackagedField, "indicates if the process generating the event is packaged with the MSIX technology", params.Bool, []string{"ps.is_packaged"}, nil, nil}, + PsIsProtectedField: {PsIsProtectedField, "indicates if the process generating the event is a protected process", params.Bool, []string{"ps.is_protected"}, nil, nil}, + PsParentIsWOW64Field: {PsParentIsWOW64Field, "indicates if the parent process generating the event is a 32-bit process created in 64-bit Windows system", params.Bool, []string{"ps.parent.is_wow64"}, nil, nil}, + PsParentIsPackagedField: {PsParentIsPackagedField, "indicates if the parent process generating the event is packaged with the MSIX technology", params.Bool, []string{"ps.parent.is_packaged"}, nil, nil}, + PsParentIsProtectedField: {PsParentIsProtectedField, "indicates if the the parent process generating the event is a protected process", params.Bool, []string{"ps.parent.is_protected"}, nil, nil}, + PsAncestor: {PsAncestor, "the process ancestor name", params.UnicodeString, []string{"ps.ancestor[1] = 'svchost.exe'", "ps.ancestor in ('winword.exe')"}, nil, &Argument{Optional: true, Pattern: "[0-9]+", ValidationFunc: isNumber}}, - ThreadBasePrio: {ThreadBasePrio, "scheduler priority of the thread", kparams.Int8, []string{"thread.prio = 5"}, nil, nil}, - ThreadIOPrio: {ThreadIOPrio, "I/O priority hint for scheduling I/O operations", kparams.Int8, []string{"thread.io.prio = 4"}, nil, nil}, - ThreadPagePrio: {ThreadPagePrio, "memory page priority hint for memory pages accessed by the thread", kparams.Int8, []string{"thread.page.prio = 12"}, nil, nil}, - ThreadKstackBase: {ThreadKstackBase, "base address of the thread's kernel space stack", kparams.Address, []string{"thread.kstack.base = 'a65d800000'"}, nil, nil}, - ThreadKstackLimit: {ThreadKstackLimit, "limit of the thread's kernel space stack", kparams.Address, []string{"thread.kstack.limit = 'a85d800000'"}, nil, nil}, - ThreadUstackBase: {ThreadUstackBase, "base address of the thread's user space stack", kparams.Address, []string{"thread.ustack.base = '7ffe0000'"}, nil, nil}, - ThreadUstackLimit: {ThreadUstackLimit, "limit of the thread's user space stack", kparams.Address, []string{"thread.ustack.limit = '8ffe0000'"}, nil, nil}, - ThreadEntrypoint: {ThreadEntrypoint, "starting address of the function to be executed by the thread", kparams.Address, []string{"thread.entrypoint = '7efe0000'"}, &Deprecation{Since: "2.3.0", Fields: []Field{ThreadStartAddress}}, nil}, - ThreadStartAddress: {ThreadStartAddress, "thread start address", kparams.Address, []string{"thread.start_address = '7efe0000'"}, nil, nil}, - ThreadStartAddressSymbol: {ThreadStartAddressSymbol, "thread start address symbol", kparams.UnicodeString, []string{"thread.start_address.symbol = 'LoadImage'"}, nil, nil}, - ThreadStartAddressModule: {ThreadStartAddressModule, "thread start address module", kparams.UnicodeString, []string{"thread.start_address.module endswith 'kernel32.dll'"}, nil, nil}, - ThreadPID: {ThreadPID, "the process identifier where the thread is created", kparams.Uint32, []string{"kevt.pid != thread.pid"}, nil, nil}, - ThreadTEB: {ThreadTEB, "the base address of the thread environment block", kparams.Address, []string{"thread.teb_address = '8f30893000'"}, nil, nil}, - ThreadAccessMask: {ThreadAccessMask, "thread desired access rights", kparams.AnsiString, []string{"thread.access.mask = '0x1fffff'"}, nil, nil}, - ThreadAccessMaskNames: {ThreadAccessMaskNames, "thread desired access rights as a string list", kparams.Slice, []string{"thread.access.mask.names in ('IMPERSONATE')"}, nil, nil}, - ThreadAccessStatus: {ThreadAccessStatus, "thread access status", kparams.UnicodeString, []string{"thread.access.status = 'success'"}, nil, nil}, - ThreadCallstackSummary: {ThreadCallstackSummary, "callstack summary", kparams.UnicodeString, []string{"thread.callstack.summary contains 'ntdll.dll|KERNELBASE.dll'"}, nil, nil}, - ThreadCallstackDetail: {ThreadCallstackDetail, "detailed information of each stack frame", kparams.UnicodeString, []string{"thread.callstack.detail contains 'KERNELBASE.dll!CreateProcessW'"}, nil, nil}, - ThreadCallstackModules: {ThreadCallstackModules, "list of modules comprising the callstack", kparams.Slice, []string{"thread.callstack.modules in ('C:\\WINDOWS\\System32\\KERNELBASE.dll')", "base(thread.callstack.modules[7]) = 'ntdll.dll'"}, nil, &Argument{Optional: true, Pattern: "[0-9]+", ValidationFunc: isNumber}}, - ThreadCallstackSymbols: {ThreadCallstackSymbols, "list of symbols comprising the callstack", kparams.Slice, []string{"thread.callstack.symbols in ('ntdll.dll!NtCreateProcess')", "thread.callstack.symbols[3] = 'ntdll!NtCreateProcess'"}, nil, &Argument{Optional: true, Pattern: "[0-9]+", ValidationFunc: isNumber}}, - ThreadCallstackAllocationSizes: {ThreadCallstackAllocationSizes, "allocation sizes of private pages", kparams.Slice, []string{"thread.callstack.allocation_sizes > 10000"}, nil, nil}, - ThreadCallstackProtections: {ThreadCallstackProtections, "page protections masks of each frame", kparams.Slice, []string{"thread.callstack.protections in ('RWX', 'WX')"}, nil, nil}, - ThreadCallstackCallsiteLeadingAssembly: {ThreadCallstackCallsiteLeadingAssembly, "callsite leading assembly instructions", kparams.Slice, []string{"thread.callstack.callsite_leading_assembly in ('mov r10,rcx', 'syscall')"}, nil, nil}, - ThreadCallstackCallsiteTrailingAssembly: {ThreadCallstackCallsiteTrailingAssembly, "callsite trailing assembly instructions", kparams.Slice, []string{"thread.callstack.callsite_trailing_assembly in ('add esp, 0xab')"}, nil, nil}, - ThreadCallstackIsUnbacked: {ThreadCallstackIsUnbacked, "indicates if the callstack contains unbacked regions", kparams.Bool, []string{"thread.callstack.is_unbacked"}, nil, nil}, - ThreadCallstackAddresses: {ThreadCallstackAddresses, "list of all stack return addresses", kparams.Slice, []string{"thread.callstack.addresses in ('7ffb5c1d0396')"}, nil, nil}, - ThreadCallstackFinalUserModuleName: {ThreadCallstackFinalUserModuleName, "final user space stack frame module name", kparams.UnicodeString, []string{"thread.callstack.final_user_module.name != 'ntdll.dll'"}, nil, nil}, - ThreadCallstackFinalUserModulePath: {ThreadCallstackFinalUserModulePath, "final user space stack frame module path", kparams.UnicodeString, []string{"thread.callstack.final_user_module.path imatches '?:\\Windows\\System32\\ntdll.dll'"}, nil, nil}, - ThreadCallstackFinalUserSymbolName: {ThreadCallstackFinalUserSymbolName, "final user space stack symbol name", kparams.UnicodeString, []string{"thread.callstack.final_user_symbol.name imatches 'CreateProcess*'"}, nil, nil}, - ThreadCallstackFinalKernelModuleName: {ThreadCallstackFinalKernelModuleName, "final kernel space stack frame module name", kparams.UnicodeString, []string{"thread.callstack.final_kernel_module.name = 'FLTMGR.SYS'"}, nil, nil}, - ThreadCallstackFinalKernelModulePath: {ThreadCallstackFinalKernelModulePath, "final kernel space stack frame module path", kparams.UnicodeString, []string{"thread.callstack.final_kernel_module.path imatches '?:\\WINDOWS\\System32\\drivers\\FLTMGR.SYS'"}, nil, nil}, - ThreadCallstackFinalKernelSymbolName: {ThreadCallstackFinalKernelSymbolName, "final kernel space stack symbol name", kparams.UnicodeString, []string{"thread.callstack.final_kernel_symbol.name = 'FltGetStreamContext'"}, nil, nil}, - ThreadCallstackFinalUserModuleSignatureIsSigned: {ThreadCallstackFinalUserModuleSignatureIsSigned, "signature status of the final user space stack frame module", kparams.Bool, []string{"thread.callstack.final_user_module.signature.is_signed = true"}, nil, nil}, - ThreadCallstackFinalUserModuleSignatureIsTrusted: {ThreadCallstackFinalUserModuleSignatureIsTrusted, "signature trust status of the final user space stack frame module", kparams.Bool, []string{"thread.callstack.final_user_module.signature.is_trusted = true"}, nil, nil}, - ThreadCallstackFinalUserModuleSignatureCertIssuer: {ThreadCallstackFinalUserModuleSignatureCertIssuer, "final user space stack frame module signature certificate issuer", kparams.UnicodeString, []string{"thread.callstack.final_user_module.signature.cert.issuer imatches '*Microsoft Corporation*'"}, nil, nil}, - ThreadCallstackFinalUserModuleSignatureCertSubject: {ThreadCallstackFinalUserModuleSignatureCertSubject, "final user space stack frame module signature certificate subject", kparams.UnicodeString, []string{"thread.callstack.final_user_module.signature.cert.subject imatches '*Microsoft Windows*'"}, nil, nil}, + ThreadBasePrio: {ThreadBasePrio, "scheduler priority of the thread", params.Int8, []string{"thread.prio = 5"}, nil, nil}, + ThreadIOPrio: {ThreadIOPrio, "I/O priority hint for scheduling I/O operations", params.Int8, []string{"thread.io.prio = 4"}, nil, nil}, + ThreadPagePrio: {ThreadPagePrio, "memory page priority hint for memory pages accessed by the thread", params.Int8, []string{"thread.page.prio = 12"}, nil, nil}, + ThreadKstackBase: {ThreadKstackBase, "base address of the thread's kernel space stack", params.Address, []string{"thread.kstack.base = 'a65d800000'"}, nil, nil}, + ThreadKstackLimit: {ThreadKstackLimit, "limit of the thread's kernel space stack", params.Address, []string{"thread.kstack.limit = 'a85d800000'"}, nil, nil}, + ThreadUstackBase: {ThreadUstackBase, "base address of the thread's user space stack", params.Address, []string{"thread.ustack.base = '7ffe0000'"}, nil, nil}, + ThreadUstackLimit: {ThreadUstackLimit, "limit of the thread's user space stack", params.Address, []string{"thread.ustack.limit = '8ffe0000'"}, nil, nil}, + ThreadEntrypoint: {ThreadEntrypoint, "starting address of the function to be executed by the thread", params.Address, []string{"thread.entrypoint = '7efe0000'"}, &Deprecation{Since: "2.3.0", Fields: []Field{ThreadStartAddress}}, nil}, + ThreadStartAddress: {ThreadStartAddress, "thread start address", params.Address, []string{"thread.start_address = '7efe0000'"}, nil, nil}, + ThreadStartAddressSymbol: {ThreadStartAddressSymbol, "thread start address symbol", params.UnicodeString, []string{"thread.start_address.symbol = 'LoadImage'"}, nil, nil}, + ThreadStartAddressModule: {ThreadStartAddressModule, "thread start address module", params.UnicodeString, []string{"thread.start_address.module endswith 'kernel32.dll'"}, nil, nil}, + ThreadPID: {ThreadPID, "the process identifier where the thread is created", params.Uint32, []string{"evt.pid != thread.pid"}, nil, nil}, + ThreadTEB: {ThreadTEB, "the base address of the thread environment block", params.Address, []string{"thread.teb_address = '8f30893000'"}, nil, nil}, + ThreadAccessMask: {ThreadAccessMask, "thread desired access rights", params.AnsiString, []string{"thread.access.mask = '0x1fffff'"}, nil, nil}, + ThreadAccessMaskNames: {ThreadAccessMaskNames, "thread desired access rights as a string list", params.Slice, []string{"thread.access.mask.names in ('IMPERSONATE')"}, nil, nil}, + ThreadAccessStatus: {ThreadAccessStatus, "thread access status", params.UnicodeString, []string{"thread.access.status = 'success'"}, nil, nil}, + ThreadCallstackSummary: {ThreadCallstackSummary, "callstack summary", params.UnicodeString, []string{"thread.callstack.summary contains 'ntdll.dll|KERNELBASE.dll'"}, nil, nil}, + ThreadCallstackDetail: {ThreadCallstackDetail, "detailed information of each stack frame", params.UnicodeString, []string{"thread.callstack.detail contains 'KERNELBASE.dll!CreateProcessW'"}, nil, nil}, + ThreadCallstackModules: {ThreadCallstackModules, "list of modules comprising the callstack", params.Slice, []string{"thread.callstack.modules in ('C:\\WINDOWS\\System32\\KERNELBASE.dll')", "base(thread.callstack.modules[7]) = 'ntdll.dll'"}, nil, &Argument{Optional: true, Pattern: "[0-9]+", ValidationFunc: isNumber}}, + ThreadCallstackSymbols: {ThreadCallstackSymbols, "list of symbols comprising the callstack", params.Slice, []string{"thread.callstack.symbols in ('ntdll.dll!NtCreateProcess')", "thread.callstack.symbols[3] = 'ntdll!NtCreateProcess'"}, nil, &Argument{Optional: true, Pattern: "[0-9]+", ValidationFunc: isNumber}}, + ThreadCallstackAllocationSizes: {ThreadCallstackAllocationSizes, "allocation sizes of private pages", params.Slice, []string{"thread.callstack.allocation_sizes > 10000"}, nil, nil}, + ThreadCallstackProtections: {ThreadCallstackProtections, "page protections masks of each frame", params.Slice, []string{"thread.callstack.protections in ('RWX', 'WX')"}, nil, nil}, + ThreadCallstackCallsiteLeadingAssembly: {ThreadCallstackCallsiteLeadingAssembly, "callsite leading assembly instructions", params.Slice, []string{"thread.callstack.callsite_leading_assembly in ('mov r10,rcx', 'syscall')"}, nil, nil}, + ThreadCallstackCallsiteTrailingAssembly: {ThreadCallstackCallsiteTrailingAssembly, "callsite trailing assembly instructions", params.Slice, []string{"thread.callstack.callsite_trailing_assembly in ('add esp, 0xab')"}, nil, nil}, + ThreadCallstackIsUnbacked: {ThreadCallstackIsUnbacked, "indicates if the callstack contains unbacked regions", params.Bool, []string{"thread.callstack.is_unbacked"}, nil, nil}, + ThreadCallstackAddresses: {ThreadCallstackAddresses, "list of all stack return addresses", params.Slice, []string{"thread.callstack.addresses in ('7ffb5c1d0396')"}, nil, nil}, + ThreadCallstackFinalUserModuleName: {ThreadCallstackFinalUserModuleName, "final user space stack frame module name", params.UnicodeString, []string{"thread.callstack.final_user_module.name != 'ntdll.dll'"}, nil, nil}, + ThreadCallstackFinalUserModulePath: {ThreadCallstackFinalUserModulePath, "final user space stack frame module path", params.UnicodeString, []string{"thread.callstack.final_user_module.path imatches '?:\\Windows\\System32\\ntdll.dll'"}, nil, nil}, + ThreadCallstackFinalUserSymbolName: {ThreadCallstackFinalUserSymbolName, "final user space stack symbol name", params.UnicodeString, []string{"thread.callstack.final_user_symbol.name imatches 'CreateProcess*'"}, nil, nil}, + ThreadCallstackFinalKernelModuleName: {ThreadCallstackFinalKernelModuleName, "final kernel space stack frame module name", params.UnicodeString, []string{"thread.callstack.final_kernel_module.name = 'FLTMGR.SYS'"}, nil, nil}, + ThreadCallstackFinalKernelModulePath: {ThreadCallstackFinalKernelModulePath, "final kernel space stack frame module path", params.UnicodeString, []string{"thread.callstack.final_kernel_module.path imatches '?:\\WINDOWS\\System32\\drivers\\FLTMGR.SYS'"}, nil, nil}, + ThreadCallstackFinalKernelSymbolName: {ThreadCallstackFinalKernelSymbolName, "final kernel space stack symbol name", params.UnicodeString, []string{"thread.callstack.final_kernel_symbol.name = 'FltGetStreamContext'"}, nil, nil}, + ThreadCallstackFinalUserModuleSignatureIsSigned: {ThreadCallstackFinalUserModuleSignatureIsSigned, "signature status of the final user space stack frame module", params.Bool, []string{"thread.callstack.final_user_module.signature.is_signed = true"}, nil, nil}, + ThreadCallstackFinalUserModuleSignatureIsTrusted: {ThreadCallstackFinalUserModuleSignatureIsTrusted, "signature trust status of the final user space stack frame module", params.Bool, []string{"thread.callstack.final_user_module.signature.is_trusted = true"}, nil, nil}, + ThreadCallstackFinalUserModuleSignatureCertIssuer: {ThreadCallstackFinalUserModuleSignatureCertIssuer, "final user space stack frame module signature certificate issuer", params.UnicodeString, []string{"thread.callstack.final_user_module.signature.cert.issuer imatches '*Microsoft Corporation*'"}, nil, nil}, + ThreadCallstackFinalUserModuleSignatureCertSubject: {ThreadCallstackFinalUserModuleSignatureCertSubject, "final user space stack frame module signature certificate subject", params.UnicodeString, []string{"thread.callstack.final_user_module.signature.cert.subject imatches '*Microsoft Windows*'"}, nil, nil}, - ImagePath: {ImagePath, "full image path", kparams.UnicodeString, []string{"image.patj = 'C:\\Windows\\System32\\advapi32.dll'"}, nil, nil}, - ImageName: {ImageName, "image name", kparams.UnicodeString, []string{"image.name = 'advapi32.dll'"}, nil, nil}, - ImageBase: {ImageBase, "the base address of process in which the image is loaded", kparams.Address, []string{"image.base.address = 'a65d800000'"}, nil, nil}, - ImageChecksum: {ImageChecksum, "image checksum", kparams.Uint32, []string{"image.checksum = 746424"}, nil, nil}, - ImageSize: {ImageSize, "image size", kparams.Uint32, []string{"image.size > 1024"}, nil, nil}, - ImageDefaultAddress: {ImageDefaultAddress, "default image address", kparams.Address, []string{"image.default.address = '7efe0000'"}, nil, nil}, - ImagePID: {ImagePID, "target process identifier", kparams.Uint32, []string{"image.pid = 80"}, nil, nil}, - ImageSignatureType: {ImageSignatureType, "image signature type", kparams.AnsiString, []string{"image.signature.type != 'NONE'"}, nil, nil}, - ImageSignatureLevel: {ImageSignatureLevel, "image signature level", kparams.AnsiString, []string{"image.signature.level = 'AUTHENTICODE'"}, nil, nil}, - ImageCertSerial: {ImageCertSerial, "image certificate serial number", kparams.UnicodeString, []string{"image.cert.serial = '330000023241fb59996dcc4dff000000000232'"}, nil, nil}, - ImageCertSubject: {ImageCertSubject, "image certificate subject", kparams.UnicodeString, []string{"image.cert.subject contains 'Washington, Redmond, Microsoft Corporation'"}, nil, nil}, - ImageCertIssuer: {ImageCertIssuer, "image certificate CA", kparams.UnicodeString, []string{"image.cert.issuer contains 'Washington, Redmond, Microsoft Corporation'"}, nil, nil}, - ImageCertAfter: {ImageCertAfter, "image certificate expiration date", kparams.Time, []string{"image.cert.after contains '2024-02-01 00:05:42 +0000 UTC'"}, nil, nil}, - ImageCertBefore: {ImageCertBefore, "image certificate enrollment date", kparams.Time, []string{"image.cert.before contains '2024-02-01 00:05:42 +0000 UTC'"}, nil, nil}, - ImageIsDriverMalicious: {ImageIsDriverMalicious, "indicates if the loaded driver is malicious", kparams.Bool, []string{"image.is_driver_malicious"}, nil, nil}, - ImageIsDriverVulnerable: {ImageIsDriverVulnerable, "indicates if the loaded driver is vulnerable", kparams.Bool, []string{"image.is_driver_vulnerable"}, nil, nil}, - ImageIsDLL: {ImageIsDLL, "indicates if the loaded image is a DLL", kparams.Bool, []string{"image.is_dll'"}, nil, nil}, - ImageIsDriver: {ImageIsDriver, "indicates if the loaded image is a driver", kparams.Bool, []string{"image.is_driver'"}, nil, nil}, - ImageIsExecutable: {ImageIsExecutable, "indicates if the loaded image is an executable", kparams.Bool, []string{"image.is_exec'"}, nil, nil}, - ImageIsDotnet: {ImageIsDotnet, "indicates if the loaded image is a .NET assembly", kparams.Bool, []string{"image.is_dotnet'"}, nil, nil}, + ImagePath: {ImagePath, "full image path", params.UnicodeString, []string{"image.patj = 'C:\\Windows\\System32\\advapi32.dll'"}, nil, nil}, + ImageName: {ImageName, "image name", params.UnicodeString, []string{"image.name = 'advapi32.dll'"}, nil, nil}, + ImageBase: {ImageBase, "the base address of process in which the image is loaded", params.Address, []string{"image.base.address = 'a65d800000'"}, nil, nil}, + ImageChecksum: {ImageChecksum, "image checksum", params.Uint32, []string{"image.checksum = 746424"}, nil, nil}, + ImageSize: {ImageSize, "image size", params.Uint32, []string{"image.size > 1024"}, nil, nil}, + ImageDefaultAddress: {ImageDefaultAddress, "default image address", params.Address, []string{"image.default.address = '7efe0000'"}, nil, nil}, + ImagePID: {ImagePID, "target process identifier", params.Uint32, []string{"image.pid = 80"}, nil, nil}, + ImageSignatureType: {ImageSignatureType, "image signature type", params.AnsiString, []string{"image.signature.type != 'NONE'"}, nil, nil}, + ImageSignatureLevel: {ImageSignatureLevel, "image signature level", params.AnsiString, []string{"image.signature.level = 'AUTHENTICODE'"}, nil, nil}, + ImageCertSerial: {ImageCertSerial, "image certificate serial number", params.UnicodeString, []string{"image.cert.serial = '330000023241fb59996dcc4dff000000000232'"}, nil, nil}, + ImageCertSubject: {ImageCertSubject, "image certificate subject", params.UnicodeString, []string{"image.cert.subject contains 'Washington, Redmond, Microsoft Corporation'"}, nil, nil}, + ImageCertIssuer: {ImageCertIssuer, "image certificate CA", params.UnicodeString, []string{"image.cert.issuer contains 'Washington, Redmond, Microsoft Corporation'"}, nil, nil}, + ImageCertAfter: {ImageCertAfter, "image certificate expiration date", params.Time, []string{"image.cert.after contains '2024-02-01 00:05:42 +0000 UTC'"}, nil, nil}, + ImageCertBefore: {ImageCertBefore, "image certificate enrollment date", params.Time, []string{"image.cert.before contains '2024-02-01 00:05:42 +0000 UTC'"}, nil, nil}, + ImageIsDriverMalicious: {ImageIsDriverMalicious, "indicates if the loaded driver is malicious", params.Bool, []string{"image.is_driver_malicious"}, nil, nil}, + ImageIsDriverVulnerable: {ImageIsDriverVulnerable, "indicates if the loaded driver is vulnerable", params.Bool, []string{"image.is_driver_vulnerable"}, nil, nil}, + ImageIsDLL: {ImageIsDLL, "indicates if the loaded image is a DLL", params.Bool, []string{"image.is_dll'"}, nil, nil}, + ImageIsDriver: {ImageIsDriver, "indicates if the loaded image is a driver", params.Bool, []string{"image.is_driver'"}, nil, nil}, + ImageIsExecutable: {ImageIsExecutable, "indicates if the loaded image is an executable", params.Bool, []string{"image.is_exec'"}, nil, nil}, + ImageIsDotnet: {ImageIsDotnet, "indicates if the loaded image is a .NET assembly", params.Bool, []string{"image.is_dotnet'"}, nil, nil}, - FileObject: {FileObject, "file object address", kparams.Uint64, []string{"file.object = 18446738026482168384"}, nil, nil}, - FilePath: {FilePath, "full file path", kparams.UnicodeString, []string{"file.path = 'C:\\Windows\\System32'"}, nil, nil}, - FileName: {FileName, "full file name", kparams.UnicodeString, []string{"file.name contains 'mimikatz'"}, nil, nil}, - FileOperation: {FileOperation, "file operation", kparams.AnsiString, []string{"file.operation = 'open'"}, nil, nil}, - FileShareMask: {FileShareMask, "file share mask", kparams.AnsiString, []string{"file.share.mask = 'rw-'"}, nil, nil}, - FileIOSize: {FileIOSize, "file I/O size", kparams.Uint32, []string{"file.io.size > 512"}, nil, nil}, - FileOffset: {FileOffset, "file offset", kparams.Uint64, []string{"file.offset = 1024"}, nil, nil}, - FileType: {FileType, "file type", kparams.AnsiString, []string{"file.type = 'directory'"}, nil, nil}, - FileExtension: {FileExtension, "file extension", kparams.AnsiString, []string{"file.extension = '.dll'"}, nil, nil}, - FileAttributes: {FileAttributes, "file attributes", kparams.Slice, []string{"file.attributes in ('archive', 'hidden')"}, nil, nil}, - FileStatus: {FileStatus, "file operation status message", kparams.UnicodeString, []string{"file.status != 'success'"}, nil, nil}, - FileViewBase: {FileViewBase, "view base address", kparams.Address, []string{"file.view.base = '25d42170000'"}, nil, nil}, - FileViewSize: {FileViewSize, "size of the mapped view", kparams.Uint64, []string{"file.view.size > 1024"}, nil, nil}, - FileViewType: {FileViewType, "type of the mapped view section", kparams.Enum, []string{"file.view.type = 'IMAGE'"}, nil, nil}, - FileViewProtection: {FileViewProtection, "protection rights of the section view", kparams.AnsiString, []string{"file.view.protection = 'READONLY'"}, nil, nil}, - FileIsDriverMalicious: {FileIsDriverMalicious, "indicates if the dropped driver is malicious", kparams.Bool, []string{"file.is_driver_malicious"}, nil, nil}, - FileIsDriverVulnerable: {FileIsDriverVulnerable, "indicates if the dropped driver is vulnerable", kparams.Bool, []string{"file.is_driver_vulnerable"}, nil, nil}, - FileIsDLL: {FileIsDLL, "indicates if the created file is a DLL", kparams.Bool, []string{"file.is_dll'"}, nil, nil}, - FileIsDriver: {FileIsDriver, "indicates if the created file is a driver", kparams.Bool, []string{"file.is_driver'"}, nil, nil}, - FileIsExecutable: {FileIsExecutable, "indicates if the created file is an executable", kparams.Bool, []string{"file.is_exec'"}, nil, nil}, - FilePID: {FilePID, "denotes the process id performing file operation", kparams.PID, []string{"file.pid = 4"}, nil, nil}, - FileKey: {FileKey, "uniquely identifies the file object", kparams.Uint64, []string{"file.key = 12446738026482168384"}, nil, nil}, - FileInfoClass: {FileInfoClass, "identifies the file information class", kparams.Enum, []string{"file.info_class = 'Allocation'"}, nil, nil}, - FileInfoAllocationSize: {FileInfoAllocationSize, "file allocation size", kparams.Uint64, []string{"file.info.allocation_size > 645400"}, nil, nil}, - FileInfoEOFSize: {FileInfoEOFSize, "file EOF size", kparams.Uint64, []string{"file.info.eof_size > 1000"}, nil, nil}, - FileInfoIsDispositionDeleteFile: {FileInfoIsDispositionDeleteFile, "indicates if the file is deleted when its handle is closed", kparams.Bool, []string{"file.info.is_disposition_file_delete = true"}, nil, nil}, + FileObject: {FileObject, "file object address", params.Uint64, []string{"file.object = 18446738026482168384"}, nil, nil}, + FilePath: {FilePath, "full file path", params.UnicodeString, []string{"file.path = 'C:\\Windows\\System32'"}, nil, nil}, + FileName: {FileName, "full file name", params.UnicodeString, []string{"file.name contains 'mimikatz'"}, nil, nil}, + FileOperation: {FileOperation, "file operation", params.AnsiString, []string{"file.operation = 'open'"}, nil, nil}, + FileShareMask: {FileShareMask, "file share mask", params.AnsiString, []string{"file.share.mask = 'rw-'"}, nil, nil}, + FileIOSize: {FileIOSize, "file I/O size", params.Uint32, []string{"file.io.size > 512"}, nil, nil}, + FileOffset: {FileOffset, "file offset", params.Uint64, []string{"file.offset = 1024"}, nil, nil}, + FileType: {FileType, "file type", params.AnsiString, []string{"file.type = 'directory'"}, nil, nil}, + FileExtension: {FileExtension, "file extension", params.AnsiString, []string{"file.extension = '.dll'"}, nil, nil}, + FileAttributes: {FileAttributes, "file attributes", params.Slice, []string{"file.attributes in ('archive', 'hidden')"}, nil, nil}, + FileStatus: {FileStatus, "file operation status message", params.UnicodeString, []string{"file.status != 'success'"}, nil, nil}, + FileViewBase: {FileViewBase, "view base address", params.Address, []string{"file.view.base = '25d42170000'"}, nil, nil}, + FileViewSize: {FileViewSize, "size of the mapped view", params.Uint64, []string{"file.view.size > 1024"}, nil, nil}, + FileViewType: {FileViewType, "type of the mapped view section", params.Enum, []string{"file.view.type = 'IMAGE'"}, nil, nil}, + FileViewProtection: {FileViewProtection, "protection rights of the section view", params.AnsiString, []string{"file.view.protection = 'READONLY'"}, nil, nil}, + FileIsDriverMalicious: {FileIsDriverMalicious, "indicates if the dropped driver is malicious", params.Bool, []string{"file.is_driver_malicious"}, nil, nil}, + FileIsDriverVulnerable: {FileIsDriverVulnerable, "indicates if the dropped driver is vulnerable", params.Bool, []string{"file.is_driver_vulnerable"}, nil, nil}, + FileIsDLL: {FileIsDLL, "indicates if the created file is a DLL", params.Bool, []string{"file.is_dll'"}, nil, nil}, + FileIsDriver: {FileIsDriver, "indicates if the created file is a driver", params.Bool, []string{"file.is_driver'"}, nil, nil}, + FileIsExecutable: {FileIsExecutable, "indicates if the created file is an executable", params.Bool, []string{"file.is_exec'"}, nil, nil}, + FilePID: {FilePID, "denotes the process id performing file operation", params.PID, []string{"file.pid = 4"}, nil, nil}, + FileKey: {FileKey, "uniquely identifies the file object", params.Uint64, []string{"file.key = 12446738026482168384"}, nil, nil}, + FileInfoClass: {FileInfoClass, "identifies the file information class", params.Enum, []string{"file.info_class = 'Allocation'"}, nil, nil}, + FileInfoAllocationSize: {FileInfoAllocationSize, "file allocation size", params.Uint64, []string{"file.info.allocation_size > 645400"}, nil, nil}, + FileInfoEOFSize: {FileInfoEOFSize, "file EOF size", params.Uint64, []string{"file.info.eof_size > 1000"}, nil, nil}, + FileInfoIsDispositionDeleteFile: {FileInfoIsDispositionDeleteFile, "indicates if the file is deleted when its handle is closed", params.Bool, []string{"file.info.is_disposition_file_delete = true"}, nil, nil}, - RegistryPath: {RegistryPath, "fully qualified registry path", kparams.UnicodeString, []string{"registry.path = 'HKEY_LOCAL_MACHINE\\SYSTEM'"}, nil, nil}, - RegistryKeyName: {RegistryKeyName, "registry key name", kparams.UnicodeString, []string{"registry.key.name = 'CurrentControlSet'"}, nil, nil}, - RegistryKeyHandle: {RegistryKeyHandle, "registry key object address", kparams.Address, []string{"registry.key.handle = 'FFFFB905D60C2268'"}, nil, nil}, - RegistryValue: {RegistryValue, "registry value content", kparams.UnicodeString, []string{"registry.value = '%SystemRoot%\\system32'"}, nil, nil}, - RegistryValueType: {RegistryValueType, "type of registry value", kparams.UnicodeString, []string{"registry.value.type = 'REG_SZ'"}, nil, nil}, - RegistryStatus: {RegistryStatus, "status of registry operation", kparams.UnicodeString, []string{"registry.status != 'success'"}, nil, nil}, + RegistryPath: {RegistryPath, "fully qualified registry path", params.UnicodeString, []string{"registry.path = 'HKEY_LOCAL_MACHINE\\SYSTEM'"}, nil, nil}, + RegistryKeyName: {RegistryKeyName, "registry key name", params.UnicodeString, []string{"registry.key.name = 'CurrentControlSet'"}, nil, nil}, + RegistryKeyHandle: {RegistryKeyHandle, "registry key object address", params.Address, []string{"registry.key.handle = 'FFFFB905D60C2268'"}, nil, nil}, + RegistryValue: {RegistryValue, "registry value content", params.UnicodeString, []string{"registry.value = '%SystemRoot%\\system32'"}, nil, nil}, + RegistryValueType: {RegistryValueType, "type of registry value", params.UnicodeString, []string{"registry.value.type = 'REG_SZ'"}, nil, nil}, + RegistryStatus: {RegistryStatus, "status of registry operation", params.UnicodeString, []string{"registry.status != 'success'"}, nil, nil}, - NetDIP: {NetDIP, "destination IP address", kparams.IP, []string{"net.dip = 172.17.0.3"}, nil, nil}, - NetSIP: {NetSIP, "source IP address", kparams.IP, []string{"net.sip = 127.0.0.1"}, nil, nil}, - NetDport: {NetDport, "destination port", kparams.Uint16, []string{"net.dport in (80, 443, 8080)"}, nil, nil}, - NetSport: {NetSport, "source port", kparams.Uint16, []string{"net.sport != 3306"}, nil, nil}, - NetDportName: {NetDportName, "destination port name", kparams.AnsiString, []string{"net.dport.name = 'dns'"}, nil, nil}, - NetSportName: {NetSportName, "source port name", kparams.AnsiString, []string{"net.sport.name = 'http'"}, nil, nil}, - NetL4Proto: {NetL4Proto, "layer 4 protocol name", kparams.AnsiString, []string{"net.l4.proto = 'TCP"}, nil, nil}, - NetPacketSize: {NetPacketSize, "packet size", kparams.Uint32, []string{"net.size > 512"}, nil, nil}, - NetSIPNames: {NetSIPNames, "source IP names", kparams.Slice, []string{"net.sip.names in ('github.com.')"}, nil, nil}, - NetDIPNames: {NetDIPNames, "destination IP names", kparams.Slice, []string{"net.dip.names in ('github.com.')"}, nil, nil}, + NetDIP: {NetDIP, "destination IP address", params.IP, []string{"net.dip = 172.17.0.3"}, nil, nil}, + NetSIP: {NetSIP, "source IP address", params.IP, []string{"net.sip = 127.0.0.1"}, nil, nil}, + NetDport: {NetDport, "destination port", params.Uint16, []string{"net.dport in (80, 443, 8080)"}, nil, nil}, + NetSport: {NetSport, "source port", params.Uint16, []string{"net.sport != 3306"}, nil, nil}, + NetDportName: {NetDportName, "destination port name", params.AnsiString, []string{"net.dport.name = 'dns'"}, nil, nil}, + NetSportName: {NetSportName, "source port name", params.AnsiString, []string{"net.sport.name = 'http'"}, nil, nil}, + NetL4Proto: {NetL4Proto, "layer 4 protocol name", params.AnsiString, []string{"net.l4.proto = 'TCP"}, nil, nil}, + NetPacketSize: {NetPacketSize, "packet size", params.Uint32, []string{"net.size > 512"}, nil, nil}, + NetSIPNames: {NetSIPNames, "source IP names", params.Slice, []string{"net.sip.names in ('github.com.')"}, nil, nil}, + NetDIPNames: {NetDIPNames, "destination IP names", params.Slice, []string{"net.dip.names in ('github.com.')"}, nil, nil}, - HandleID: {HandleID, "handle identifier", kparams.Uint16, []string{"handle.id = 24"}, nil, nil}, - HandleObject: {HandleObject, "handle object address", kparams.Address, []string{"handle.object = 'FFFFB905DBF61988'"}, nil, nil}, - HandleName: {HandleName, "handle name", kparams.UnicodeString, []string{"handle.name = '\\Device\\NamedPipe\\chrome.12644.28.105826381'"}, nil, nil}, - HandleType: {HandleType, "handle type", kparams.AnsiString, []string{"handle.type = 'Mutant'"}, nil, nil}, + HandleID: {HandleID, "handle identifier", params.Uint16, []string{"handle.id = 24"}, nil, nil}, + HandleObject: {HandleObject, "handle object address", params.Address, []string{"handle.object = 'FFFFB905DBF61988'"}, nil, nil}, + HandleName: {HandleName, "handle name", params.UnicodeString, []string{"handle.name = '\\Device\\NamedPipe\\chrome.12644.28.105826381'"}, nil, nil}, + HandleType: {HandleType, "handle type", params.AnsiString, []string{"handle.type = 'Mutant'"}, nil, nil}, - PeNumSections: {PeNumSections, "number of sections", kparams.Uint16, []string{"pe.nsections < 5"}, nil, nil}, - PeNumSymbols: {PeNumSymbols, "number of entries in the symbol table", kparams.Uint32, []string{"pe.nsymbols > 230"}, nil, nil}, - PeBaseAddress: {PeBaseAddress, "image base address", kparams.Address, []string{"pe.address.base = '140000000'"}, nil, nil}, - PeEntrypoint: {PeEntrypoint, "address of the entrypoint function", kparams.Address, []string{"pe.address.entrypoint = '20110'"}, nil, nil}, - PeSymbols: {PeSymbols, "imported symbols", kparams.Slice, []string{"pe.symbols in ('GetTextFaceW', 'GetProcessHeap')"}, nil, nil}, - PeImports: {PeImports, "imported dynamic linked libraries", kparams.Slice, []string{"pe.imports in ('msvcrt.dll', 'GDI32.dll'"}, nil, nil}, + PeNumSections: {PeNumSections, "number of sections", params.Uint16, []string{"pe.nsections < 5"}, nil, nil}, + PeNumSymbols: {PeNumSymbols, "number of entries in the symbol table", params.Uint32, []string{"pe.nsymbols > 230"}, nil, nil}, + PeBaseAddress: {PeBaseAddress, "image base address", params.Address, []string{"pe.address.base = '140000000'"}, nil, nil}, + PeEntrypoint: {PeEntrypoint, "address of the entrypoint function", params.Address, []string{"pe.address.entrypoint = '20110'"}, nil, nil}, + PeSymbols: {PeSymbols, "imported symbols", params.Slice, []string{"pe.symbols in ('GetTextFaceW', 'GetProcessHeap')"}, nil, nil}, + PeImports: {PeImports, "imported dynamic linked libraries", params.Slice, []string{"pe.imports in ('msvcrt.dll', 'GDI32.dll'"}, nil, nil}, - PeResources: {PeResources, "version resources", kparams.Map, []string{"pe.resources[FileDescription] = 'Notepad'"}, nil, &Argument{Optional: true, Pattern: "[a-zA-Z0-9_]+", ValidationFunc: func(s string) bool { + PeResources: {PeResources, "version resources", params.Map, []string{"pe.resources[FileDescription] = 'Notepad'"}, nil, &Argument{Optional: true, Pattern: "[a-zA-Z0-9_]+", ValidationFunc: func(s string) bool { for _, c := range s { switch { case unicode.IsLower(c): @@ -965,58 +965,58 @@ var fields = map[Field]FieldInfo{ return true }}}, - PeCompany: {PeCompany, "internal company name of the file provided at compile-time", kparams.UnicodeString, []string{"pe.company = 'Microsoft Corporation'"}, nil, nil}, - PeCopyright: {PeCopyright, "copyright notice for the file emitted at compile-time", kparams.UnicodeString, []string{"pe.copyright = '© Microsoft Corporation'"}, nil, nil}, - PeDescription: {PeDescription, "internal description of the file provided at compile-time", kparams.UnicodeString, []string{"pe.description = 'Notepad'"}, nil, nil}, - PeFileName: {PeFileName, "original file name supplied at compile-time", kparams.UnicodeString, []string{"pe.file.name = 'NOTEPAD.EXE'"}, nil, nil}, - PeFileVersion: {PeFileVersion, "file version supplied at compile-time", kparams.UnicodeString, []string{"pe.file.version = '10.0.18362.693 (WinBuild.160101.0800)'"}, nil, nil}, - PeProduct: {PeProduct, "internal product name of the file provided at compile-time", kparams.UnicodeString, []string{"pe.product = 'Microsoft® Windows® Operating System'"}, nil, nil}, - PeProductVersion: {PeProductVersion, "internal product version of the file provided at compile-time", kparams.UnicodeString, []string{"pe.product.version = '10.0.18362.693'"}, nil, nil}, - PeIsDLL: {PeIsDLL, "indicates if the loaded image or created file is a DLL", kparams.Bool, []string{"pe.is_dll'"}, &Deprecation{Since: "2.0.0", Fields: []Field{FileIsDLL, ImageIsDLL}}, nil}, - PeIsDriver: {PeIsDriver, "indicates if the loaded image or created file is a driver", kparams.Bool, []string{"pe.is_driver'"}, &Deprecation{Since: "2.0.0", Fields: []Field{FileIsDriver, ImageIsDriver}}, nil}, - PeIsExecutable: {PeIsExecutable, "indicates if the loaded image or created file is an executable", kparams.Bool, []string{"pe.is_exec'"}, &Deprecation{Since: "2.0.0", Fields: []Field{FileIsExecutable, ImageIsExecutable}}, nil}, - PeImphash: {PeImphash, "import hash", kparams.AnsiString, []string{"pe.impash = '5d3861c5c547f8a34e471ba273a732b2'"}, nil, nil}, - PeIsDotnet: {PeIsDotnet, "indicates if PE contains CLR data", kparams.Bool, []string{"pe.is_dotnet"}, nil, nil}, - PeAnomalies: {PeAnomalies, "contains PE anomalies detected during parsing", kparams.Slice, []string{"pe.anomalies in ('number of sections is 0')"}, nil, nil}, - PeIsSigned: {PeIsSigned, "indicates if the PE has embedded or catalog signature", kparams.Bool, []string{"pe.is_signed"}, nil, nil}, - PeIsTrusted: {PeIsTrusted, "indicates if the PE certificate chain is trusted", kparams.Bool, []string{"pe.is_trusted"}, nil, nil}, - PeCertSerial: {PeCertSerial, "PE certificate serial number", kparams.UnicodeString, []string{"pe.cert.serial = '330000023241fb59996dcc4dff000000000232'"}, nil, nil}, - PeCertSubject: {PeCertSubject, "PE certificate subject", kparams.UnicodeString, []string{"pe.cert.subject contains 'Washington, Redmond, Microsoft Corporation'"}, nil, nil}, - PeCertIssuer: {PeCertIssuer, "PE certificate CA", kparams.UnicodeString, []string{"pe.cert.issuer contains 'Washington, Redmond, Microsoft Corporation'"}, nil, nil}, - PeCertAfter: {PeCertAfter, "PE certificate expiration date", kparams.Time, []string{"pe.cert.after contains '2024-02-01 00:05:42 +0000 UTC'"}, nil, nil}, - PeCertBefore: {PeCertBefore, "PE certificate enrollment date", kparams.Time, []string{"pe.cert.before contains '2024-02-01 00:05:42 +0000 UTC'"}, nil, nil}, - PeIsModified: {PeIsModified, "indicates if disk and in-memory PE headers differ", kparams.Bool, []string{"pe.is_modified"}, nil, nil}, - PePsChildFileName: {PePsChildFileName, "original file name of the child process executable supplied at compile-time", kparams.UnicodeString, []string{"pe.ps.child.file.name = 'NOTEPAD.EXE'"}, &Deprecation{Since: "2.3.0", Fields: []Field{PsChildPeFilename}}, nil}, + PeCompany: {PeCompany, "internal company name of the file provided at compile-time", params.UnicodeString, []string{"pe.company = 'Microsoft Corporation'"}, nil, nil}, + PeCopyright: {PeCopyright, "copyright notice for the file emitted at compile-time", params.UnicodeString, []string{"pe.copyright = '© Microsoft Corporation'"}, nil, nil}, + PeDescription: {PeDescription, "internal description of the file provided at compile-time", params.UnicodeString, []string{"pe.description = 'Notepad'"}, nil, nil}, + PeFileName: {PeFileName, "original file name supplied at compile-time", params.UnicodeString, []string{"pe.file.name = 'NOTEPAD.EXE'"}, nil, nil}, + PeFileVersion: {PeFileVersion, "file version supplied at compile-time", params.UnicodeString, []string{"pe.file.version = '10.0.18362.693 (WinBuild.160101.0800)'"}, nil, nil}, + PeProduct: {PeProduct, "internal product name of the file provided at compile-time", params.UnicodeString, []string{"pe.product = 'Microsoft® Windows® Operating System'"}, nil, nil}, + PeProductVersion: {PeProductVersion, "internal product version of the file provided at compile-time", params.UnicodeString, []string{"pe.product.version = '10.0.18362.693'"}, nil, nil}, + PeIsDLL: {PeIsDLL, "indicates if the loaded image or created file is a DLL", params.Bool, []string{"pe.is_dll'"}, &Deprecation{Since: "2.0.0", Fields: []Field{FileIsDLL, ImageIsDLL}}, nil}, + PeIsDriver: {PeIsDriver, "indicates if the loaded image or created file is a driver", params.Bool, []string{"pe.is_driver'"}, &Deprecation{Since: "2.0.0", Fields: []Field{FileIsDriver, ImageIsDriver}}, nil}, + PeIsExecutable: {PeIsExecutable, "indicates if the loaded image or created file is an executable", params.Bool, []string{"pe.is_exec'"}, &Deprecation{Since: "2.0.0", Fields: []Field{FileIsExecutable, ImageIsExecutable}}, nil}, + PeImphash: {PeImphash, "import hash", params.AnsiString, []string{"pe.impash = '5d3861c5c547f8a34e471ba273a732b2'"}, nil, nil}, + PeIsDotnet: {PeIsDotnet, "indicates if PE contains CLR data", params.Bool, []string{"pe.is_dotnet"}, nil, nil}, + PeAnomalies: {PeAnomalies, "contains PE anomalies detected during parsing", params.Slice, []string{"pe.anomalies in ('number of sections is 0')"}, nil, nil}, + PeIsSigned: {PeIsSigned, "indicates if the PE has embedded or catalog signature", params.Bool, []string{"pe.is_signed"}, nil, nil}, + PeIsTrusted: {PeIsTrusted, "indicates if the PE certificate chain is trusted", params.Bool, []string{"pe.is_trusted"}, nil, nil}, + PeCertSerial: {PeCertSerial, "PE certificate serial number", params.UnicodeString, []string{"pe.cert.serial = '330000023241fb59996dcc4dff000000000232'"}, nil, nil}, + PeCertSubject: {PeCertSubject, "PE certificate subject", params.UnicodeString, []string{"pe.cert.subject contains 'Washington, Redmond, Microsoft Corporation'"}, nil, nil}, + PeCertIssuer: {PeCertIssuer, "PE certificate CA", params.UnicodeString, []string{"pe.cert.issuer contains 'Washington, Redmond, Microsoft Corporation'"}, nil, nil}, + PeCertAfter: {PeCertAfter, "PE certificate expiration date", params.Time, []string{"pe.cert.after contains '2024-02-01 00:05:42 +0000 UTC'"}, nil, nil}, + PeCertBefore: {PeCertBefore, "PE certificate enrollment date", params.Time, []string{"pe.cert.before contains '2024-02-01 00:05:42 +0000 UTC'"}, nil, nil}, + PeIsModified: {PeIsModified, "indicates if disk and in-memory PE headers differ", params.Bool, []string{"pe.is_modified"}, nil, nil}, + PePsChildFileName: {PePsChildFileName, "original file name of the child process executable supplied at compile-time", params.UnicodeString, []string{"pe.ps.child.file.name = 'NOTEPAD.EXE'"}, &Deprecation{Since: "2.3.0", Fields: []Field{PsChildPeFilename}}, nil}, - MemBaseAddress: {MemBaseAddress, "region base address", kparams.Address, []string{"mem.address = '211d13f2000'"}, nil, nil}, - MemRegionSize: {MemRegionSize, "region size", kparams.Uint64, []string{"mem.size > 438272"}, nil, nil}, - MemAllocType: {MemAllocType, "region allocation or release type", kparams.Flags, []string{"mem.alloc = 'COMMIT'"}, nil, nil}, - MemPageType: {MemPageType, "page type of the allocated region", kparams.Enum, []string{"mem.type = 'PRIVATE'"}, nil, nil}, - MemProtection: {MemProtection, "allocated region protection type", kparams.Enum, []string{"mem.protection = 'READWRITE'"}, nil, nil}, - MemProtectionMask: {MemProtectionMask, "allocated region protection in mask notation", kparams.Enum, []string{"mem.protection.mask = 'RWX'"}, nil, nil}, + MemBaseAddress: {MemBaseAddress, "region base address", params.Address, []string{"mem.address = '211d13f2000'"}, nil, nil}, + MemRegionSize: {MemRegionSize, "region size", params.Uint64, []string{"mem.size > 438272"}, nil, nil}, + MemAllocType: {MemAllocType, "region allocation or release type", params.Flags, []string{"mem.alloc = 'COMMIT'"}, nil, nil}, + MemPageType: {MemPageType, "page type of the allocated region", params.Enum, []string{"mem.type = 'PRIVATE'"}, nil, nil}, + MemProtection: {MemProtection, "allocated region protection type", params.Enum, []string{"mem.protection = 'READWRITE'"}, nil, nil}, + MemProtectionMask: {MemProtectionMask, "allocated region protection in mask notation", params.Enum, []string{"mem.protection.mask = 'RWX'"}, nil, nil}, - DNSName: {DNSName, "dns query name", kparams.UnicodeString, []string{"dns.name = 'example.org'"}, nil, nil}, - DNSRR: {DNSRR, "dns resource record type", kparams.AnsiString, []string{"dns.rr = 'AA'"}, nil, nil}, - DNSOptions: {DNSOptions, "dns query options", kparams.Flags64, []string{"dns.options in ('ADDRCONFIG', 'DUAL_ADDR')"}, nil, nil}, - DNSRcode: {DNSRR, "dns response status", kparams.AnsiString, []string{"dns.rcode = 'NXDOMAIN'"}, nil, nil}, - DNSAnswers: {DNSAnswers, "dns response answers", kparams.Slice, []string{"dns.answers in ('o.lencr.edgesuite.net', 'a1887.dscq.akamai.net')"}, nil, nil}, + DNSName: {DNSName, "dns query name", params.UnicodeString, []string{"dns.name = 'example.org'"}, nil, nil}, + DNSRR: {DNSRR, "dns resource record type", params.AnsiString, []string{"dns.rr = 'AA'"}, nil, nil}, + DNSOptions: {DNSOptions, "dns query options", params.Flags64, []string{"dns.options in ('ADDRCONFIG', 'DUAL_ADDR')"}, nil, nil}, + DNSRcode: {DNSRR, "dns response status", params.AnsiString, []string{"dns.rcode = 'NXDOMAIN'"}, nil, nil}, + DNSAnswers: {DNSAnswers, "dns response answers", params.Slice, []string{"dns.answers in ('o.lencr.edgesuite.net', 'a1887.dscq.akamai.net')"}, nil, nil}, - ThreadpoolPoolID: {ThreadpoolPoolID, "thread pool identifier", kparams.Address, []string{"threadpool.id = '20f5fc02440'"}, nil, nil}, - ThreadpoolTaskID: {ThreadpoolTaskID, "thread pool task identifier", kparams.Address, []string{"threadpool.task.id = '20f7ecd21f8'"}, nil, nil}, - ThreadpoolCallbackAddress: {ThreadpoolCallbackAddress, "thread pool callback address", kparams.Address, []string{"threadpool.callback.address = '7ff868739ed0'"}, nil, nil}, - ThreadpoolCallbackSymbol: {ThreadpoolCallbackSymbol, "thread pool callback symbol", kparams.UnicodeString, []string{"threadpool.callback.symbol = 'RtlDestroyQueryDebugBuffer'"}, nil, nil}, - ThreadpoolCallbackModule: {ThreadpoolCallbackModule, "thread pool module containing the callback symbol", kparams.UnicodeString, []string{"threadpool.callback.module contains 'ntdll.dll'"}, nil, nil}, - ThreadpoolCallbackContext: {ThreadpoolCallbackContext, "thread pool callback context address", kparams.Address, []string{"threadpool.callback.context = '1df41e07bd0'"}, nil, nil}, - ThreadpoolCallbackContextRip: {ThreadpoolCallbackContextRip, "thread pool callback thread context instruction pointer", kparams.Address, []string{"threadpool.callback.context.rip = '1df42ffc1f8'"}, nil, nil}, - ThreadpoolCallbackContextRipSymbol: {ThreadpoolCallbackContextRipSymbol, "thread pool callback thread context instruction pointer symbol", kparams.UnicodeString, []string{"threadpool.callback.context.rip.symbol = 'VirtualProtect'"}, nil, nil}, - ThreadpoolCallbackContextRipModule: {ThreadpoolCallbackContextRipModule, "thread pool callback thread context instruction pointer symbol module", kparams.UnicodeString, []string{"threadpool.callback.context.rip.module contains 'ntdll.dll'"}, nil, nil}, - ThreadpoolSubprocessTag: {ThreadpoolSubprocessTag, "thread pool service identifier", kparams.Address, []string{"threadpool.subprocess_tag = '10d'"}, nil, nil}, - ThreadpoolTimerDuetime: {ThreadpoolTimerDuetime, "thread pool timer due time", kparams.Uint64, []string{"threadpool.timer.duetime > 10"}, nil, nil}, - ThreadpoolTimerSubqueue: {ThreadpoolTimerSubqueue, "thread pool timer subqueue address", kparams.Address, []string{"threadpool.timer.subqueue = '1db401703e8'"}, nil, nil}, - ThreadpoolTimer: {ThreadpoolTimer, "thread pool timer address", kparams.Address, []string{"threadpool.timer.address = '3e8'"}, nil, nil}, - ThreadpoolTimerPeriod: {ThreadpoolTimerPeriod, "thread pool timer period", kparams.Uint32, []string{"threadpool.timer.period = 0'"}, nil, nil}, - ThreadpoolTimerWindow: {ThreadpoolTimerWindow, "thread pool timer tolerate period", kparams.Uint32, []string{"threadpool.timer.window = 0'"}, nil, nil}, - ThreadpoolTimerAbsolute: {ThreadpoolTimerAbsolute, "indicates if the thread pool timer is absolute or relative", kparams.Bool, []string{"threadpool.timer.is_absolute = true'"}, nil, nil}, + ThreadpoolPoolID: {ThreadpoolPoolID, "thread pool identifier", params.Address, []string{"threadpool.id = '20f5fc02440'"}, nil, nil}, + ThreadpoolTaskID: {ThreadpoolTaskID, "thread pool task identifier", params.Address, []string{"threadpool.task.id = '20f7ecd21f8'"}, nil, nil}, + ThreadpoolCallbackAddress: {ThreadpoolCallbackAddress, "thread pool callback address", params.Address, []string{"threadpool.callback.address = '7ff868739ed0'"}, nil, nil}, + ThreadpoolCallbackSymbol: {ThreadpoolCallbackSymbol, "thread pool callback symbol", params.UnicodeString, []string{"threadpool.callback.symbol = 'RtlDestroyQueryDebugBuffer'"}, nil, nil}, + ThreadpoolCallbackModule: {ThreadpoolCallbackModule, "thread pool module containing the callback symbol", params.UnicodeString, []string{"threadpool.callback.module contains 'ntdll.dll'"}, nil, nil}, + ThreadpoolCallbackContext: {ThreadpoolCallbackContext, "thread pool callback context address", params.Address, []string{"threadpool.callback.context = '1df41e07bd0'"}, nil, nil}, + ThreadpoolCallbackContextRip: {ThreadpoolCallbackContextRip, "thread pool callback thread context instruction pointer", params.Address, []string{"threadpool.callback.context.rip = '1df42ffc1f8'"}, nil, nil}, + ThreadpoolCallbackContextRipSymbol: {ThreadpoolCallbackContextRipSymbol, "thread pool callback thread context instruction pointer symbol", params.UnicodeString, []string{"threadpool.callback.context.rip.symbol = 'VirtualProtect'"}, nil, nil}, + ThreadpoolCallbackContextRipModule: {ThreadpoolCallbackContextRipModule, "thread pool callback thread context instruction pointer symbol module", params.UnicodeString, []string{"threadpool.callback.context.rip.module contains 'ntdll.dll'"}, nil, nil}, + ThreadpoolSubprocessTag: {ThreadpoolSubprocessTag, "thread pool service identifier", params.Address, []string{"threadpool.subprocess_tag = '10d'"}, nil, nil}, + ThreadpoolTimerDuetime: {ThreadpoolTimerDuetime, "thread pool timer due time", params.Uint64, []string{"threadpool.timer.duetime > 10"}, nil, nil}, + ThreadpoolTimerSubqueue: {ThreadpoolTimerSubqueue, "thread pool timer subqueue address", params.Address, []string{"threadpool.timer.subqueue = '1db401703e8'"}, nil, nil}, + ThreadpoolTimer: {ThreadpoolTimer, "thread pool timer address", params.Address, []string{"threadpool.timer.address = '3e8'"}, nil, nil}, + ThreadpoolTimerPeriod: {ThreadpoolTimerPeriod, "thread pool timer period", params.Uint32, []string{"threadpool.timer.period = 0'"}, nil, nil}, + ThreadpoolTimerWindow: {ThreadpoolTimerWindow, "thread pool timer tolerate period", params.Uint32, []string{"threadpool.timer.window = 0'"}, nil, nil}, + ThreadpoolTimerAbsolute: {ThreadpoolTimerAbsolute, "indicates if the thread pool timer is absolute or relative", params.Bool, []string{"threadpool.timer.is_absolute = true'"}, nil, nil}, } // ArgumentOf returns argument data for the specified field. diff --git a/pkg/filter/fields/fields_windows_test.go b/pkg/filter/fields/fields_windows_test.go index 4359593ff..af082f1f4 100644 --- a/pkg/filter/fields/fields_windows_test.go +++ b/pkg/filter/fields/fields_windows_test.go @@ -31,9 +31,9 @@ func TestIsField(t *testing.T) { {"ps.pid", true}, {"ps.none", false}, {"ps.envs[ALLUSERSPROFILE]", false}, - {"kevt.arg", true}, + {"evt.arg", true}, {"thread._callstack", true}, - {"kevt._callstack", false}, + {"evt._callstack", false}, } for _, tt := range tests { diff --git a/pkg/filter/filter.go b/pkg/filter/filter.go index bb38ada64..ed92de1c4 100644 --- a/pkg/filter/filter.go +++ b/pkg/filter/filter.go @@ -22,10 +22,10 @@ import ( "errors" "expvar" "fmt" - kerrors "github.com/rabbitstack/fibratus/pkg/errors" + errs "github.com/rabbitstack/fibratus/pkg/errors" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/filter/fields" "github.com/rabbitstack/fibratus/pkg/filter/ql" - "github.com/rabbitstack/fibratus/pkg/kevent" "github.com/rabbitstack/fibratus/pkg/util/bytes" "github.com/rabbitstack/fibratus/pkg/util/hashers" "net" @@ -49,11 +49,11 @@ type Filter interface { Compile() error // Run runs a filter with a single expression. The return value decides // if the incoming event has successfully matched the filter expression. - Run(evt *kevent.Kevent) bool + Run(evt *event.Event) bool // RunSequence runs a filter with sequence expressions. Sequence rules depend // on the state machine transitions and partial matches to decide whether the // rule is fired. - RunSequence(evt *kevent.Kevent, seqID int, partials map[int][]*kevent.Kevent, rawMatch bool) bool + RunSequence(evt *event.Event, seqID int, partials map[int][]*event.Event, rawMatch bool) bool // GetStringFields returns field names mapped to their string values. GetStringFields() map[fields.Field][]string // GetFields returns all fields used in the filter expression. @@ -182,14 +182,14 @@ func (f *filter) Compile() error { return f.checkBoundRefs() } -func (f *filter) Run(e *kevent.Kevent) bool { +func (f *filter) Run(e *event.Event) bool { if f.expr == nil { return false } return ql.Eval(f.expr, f.mapValuer(e), f.hasFunctions) } -func (f *filter) RunSequence(e *kevent.Kevent, seqID int, partials map[int][]*kevent.Kevent, rawMatch bool) bool { +func (f *filter) RunSequence(e *event.Event, seqID int, partials map[int][]*event.Event, rawMatch bool) bool { if f.seq == nil { return false } @@ -211,7 +211,7 @@ func (f *filter) RunSequence(e *kevent.Kevent, seqID int, partials map[int][]*ke // if a sequence expression contains references to // bound fields we map all partials to their sequence // aliases - p := make(map[string][]*kevent.Kevent) + p := make(map[string][]*event.Event) nslots := len(partials[seqID]) for i := 0; i < seqID; i++ { alias := f.seq.Expressions[i].Alias @@ -234,7 +234,7 @@ func (f *filter) RunSequence(e *kevent.Kevent, seqID int, partials map[int][]*ke hash := make([]byte, 0) for nslots > 0 { nslots-- - var evt *kevent.Kevent + var evt *event.Event for _, field := range flds { // get all events pertaining to the bounded event evts := p[field.BoundVar] @@ -252,7 +252,7 @@ func (f *filter) RunSequence(e *kevent.Kevent, seqID int, partials map[int][]*ke continue } v, err := accessor.Get(field.Field, evt) - if err != nil && !kerrors.IsKparamNotFound(err) { + if err != nil && !errs.IsParamNotFound(err) { accessorErrors.Add(err.Error(), 1) continue } @@ -292,8 +292,8 @@ func (f *filter) RunSequence(e *kevent.Kevent, seqID int, partials map[int][]*ke match = ql.Eval(expr.Expr, valuer, f.hasFunctions) if match { // compute sequence key hash to tie the events - evt.AddMeta(kevent.RuleSequenceLink, hashers.FnvUint64(hash)) - e.AddMeta(kevent.RuleSequenceLink, hashers.FnvUint64(hash)) + evt.AddMeta(event.RuleSequenceLink, hashers.FnvUint64(hash)) + e.AddMeta(event.RuleSequenceLink, hashers.FnvUint64(hash)) break } } @@ -323,7 +323,7 @@ func (f *filter) RunSequence(e *kevent.Kevent, seqID int, partials map[int][]*ke if match && by != nil { if v := valuer[by.Value]; v != nil { - e.AddMeta(kevent.RuleSequenceLink, v) + e.AddMeta(event.RuleSequenceLink, v) } } } @@ -349,7 +349,7 @@ func (f *filter) GetSequence() *ql.Sequence { return f.seq } // with values extracted from the event. Field modifiers may contain a leading ordinal // which refers to the event in particular sequence stage. Otherwise, the modifier is // a well-known field name prepended with the `%` symbol. -func InterpolateFields(s string, evts []*kevent.Kevent) string { +func InterpolateFields(s string, evts []*event.Event) string { var fieldsReplRegexp = regexp.MustCompile(`%([1-9]?)\.?([a-z0-9A-Z\[\]._]+)`) matches := fieldsReplRegexp.FindAllStringSubmatch(s, -1) r := s @@ -384,14 +384,14 @@ func InterpolateFields(s string, evts []*kevent.Kevent) string { if i-1 > len(evts)-1 { continue } - kevt := evts[i-1] + evt := evts[i-1] // extract field value from the event and replace in string var val any for _, accessor := range GetAccessors() { name, arg := split(m[2]) f := Field{Value: m[2], Name: fields.Field(name), Arg: arg} var err error - val, err = accessor.Get(f, kevt) + val, err = accessor.Get(f, evt) if err != nil { continue } @@ -415,7 +415,7 @@ func InterpolateFields(s string, evts []*kevent.Kevent) string { // accessors and extract the field values that are // supplied to the valuer. The valuer feeds the // expression with correct values. -func (f *filter) mapValuer(evt *kevent.Kevent) map[string]interface{} { +func (f *filter) mapValuer(evt *event.Event) map[string]interface{} { valuer := make(map[string]interface{}, len(f.fields)) for _, field := range f.fields { for _, accessor := range f.accessors { @@ -423,7 +423,7 @@ func (f *filter) mapValuer(evt *kevent.Kevent) map[string]interface{} { continue } v, err := accessor.Get(field, evt) - if err != nil && !kerrors.IsKparamNotFound(err) { + if err != nil && !errs.IsParamNotFound(err) { accessorErrors.Add(err.Error(), 1) continue } diff --git a/pkg/filter/filter_test.go b/pkg/filter/filter_test.go index cc7c5c917..aefce2002 100644 --- a/pkg/filter/filter_test.go +++ b/pkg/filter/filter_test.go @@ -22,11 +22,10 @@ import ( "github.com/rabbitstack/fibratus/internal/etw/processors" "github.com/rabbitstack/fibratus/pkg/callstack" "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/filter/fields" "github.com/rabbitstack/fibratus/pkg/fs" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/rabbitstack/fibratus/pkg/pe" "github.com/rabbitstack/fibratus/pkg/ps" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" @@ -78,8 +77,8 @@ func TestFilterCompile(t *testing.T) { func TestSeqFilterCompile(t *testing.T) { f := New(`sequence -|kevt.name = 'CreateProcess'| by ps.exe -|kevt.name = 'CreateFile' and file.operation = 'create'| by file.name +|evt.name = 'CreateProcess'| by ps.exe +|evt.name = 'CreateFile' and file.operation = 'create'| by file.name `, cfg) require.NoError(t, f.Compile()) require.NotNil(t, f.GetSequence()) @@ -90,19 +89,19 @@ func TestSeqFilterCompile(t *testing.T) { func TestSeqFilterInvalidBoundRefs(t *testing.T) { f := New(`sequence -|kevt.name = 'CreateProcess'| as e1 -|kevt.name = 'CreateFile' and file.name = $e.ps.exe | +|evt.name = 'CreateProcess'| as e1 +|evt.name = 'CreateFile' and file.name = $e.ps.exe | `, cfg) require.Error(t, f.Compile()) f1 := New(`sequence -|kevt.name = 'CreateProcess'| as e1 -|kevt.name = 'CreateFile' and file.name = $e1.ps.exe | +|evt.name = 'CreateProcess'| as e1 +|evt.name = 'CreateFile' and file.name = $e1.ps.exe | `, cfg) require.NoError(t, f1.Compile()) } func TestStringFields(t *testing.T) { - f := New(`ps.name = 'cmd.exe' and kevt.name = 'CreateProcess' or kevt.name in ('TerminateProcess', 'CreateFile')`, cfg) + f := New(`ps.name = 'cmd.exe' and evt.name = 'CreateProcess' or evt.name in ('TerminateProcess', 'CreateFile')`, cfg) require.NoError(t, f.Compile()) assert.Len(t, f.GetStringFields(), 2) assert.Len(t, f.GetStringFields()[fields.KevtName], 3) @@ -110,19 +109,19 @@ func TestStringFields(t *testing.T) { } func TestProcFilter(t *testing.T) { - kpars := kevent.Kparams{ - kparams.Cmdline: {Name: kparams.Cmdline, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\svchost-fake.exe -k RPCSS"}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.AnsiString, Value: "svchost-fake.exe"}, - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(1234)}, - kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.PID, Value: uint32(345)}, - kparams.UserSID: {Name: kparams.UserSID, Type: kparams.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, - kparams.Username: {Name: kparams.Username, Type: kparams.UnicodeString, Value: "loki"}, - kparams.Domain: {Name: kparams.Domain, Type: kparams.UnicodeString, Value: "TITAN"}, - kparams.ProcessFlags: {Name: kparams.ProcessFlags, Type: kparams.Flags, Value: uint32(0x000000E)}, + pars := event.Params{ + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\svchost-fake.exe -k RPCSS"}, + params.ProcessName: {Name: params.ProcessName, Type: params.AnsiString, Value: "svchost-fake.exe"}, + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1234)}, + params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(345)}, + params.UserSID: {Name: params.UserSID, Type: params.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, + params.Username: {Name: params.Username, Type: params.UnicodeString, Value: "loki"}, + params.Domain: {Name: params.Domain, Type: params.UnicodeString, Value: "TITAN"}, + params.ProcessFlags: {Name: params.ProcessFlags, Type: params.Flags, Value: uint32(0x000000E)}, } - kpars1 := kevent.Kparams{ - kparams.DesiredAccess: {Name: kparams.DesiredAccess, Type: kparams.Flags, Value: uint32(0x1400), Flags: kevent.PsAccessRightFlags}, + pars1 := event.Params{ + params.DesiredAccess: {Name: params.DesiredAccess, Type: params.Flags, Value: uint32(0x1400), Flags: event.PsAccessRightFlags}, } ps1 := &pstypes.PS{ @@ -144,10 +143,10 @@ func TestProcFilter(t *testing.T) { IsPackaged: false, } - kevt := &kevent.Kevent{ - Type: ktypes.CreateProcess, - Category: ktypes.Process, - Kparams: kpars, + evt := &event.Event{ + Type: event.CreateProcess, + Category: event.Process, + Params: pars, Name: "CreateProcess", PID: 1023, PS: &pstypes.PS{ @@ -177,12 +176,12 @@ func TestProcFilter(t *testing.T) { IsWOW64: false, }, } - kevt.Timestamp, _ = time.Parse(time.RFC3339, "2011-05-03T15:04:05.323Z") + evt.Timestamp, _ = time.Parse(time.RFC3339, "2011-05-03T15:04:05.323Z") - kevt1 := &kevent.Kevent{ - Type: ktypes.OpenProcess, - Category: ktypes.Process, - Kparams: kpars1, + evt1 := &event.Event{ + Type: event.OpenProcess, + Category: event.Process, + Params: pars1, Name: "OpenProcess", PID: 1023, PS: &pstypes.PS{ @@ -241,10 +240,10 @@ func TestProcFilter(t *testing.T) { {`ps.parent.is_wow64`, false}, {`ps.parent.is_packaged`, false}, {`ps.parent.is_protected`, true}, - {`kevt.name = 'CreateProcess' and ps.name contains 'svchost'`, true}, + {`evt.name = 'CreateProcess' and ps.name contains 'svchost'`, true}, {`ps.modules IN ('kernel32.dll')`, true}, - {`kevt.name = 'CreateProcess' and kevt.pid != ps.ppid`, true}, + {`evt.name = 'CreateProcess' and evt.pid != ps.ppid`, true}, {`ps.parent.name = 'wininit.exe'`, true}, {`ps.ancestor[0] = 'svchost.exe'`, false}, @@ -299,7 +298,7 @@ func TestProcFilter(t *testing.T) { if err != nil { t.Fatal(err) } - matches := f.Run(kevt) + matches := f.Run(evt) if matches != tt.matches { t.Errorf("%d. %q ps filter mismatch: exp=%t got=%t", i, tt.filter, tt.matches, matches) } @@ -320,7 +319,7 @@ func TestProcFilter(t *testing.T) { if err != nil { t.Fatal(err) } - matches := f.Run(kevt1) + matches := f.Run(evt1) if matches != tt.matches { t.Errorf("%d. %q ps filter mismatch: exp=%t got=%t", i, tt.filter, tt.matches, matches) } @@ -328,27 +327,27 @@ func TestProcFilter(t *testing.T) { } func TestThreadFilter(t *testing.T) { - kpars := kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(os.Getpid())}, - kparams.ThreadID: {Name: kparams.ThreadID, Type: kparams.TID, Value: uint32(3453)}, - kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Uint8, Value: uint8(13)}, - kparams.StartAddress: {Name: kparams.StartAddress, Type: kparams.Address, Value: uint64(140729524944768)}, - kparams.TEB: {Name: kparams.TEB, Type: kparams.Address, Value: uint64(614994620416)}, - kparams.IOPrio: {Name: kparams.IOPrio, Type: kparams.Uint8, Value: uint8(2)}, - kparams.KstackBase: {Name: kparams.KstackBase, Type: kparams.Address, Value: uint64(18446677035730165760)}, - kparams.KstackLimit: {Name: kparams.KstackLimit, Type: kparams.Address, Value: uint64(18446677035730137088)}, - kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(5)}, - kparams.UstackBase: {Name: kparams.UstackBase, Type: kparams.Address, Value: uint64(86376448)}, - kparams.UstackLimit: {Name: kparams.UstackLimit, Type: kparams.Address, Value: uint64(86372352)}, - kparams.StartAddressSymbol: {Name: kparams.StartAddressSymbol, Type: kparams.UnicodeString, Value: "LoadImage"}, - kparams.StartAddressModule: {Name: kparams.StartAddressModule, Type: kparams.UnicodeString, Value: "C:\\Windows\\System32\\kernel32.dll"}, - } - kevt := &kevent.Kevent{ - Type: ktypes.CreateThread, - Kparams: kpars, + pars := event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(os.Getpid())}, + params.ThreadID: {Name: params.ThreadID, Type: params.TID, Value: uint32(3453)}, + params.BasePrio: {Name: params.BasePrio, Type: params.Uint8, Value: uint8(13)}, + params.StartAddress: {Name: params.StartAddress, Type: params.Address, Value: uint64(140729524944768)}, + params.TEB: {Name: params.TEB, Type: params.Address, Value: uint64(614994620416)}, + params.IOPrio: {Name: params.IOPrio, Type: params.Uint8, Value: uint8(2)}, + params.KstackBase: {Name: params.KstackBase, Type: params.Address, Value: uint64(18446677035730165760)}, + params.KstackLimit: {Name: params.KstackLimit, Type: params.Address, Value: uint64(18446677035730137088)}, + params.PagePrio: {Name: params.PagePrio, Type: params.Uint8, Value: uint8(5)}, + params.UstackBase: {Name: params.UstackBase, Type: params.Address, Value: uint64(86376448)}, + params.UstackLimit: {Name: params.UstackLimit, Type: params.Address, Value: uint64(86372352)}, + params.StartAddressSymbol: {Name: params.StartAddressSymbol, Type: params.UnicodeString, Value: "LoadImage"}, + params.StartAddressModule: {Name: params.StartAddressModule, Type: params.UnicodeString, Value: "C:\\Windows\\System32\\kernel32.dll"}, + } + evt := &event.Event{ + Type: event.CreateThread, + Params: pars, Name: "CreateThread", PID: windows.GetCurrentProcessId(), - Category: ktypes.Thread, + Category: event.Thread, PS: &pstypes.PS{ Name: "svchost.exe", Envs: map[string]string{"ALLUSERSPROFILE": "C:\\ProgramData", "OS": "Windows_NT", "ProgramFiles(x86)": "C:\\Program Files (x86)"}, @@ -379,15 +378,15 @@ func TestThreadFilter(t *testing.T) { } require.NoError(t, windows.WriteProcessMemory(windows.CurrentProcess(), base, &insns[0], uintptr(len(insns)), nil)) - kevt.Callstack.Init(8) - kevt.Callstack.PushFrame(callstack.Frame{PID: kevt.PID, Addr: 0x2638e59e0a5, Offset: 0, Symbol: "?", Module: "unbacked"}) - kevt.Callstack.PushFrame(callstack.Frame{PID: kevt.PID, Addr: va.Address(base), Offset: 0, Symbol: "?", Module: "unbacked"}) - kevt.Callstack.PushFrame(callstack.Frame{PID: kevt.PID, Addr: 0x7ffb313853b2, Offset: 0x10a, Symbol: "Java_java_lang_ProcessImpl_create", Module: "C:\\Program Files\\JetBrains\\GoLand 2021.2.3\\jbr\\bin\\java.dll"}) - kevt.Callstack.PushFrame(callstack.Frame{PID: kevt.PID, Addr: 0x7ffb3138592e, ModuleAddress: 0x7ffb3138592e, Offset: 0x3a2, Symbol: "Java_java_lang_ProcessImpl_waitForTimeoutInterruptibly", Module: "C:\\Program Files\\JetBrains\\GoLand 2021.2.3\\jbr\\bin\\java.dll"}) - kevt.Callstack.PushFrame(callstack.Frame{PID: kevt.PID, Addr: 0x7ffb5d8e61f4, Offset: 0x54, Symbol: "CreateProcessW", Module: "C:\\WINDOWS\\System32\\KERNEL32.DLL"}) - kevt.Callstack.PushFrame(callstack.Frame{PID: kevt.PID, Addr: 0x7ffb5c1d0396, Offset: 0x66, Symbol: "CreateProcessW", Module: "C:\\WINDOWS\\System32\\KERNELBASE.dll"}) - kevt.Callstack.PushFrame(callstack.Frame{PID: kevt.PID, Addr: 0xfffff8072ebc1f6f, Offset: 0x4ef, Symbol: "FltRequestFileInfoOnCreateCompletion", Module: "C:\\WINDOWS\\System32\\drivers\\FLTMGR.SYS"}) - kevt.Callstack.PushFrame(callstack.Frame{PID: kevt.PID, Addr: 0xfffff8072eb8961b, Offset: 0x20cb, Symbol: "FltGetStreamContext", Module: "C:\\WINDOWS\\System32\\drivers\\FLTMGR.SYS"}) + evt.Callstack.Init(8) + evt.Callstack.PushFrame(callstack.Frame{PID: evt.PID, Addr: 0x2638e59e0a5, Offset: 0, Symbol: "?", Module: "unbacked"}) + evt.Callstack.PushFrame(callstack.Frame{PID: evt.PID, Addr: va.Address(base), Offset: 0, Symbol: "?", Module: "unbacked"}) + evt.Callstack.PushFrame(callstack.Frame{PID: evt.PID, Addr: 0x7ffb313853b2, Offset: 0x10a, Symbol: "Java_java_lang_ProcessImpl_create", Module: "C:\\Program Files\\JetBrains\\GoLand 2021.2.3\\jbr\\bin\\java.dll"}) + evt.Callstack.PushFrame(callstack.Frame{PID: evt.PID, Addr: 0x7ffb3138592e, ModuleAddress: 0x7ffb3138592e, Offset: 0x3a2, Symbol: "Java_java_lang_ProcessImpl_waitForTimeoutInterruptibly", Module: "C:\\Program Files\\JetBrains\\GoLand 2021.2.3\\jbr\\bin\\java.dll"}) + evt.Callstack.PushFrame(callstack.Frame{PID: evt.PID, Addr: 0x7ffb5d8e61f4, Offset: 0x54, Symbol: "CreateProcessW", Module: "C:\\WINDOWS\\System32\\KERNEL32.DLL"}) + evt.Callstack.PushFrame(callstack.Frame{PID: evt.PID, Addr: 0x7ffb5c1d0396, Offset: 0x66, Symbol: "CreateProcessW", Module: "C:\\WINDOWS\\System32\\KERNELBASE.dll"}) + evt.Callstack.PushFrame(callstack.Frame{PID: evt.PID, Addr: 0xfffff8072ebc1f6f, Offset: 0x4ef, Symbol: "FltRequestFileInfoOnCreateCompletion", Module: "C:\\WINDOWS\\System32\\drivers\\FLTMGR.SYS"}) + evt.Callstack.PushFrame(callstack.Frame{PID: evt.PID, Addr: 0xfffff8072eb8961b, Offset: 0x20cb, Symbol: "FltGetStreamContext", Module: "C:\\WINDOWS\\System32\\drivers\\FLTMGR.SYS"}) var tests = []struct { filter string @@ -452,7 +451,7 @@ func TestThreadFilter(t *testing.T) { if err != nil { t.Fatal(err) } - matches := f.Run(kevt) + matches := f.Run(evt) if matches != tt.matches { t.Errorf("%d. %q thread filter mismatch: exp=%t got=%t", i, tt.filter, tt.matches, matches) } @@ -487,7 +486,7 @@ func TestThreadFilter(t *testing.T) { } defer windows.TerminateProcess(pi.Process, 0) - kevt.PID = pi.ProcessId + evt.PID = pi.ProcessId // try until a valid address is returned // or fail if max attempts are exhausted @@ -507,7 +506,7 @@ func TestThreadFilter(t *testing.T) { var n uintptr require.NoError(t, windows.WriteProcessMemory(pi.Process, ntdll, &insns[0], uintptr(len(insns)), &n)) - kevt.Callstack[0] = callstack.Frame{PID: kevt.PID, Addr: va.Address(ntdll), Offset: 0, Symbol: "?", Module: "C:\\Windows\\System32\\ntdll.dll"} + evt.Callstack[0] = callstack.Frame{PID: evt.PID, Addr: va.Address(ntdll), Offset: 0, Symbol: "?", Module: "C:\\Windows\\System32\\ntdll.dll"} var tests1 = []struct { filter string @@ -524,7 +523,7 @@ func TestThreadFilter(t *testing.T) { if err != nil { t.Fatal(err) } - matches := f.Run(kevt) + matches := f.Run(evt) if matches != tt.matches { t.Errorf("%d. %q thread filter mismatch: exp=%t got=%t", i, tt.filter, tt.matches, matches) } @@ -532,23 +531,23 @@ func TestThreadFilter(t *testing.T) { } func TestFileFilter(t *testing.T) { - kevt := &kevent.Kevent{ - Type: ktypes.CreateFile, + evt := &event.Event{ + Type: event.CreateFile, Tid: 2484, PID: 859, CPU: 1, Seq: 2, Name: "CreateFile", - Category: ktypes.File, + Category: event.File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", - Kparams: kevent.Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.AnsiString, Value: "open"}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, } var tests = []struct { @@ -600,7 +599,7 @@ func TestFileFilter(t *testing.T) { if err != nil { t.Fatal(err) } - matches := f.Run(kevt) + matches := f.Run(evt) if matches != tt.matches { t.Errorf("%d. %q file filter mismatch: exp=%t got=%t", i, tt.filter, tt.matches, matches) } @@ -610,69 +609,69 @@ func TestFileFilter(t *testing.T) { func TestFileInfoFilter(t *testing.T) { var tests = []struct { f string - e *kevent.Kevent + e *event.Event matches bool }{ { `file.info_class = 'Allocation'`, - &kevent.Kevent{ - Category: ktypes.File, - Type: ktypes.SetFileInformation, + &event.Event{ + Category: event.File, + Type: event.SetFileInformation, Name: "SetFileInformation", - Kparams: kevent.Kparams{ - kparams.FileInfoClass: {Name: kparams.FileInfoClass, Type: kparams.Enum, Value: fs.AllocationClass, Enum: fs.FileInfoClasses}, + Params: event.Params{ + params.FileInfoClass: {Name: params.FileInfoClass, Type: params.Enum, Value: fs.AllocationClass, Enum: fs.FileInfoClasses}, }, }, true, }, { `file.info.allocation_size = 64500`, - &kevent.Kevent{ - Category: ktypes.File, - Type: ktypes.SetFileInformation, + &event.Event{ + Category: event.File, + Type: event.SetFileInformation, Name: "SetFileInformation", - Kparams: kevent.Kparams{ - kparams.FileInfoClass: {Name: kparams.FileInfoClass, Type: kparams.Enum, Value: fs.AllocationClass, Enum: fs.FileInfoClasses}, - kparams.FileExtraInfo: {Name: kparams.FileExtraInfo, Type: kparams.Uint64, Value: uint64(64500)}, + Params: event.Params{ + params.FileInfoClass: {Name: params.FileInfoClass, Type: params.Enum, Value: fs.AllocationClass, Enum: fs.FileInfoClasses}, + params.FileExtraInfo: {Name: params.FileExtraInfo, Type: params.Uint64, Value: uint64(64500)}, }, }, true, }, { `file.info.eof_size = 64500`, - &kevent.Kevent{ - Category: ktypes.File, - Type: ktypes.SetFileInformation, + &event.Event{ + Category: event.File, + Type: event.SetFileInformation, Name: "SetFileInformation", - Kparams: kevent.Kparams{ - kparams.FileInfoClass: {Name: kparams.FileInfoClass, Type: kparams.Enum, Value: fs.EOFClass, Enum: fs.FileInfoClasses}, - kparams.FileExtraInfo: {Name: kparams.FileExtraInfo, Type: kparams.Uint64, Value: uint64(64500)}, + Params: event.Params{ + params.FileInfoClass: {Name: params.FileInfoClass, Type: params.Enum, Value: fs.EOFClass, Enum: fs.FileInfoClasses}, + params.FileExtraInfo: {Name: params.FileExtraInfo, Type: params.Uint64, Value: uint64(64500)}, }, }, true, }, { `file.info.eof_size = 64500`, - &kevent.Kevent{ - Category: ktypes.File, - Type: ktypes.SetFileInformation, + &event.Event{ + Category: event.File, + Type: event.SetFileInformation, Name: "SetFileInformation", - Kparams: kevent.Kparams{ - kparams.FileInfoClass: {Name: kparams.FileInfoClass, Type: kparams.Enum, Value: fs.DispositionClass, Enum: fs.FileInfoClasses}, - kparams.FileExtraInfo: {Name: kparams.FileExtraInfo, Type: kparams.Uint64, Value: uint64(1)}, + Params: event.Params{ + params.FileInfoClass: {Name: params.FileInfoClass, Type: params.Enum, Value: fs.DispositionClass, Enum: fs.FileInfoClasses}, + params.FileExtraInfo: {Name: params.FileExtraInfo, Type: params.Uint64, Value: uint64(1)}, }, }, false, }, { `file.info.is_disposition_delete_file = true`, - &kevent.Kevent{ - Category: ktypes.File, - Type: ktypes.DeleteFile, + &event.Event{ + Category: event.File, + Type: event.DeleteFile, Name: "DeleteFile", - Kparams: kevent.Kparams{ - kparams.FileInfoClass: {Name: kparams.FileInfoClass, Type: kparams.Enum, Value: fs.DispositionClass, Enum: fs.FileInfoClasses}, - kparams.FileExtraInfo: {Name: kparams.FileExtraInfo, Type: kparams.Uint64, Value: uint64(1)}, + Params: event.Params{ + params.FileInfoClass: {Name: params.FileInfoClass, Type: params.Enum, Value: fs.DispositionClass, Enum: fs.FileInfoClasses}, + params.FileExtraInfo: {Name: params.FileExtraInfo, Type: params.Uint64, Value: uint64(1)}, }, }, true, @@ -692,63 +691,63 @@ func TestFileInfoFilter(t *testing.T) { } func TestKeventFilter(t *testing.T) { - kevt := &kevent.Kevent{ - Type: ktypes.CreateFile, + evt := &event.Event{ + Type: event.CreateFile, Tid: 2484, PID: 859, CPU: 1, Seq: 2, Name: "CreateFile", - Category: ktypes.File, + Category: event.File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(3434)}, - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(3434)}, + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.AnsiString, Value: "open"}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barz"}, } - kevt.Timestamp, _ = time.Parse(time.RFC3339, "2011-05-03T15:04:05.323Z") + evt.Timestamp, _ = time.Parse(time.RFC3339, "2011-05-03T15:04:05.323Z") var tests = []struct { filter string matches bool }{ - {`kevt.seq = 2`, true}, - {`kevt.pid = 859`, true}, - {`kevt.tid = 2484`, true}, - {`kevt.cpu = 1`, true}, - {`kevt.name = 'CreateFile'`, true}, - {`kevt.category = 'file'`, true}, - {`kevt.host = 'archrabbit'`, true}, - {`kevt.nparams = 5`, true}, - {`kevt.arg[file_path] = '\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll'`, true}, - {`kevt.arg[type] = 'file'`, true}, - {`kevt.arg[pid] = 3434`, true}, - - {`kevt.desc contains 'Creates or opens a new file'`, true}, - - {`kevt.date.d = 3 AND kevt.date.m = 5 AND kevt.time.s = 5 AND kevt.time.m = 4 and kevt.time.h = 15`, true}, - {`kevt.time = '15:04:05'`, true}, - {`concat(kevt.name, kevt.host, kevt.nparams) = 'CreateFilearchrabbit5'`, true}, - {`ltrim(kevt.host, 'arch') = 'rabbit'`, true}, - {`concat(ltrim(kevt.name, 'Create'), kevt.host) = 'Filearchrabbit'`, true}, - {`lower(rtrim(kevt.name, 'File')) = 'create'`, true}, - {`upper(rtrim(kevt.name, 'File')) = 'CREATE'`, true}, - {`replace(kevt.host, 'rabbit', '_bunny') = 'arch_bunny'`, true}, - {`replace(kevt.host, 'rabbit', '_bunny', '_bunny', 'bunny') = 'archbunny'`, true}, + {`evt.seq = 2`, true}, + {`evt.pid = 859`, true}, + {`evt.tid = 2484`, true}, + {`evt.cpu = 1`, true}, + {`evt.name = 'CreateFile'`, true}, + {`evt.category = 'file'`, true}, + {`evt.host = 'archrabbit'`, true}, + {`evt.nparams = 5`, true}, + {`evt.arg[file_path] = '\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll'`, true}, + {`evt.arg[type] = 'file'`, true}, + {`evt.arg[pid] = 3434`, true}, + + {`evt.desc contains 'Creates or opens a new file'`, true}, + + {`evt.date.d = 3 AND evt.date.m = 5 AND evt.time.s = 5 AND evt.time.m = 4 and evt.time.h = 15`, true}, + {`evt.time = '15:04:05'`, true}, + {`concat(evt.name, evt.host, evt.nparams) = 'CreateFilearchrabbit5'`, true}, + {`ltrim(evt.host, 'arch') = 'rabbit'`, true}, + {`concat(ltrim(evt.name, 'Create'), evt.host) = 'Filearchrabbit'`, true}, + {`lower(rtrim(evt.name, 'File')) = 'create'`, true}, + {`upper(rtrim(evt.name, 'File')) = 'CREATE'`, true}, + {`replace(evt.host, 'rabbit', '_bunny') = 'arch_bunny'`, true}, + {`replace(evt.host, 'rabbit', '_bunny', '_bunny', 'bunny') = 'archbunny'`, true}, {`split(file.path, '\\') IN ('windows', 'system32')`, true}, {`length(file.path) = 51`, true}, {`indexof(file.path, '\\') = 0`, true}, {`indexof(file.path, '\\', 'last') = 40`, true}, {`indexof(file.path, 'h2', 'any') = 22`, true}, {`substr(file.path, indexof(file.path, '\\'), indexof(file.path, '\\Hard')) = '\\Device'`, true}, - {`substr(kevt.desc, indexof(kevt.desc, '\\'), indexof(kevt.desc, 'NOT')) = 'Creates or opens a new file, directory, I/O device, pipe, console'`, true}, + {`substr(evt.desc, indexof(evt.desc, '\\'), indexof(evt.desc, 'NOT')) = 'Creates or opens a new file, directory, I/O device, pipe, console'`, true}, {`entropy(file.path) > 120`, true}, {`regex(file.path, '\\\\Device\\\\HarddiskVolume[2-9]+\\\\.*')`, true}, } @@ -759,27 +758,27 @@ func TestKeventFilter(t *testing.T) { if err != nil { t.Fatal(err) } - matches := f.Run(kevt) + matches := f.Run(evt) if matches != tt.matches { - t.Errorf("%d. %q kevt filter mismatch: exp=%t got=%t", i, tt.filter, tt.matches, matches) + t.Errorf("%d. %q evt filter mismatch: exp=%t got=%t", i, tt.filter, tt.matches, matches) } } } func TestNetFilter(t *testing.T) { - kevt := &kevent.Kevent{ - Type: ktypes.SendTCPv4, + evt := &event.Event{ + Type: event.SendTCPv4, Tid: 2484, PID: 859, PS: &pstypes.PS{ Name: "cmd.exe", }, - Category: ktypes.Net, - Kparams: kevent.Kparams{ - kparams.NetDport: {Name: kparams.NetDport, Type: kparams.Uint16, Value: uint16(443)}, - kparams.NetSport: {Name: kparams.NetSport, Type: kparams.Uint16, Value: uint16(43123)}, - kparams.NetSIP: {Name: kparams.NetSIP, Type: kparams.IPv4, Value: net.ParseIP("127.0.0.1")}, - kparams.NetDIP: {Name: kparams.NetDIP, Type: kparams.IPv4, Value: net.ParseIP("216.58.201.174")}, + Category: event.Net, + Params: event.Params{ + params.NetDport: {Name: params.NetDport, Type: params.Uint16, Value: uint16(443)}, + params.NetSport: {Name: params.NetSport, Type: params.Uint16, Value: uint16(43123)}, + params.NetSIP: {Name: params.NetSIP, Type: params.IPv4, Value: net.ParseIP("127.0.0.1")}, + params.NetDIP: {Name: params.NetDIP, Type: params.IPv4, Value: net.ParseIP("216.58.201.174")}, }, } @@ -802,8 +801,8 @@ func TestNetFilter(t *testing.T) { {`ps.name = 'cmd.exe' and not ((net.sip in (222.1.1.1)) or (net.sip in (12.3.4.5)))`, true}, {`cidr_contains(net.dip, '216.58.201.1/24') = true`, true}, {`cidr_contains(net.dip, '226.58.201.1/24') = false`, true}, - {`cidr_contains(net.dip, '216.58.201.1/24', '216.58.201.10/24') = true and kevt.pid = 859`, true}, - {`kevt.name not in ('CreateProcess', 'Connect') and cidr_contains(net.dip, '216.58.201.1/24') = true`, true}, + {`cidr_contains(net.dip, '216.58.201.1/24', '216.58.201.10/24') = true and evt.pid = 859`, true}, + {`evt.name not in ('CreateProcess', 'Connect') and cidr_contains(net.dip, '216.58.201.1/24') = true`, true}, } for i, tt := range tests { @@ -812,25 +811,25 @@ func TestNetFilter(t *testing.T) { if err != nil { t.Fatal(err) } - matches := f.Run(kevt) + matches := f.Run(evt) if matches != tt.matches { t.Errorf("%d. %q net filter mismatch: exp=%t got=%t", i, tt.filter, tt.matches, matches) } } - kevt1 := &kevent.Kevent{ - Type: ktypes.SendTCPv4, + evt1 := &event.Event{ + Type: event.SendTCPv4, Tid: 2484, PID: 859, PS: &pstypes.PS{ Name: "cmd.exe", }, - Category: ktypes.Net, - Kparams: kevent.Kparams{ - kparams.NetDport: {Name: kparams.NetDport, Type: kparams.Uint16, Value: uint16(53)}, - kparams.NetSport: {Name: kparams.NetSport, Type: kparams.Uint16, Value: uint16(43123)}, - kparams.NetSIP: {Name: kparams.NetSIP, Type: kparams.IPv4, Value: net.ParseIP("127.0.0.1")}, - kparams.NetDIP: {Name: kparams.NetDIP, Type: kparams.IPv4, Value: net.ParseIP("8.8.8.8")}, + Category: event.Net, + Params: event.Params{ + params.NetDport: {Name: params.NetDport, Type: params.Uint16, Value: uint16(53)}, + params.NetSport: {Name: params.NetSport, Type: params.Uint16, Value: uint16(43123)}, + params.NetSIP: {Name: params.NetSIP, Type: params.IPv4, Value: net.ParseIP("127.0.0.1")}, + params.NetDIP: {Name: params.NetDIP, Type: params.IPv4, Value: net.ParseIP("8.8.8.8")}, }, } @@ -849,7 +848,7 @@ func TestNetFilter(t *testing.T) { if err != nil { t.Fatal(err) } - matches := f.Run(kevt1) + matches := f.Run(evt1) if matches != tt.matches { t.Errorf("%d. %q net filter mismatch: exp=%t got=%t", i, tt.filter, tt.matches, matches) } @@ -857,17 +856,17 @@ func TestNetFilter(t *testing.T) { } func TestRegistryFilter(t *testing.T) { - kevt := &kevent.Kevent{ - Type: ktypes.RegSetValue, + evt := &event.Event{ + Type: event.RegSetValue, Tid: 2484, PID: 859, - Category: ktypes.Registry, - Kparams: kevent.Kparams{ - kparams.RegPath: {Name: kparams.RegPath, Type: kparams.UnicodeString, Value: `HKEY_LOCAL_MACHINE\SYSTEM\Setup\Pid`}, - kparams.RegValue: {Name: kparams.RegValue, Type: kparams.Uint32, Value: uint32(10234)}, - kparams.RegValueType: {Name: kparams.RegValueType, Type: kparams.AnsiString, Value: "DWORD"}, - kparams.NTStatus: {Name: kparams.NTStatus, Type: kparams.AnsiString, Value: "success"}, - kparams.RegKeyHandle: {Name: kparams.RegKeyHandle, Type: kparams.Address, Value: uint64(18446666033449935464)}, + Category: event.Registry, + Params: event.Params{ + params.RegPath: {Name: params.RegPath, Type: params.UnicodeString, Value: `HKEY_LOCAL_MACHINE\SYSTEM\Setup\Pid`}, + params.RegValue: {Name: params.RegValue, Type: params.Uint32, Value: uint32(10234)}, + params.RegValueType: {Name: params.RegValueType, Type: params.AnsiString, Value: "DWORD"}, + params.NTStatus: {Name: params.NTStatus, Type: params.AnsiString, Value: "success"}, + params.RegKeyHandle: {Name: params.RegKeyHandle, Type: params.Address, Value: uint64(18446666033449935464)}, }, } @@ -890,7 +889,7 @@ func TestRegistryFilter(t *testing.T) { if err != nil { t.Fatal(err) } - matches := f.Run(kevt) + matches := f.Run(evt) if matches != tt.matches { t.Errorf("%d. %q registry filter mismatch: exp=%t got=%t", i, tt.filter, tt.matches, matches) } @@ -898,17 +897,17 @@ func TestRegistryFilter(t *testing.T) { } func TestImageFilter(t *testing.T) { - e1 := &kevent.Kevent{ - Type: ktypes.LoadImage, - Category: ktypes.Image, - Kparams: kevent.Kparams{ - kparams.ImagePath: {Name: kparams.ImagePath, Type: kparams.UnicodeString, Value: filepath.Join(os.Getenv("windir"), "System32", "kernel32.dll")}, - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(1023)}, - kparams.ImageCheckSum: {Name: kparams.ImageCheckSum, Type: kparams.Uint32, Value: uint32(2323432)}, - kparams.ImageBase: {Name: kparams.ImageBase, Type: kparams.Address, Value: uint64(0x7ffb313833a3)}, - kparams.ImageSignatureType: {Name: kparams.ImageSignatureType, Type: kparams.Enum, Value: uint32(1), Enum: signature.Types}, - kparams.ImageSignatureLevel: {Name: kparams.ImageSignatureLevel, Type: kparams.Enum, Value: uint32(4), Enum: signature.Levels}, - kparams.FileIsDotnet: {Name: kparams.FileIsDotnet, Type: kparams.Bool, Value: false}, + e1 := &event.Event{ + Type: event.LoadImage, + Category: event.Image, + Params: event.Params{ + params.ImagePath: {Name: params.ImagePath, Type: params.UnicodeString, Value: filepath.Join(os.Getenv("windir"), "System32", "kernel32.dll")}, + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1023)}, + params.ImageCheckSum: {Name: params.ImageCheckSum, Type: params.Uint32, Value: uint32(2323432)}, + params.ImageBase: {Name: params.ImageBase, Type: params.Address, Value: uint64(0x7ffb313833a3)}, + params.ImageSignatureType: {Name: params.ImageSignatureType, Type: params.Enum, Value: uint32(1), Enum: signature.Types}, + params.ImageSignatureLevel: {Name: params.ImageSignatureLevel, Type: params.Enum, Value: uint32(4), Enum: signature.Levels}, + params.FileIsDotnet: {Name: params.FileIsDotnet, Type: params.Bool, Value: false}, }, } @@ -949,17 +948,17 @@ func TestImageFilter(t *testing.T) { assert.Equal(t, signature.AuthenticodeLevel, sig.Level) // now exercise unsigned/unchecked signature - e2 := &kevent.Kevent{ - Type: ktypes.LoadImage, - Category: ktypes.Image, - Kparams: kevent.Kparams{ - kparams.ImagePath: {Name: kparams.ImagePath, Type: kparams.UnicodeString, Value: filepath.Join(os.Getenv("windir"), "System32", "kernel32.dll")}, - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(1023)}, - kparams.ImageCheckSum: {Name: kparams.ImageCheckSum, Type: kparams.Uint32, Value: uint32(2323432)}, - kparams.ImageBase: {Name: kparams.ImageBase, Type: kparams.Address, Value: uint64(0x7ccb313833a3)}, - kparams.ImageSignatureType: {Name: kparams.ImageSignatureType, Type: kparams.Enum, Value: uint32(0), Enum: signature.Types}, - kparams.ImageSignatureLevel: {Name: kparams.ImageSignatureLevel, Type: kparams.Enum, Value: uint32(0), Enum: signature.Levels}, - kparams.FileIsDotnet: {Name: kparams.FileIsDotnet, Type: kparams.Bool, Value: false}, + e2 := &event.Event{ + Type: event.LoadImage, + Category: event.Image, + Params: event.Params{ + params.ImagePath: {Name: params.ImagePath, Type: params.UnicodeString, Value: filepath.Join(os.Getenv("windir"), "System32", "kernel32.dll")}, + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1023)}, + params.ImageCheckSum: {Name: params.ImageCheckSum, Type: params.Uint32, Value: uint32(2323432)}, + params.ImageBase: {Name: params.ImageBase, Type: params.Address, Value: uint64(0x7ccb313833a3)}, + params.ImageSignatureType: {Name: params.ImageSignatureType, Type: params.Enum, Value: uint32(0), Enum: signature.Types}, + params.ImageSignatureLevel: {Name: params.ImageSignatureLevel, Type: params.Enum, Value: uint32(0), Enum: signature.Levels}, + params.FileIsDotnet: {Name: params.FileIsDotnet, Type: params.Bool, Value: false}, }, } @@ -992,17 +991,17 @@ func TestImageFilter(t *testing.T) { assert.NotNil(t, signature.GetSignatures().GetSignature(0x7ccb313833a3)) - e3 := &kevent.Kevent{ - Type: ktypes.LoadImage, - Category: ktypes.Image, - Kparams: kevent.Kparams{ - kparams.ImagePath: {Name: kparams.ImagePath, Type: kparams.UnicodeString, Value: "C:\\Windows\\System32\\mscorlib.dll"}, - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(1023)}, - kparams.ImageCheckSum: {Name: kparams.ImageCheckSum, Type: kparams.Uint32, Value: uint32(2323432)}, - kparams.ImageBase: {Name: kparams.ImageBase, Type: kparams.Address, Value: uint64(0xfff313833a3)}, - kparams.ImageSignatureType: {Name: kparams.ImageSignatureType, Type: kparams.Enum, Value: uint32(0), Enum: signature.Types}, - kparams.ImageSignatureLevel: {Name: kparams.ImageSignatureLevel, Type: kparams.Enum, Value: uint32(0), Enum: signature.Levels}, - kparams.FileIsDotnet: {Name: kparams.FileIsDotnet, Type: kparams.Bool, Value: true}, + e3 := &event.Event{ + Type: event.LoadImage, + Category: event.Image, + Params: event.Params{ + params.ImagePath: {Name: params.ImagePath, Type: params.UnicodeString, Value: "C:\\Windows\\System32\\mscorlib.dll"}, + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1023)}, + params.ImageCheckSum: {Name: params.ImageCheckSum, Type: params.Uint32, Value: uint32(2323432)}, + params.ImageBase: {Name: params.ImageBase, Type: params.Address, Value: uint64(0xfff313833a3)}, + params.ImageSignatureType: {Name: params.ImageSignatureType, Type: params.Enum, Value: uint32(0), Enum: signature.Types}, + params.ImageSignatureLevel: {Name: params.ImageSignatureLevel, Type: params.Enum, Value: uint32(0), Enum: signature.Levels}, + params.FileIsDotnet: {Name: params.FileIsDotnet, Type: params.Bool, Value: true}, }, } @@ -1030,7 +1029,7 @@ func TestImageFilter(t *testing.T) { } func TestPEFilter(t *testing.T) { - kevt := &kevent.Kevent{ + evt := &event.Event{ PS: &pstypes.PS{ PE: &pe.PE{ NumberOfSections: 2, @@ -1074,7 +1073,7 @@ func TestPEFilter(t *testing.T) { if err != nil { t.Fatal(err) } - matches := f.Run(kevt) + matches := f.Run(evt) if matches != tt.matches { t.Errorf("%d. %q pe filter mismatch: exp=%t got=%t", i, tt.filter, tt.matches, matches) } @@ -1082,15 +1081,15 @@ func TestPEFilter(t *testing.T) { } func TestLazyPEFilter(t *testing.T) { - kevt := &kevent.Kevent{ - Type: ktypes.LoadImage, + evt := &event.Event{ + Type: event.LoadImage, PS: &pstypes.PS{ PID: 2312, Exe: filepath.Join(os.Getenv("windir"), "notepad.exe"), }, - Kparams: kevent.Kparams{ - kparams.FileIsDLL: {Name: kparams.FileIsDLL, Type: kparams.Bool, Value: true}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, + Params: event.Params{ + params.FileIsDLL: {Name: params.FileIsDLL, Type: params.Bool, Value: true}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, }, } @@ -1121,32 +1120,32 @@ func TestLazyPEFilter(t *testing.T) { if err != nil { t.Fatal(err) } - require.Nil(t, kevt.PS.PE) - matches := f.Run(kevt) + require.Nil(t, evt.PS.PE) + matches := f.Run(evt) if matches != tt.matches { t.Errorf("%d. %q pe lazy filter mismatch: exp=%t got=%t", i, tt.filter, tt.matches, matches) } - require.NotNil(t, kevt.PS.PE) - kevt.PS.PE = nil + require.NotNil(t, evt.PS.PE) + evt.PS.PE = nil } } func TestMemFilter(t *testing.T) { - kpars := kevent.Kparams{ - kparams.MemRegionSize: {Name: kparams.MemRegionSize, Type: kparams.Uint64, Value: uint64(8192)}, - kparams.MemBaseAddress: {Name: kparams.MemBaseAddress, Type: kparams.Address, Value: uint64(1311246336000)}, - kparams.MemAllocType: {Name: kparams.MemAllocType, Type: kparams.Flags, Value: uint32(0x00001000 | 0x00002000), Flags: kevent.MemAllocationFlags}, - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.Uint32, Value: uint32(345)}, - kparams.MemProtect: {Name: kparams.MemProtect, Type: kparams.Flags, Value: uint32(0x40), Flags: kevent.MemProtectionFlags}, - kparams.MemProtectMask: {Name: kparams.MemProtectMask, Type: kparams.AnsiString, Value: "RWX"}, - kparams.MemPageType: {Name: kparams.MemPageType, Type: kparams.Enum, Value: uint32(0x1000000), Enum: processors.MemPageTypes}, - } - - kevt := &kevent.Kevent{ - Type: ktypes.VirtualAlloc, - Kparams: kpars, + pars := event.Params{ + params.MemRegionSize: {Name: params.MemRegionSize, Type: params.Uint64, Value: uint64(8192)}, + params.MemBaseAddress: {Name: params.MemBaseAddress, Type: params.Address, Value: uint64(1311246336000)}, + params.MemAllocType: {Name: params.MemAllocType, Type: params.Flags, Value: uint32(0x00001000 | 0x00002000), Flags: event.MemAllocationFlags}, + params.ProcessID: {Name: params.ProcessID, Type: params.Uint32, Value: uint32(345)}, + params.MemProtect: {Name: params.MemProtect, Type: params.Flags, Value: uint32(0x40), Flags: event.MemProtectionFlags}, + params.MemProtectMask: {Name: params.MemProtectMask, Type: params.AnsiString, Value: "RWX"}, + params.MemPageType: {Name: params.MemPageType, Type: params.Enum, Value: uint32(0x1000000), Enum: processors.MemPageTypes}, + } + + evt := &event.Event{ + Type: event.VirtualAlloc, + Params: pars, Name: "VirtualAlloc", - Category: ktypes.Mem, + Category: event.Mem, PS: &pstypes.PS{ Name: "svchost.exe", Envs: map[string]string{"ALLUSERSPROFILE": "C:\\ProgramData", "OS": "Windows_NT", "ProgramFiles(x86)": "C:\\Program Files (x86)"}, @@ -1173,7 +1172,7 @@ func TestMemFilter(t *testing.T) { if err != nil { t.Fatal(err) } - matches := f.Run(kevt) + matches := f.Run(evt) if matches != tt.matches { t.Errorf("%d. %q mem filter mismatch: exp=%t got=%t", i, tt.filter, tt.matches, matches) } @@ -1181,20 +1180,20 @@ func TestMemFilter(t *testing.T) { } func TestDNSFilter(t *testing.T) { - kevt := &kevent.Kevent{ - Type: ktypes.ReplyDNS, + evt := &event.Event{ + Type: event.ReplyDNS, Tid: 2484, PID: 859, PS: &pstypes.PS{ Name: "cmd.exe", }, - Category: ktypes.Net, - Kparams: kevent.Kparams{ - kparams.DNSName: {Name: kparams.DNSName, Type: kparams.UnicodeString, Value: "r3.o.lencr.org"}, - kparams.DNSRR: {Name: kparams.DNSRR, Type: kparams.Enum, Value: uint32(0x0001), Enum: kevent.DNSRecordTypes}, - kparams.DNSOpts: {Name: kparams.DNSOpts, Type: kparams.Flags64, Value: uint64(0x00006000), Flags: kevent.DNSOptsFlags}, - kparams.DNSRcode: {Name: kparams.DNSRcode, Type: kparams.Enum, Value: uint32(0), Enum: kevent.DNSResponseCodes}, - kparams.DNSAnswers: {Name: kparams.DNSAnswers, Type: kparams.Slice, Value: []string{"incoming.telemetry.mozilla.org", "a1887.dscq.akamai.net"}}, + Category: event.Net, + Params: event.Params{ + params.DNSName: {Name: params.DNSName, Type: params.UnicodeString, Value: "r3.o.lencr.org"}, + params.DNSRR: {Name: params.DNSRR, Type: params.Enum, Value: uint32(0x0001), Enum: event.DNSRecordTypes}, + params.DNSOpts: {Name: params.DNSOpts, Type: params.Flags64, Value: uint64(0x00006000), Flags: event.DNSOptsFlags}, + params.DNSRcode: {Name: params.DNSRcode, Type: params.Enum, Value: uint32(0), Enum: event.DNSResponseCodes}, + params.DNSAnswers: {Name: params.DNSAnswers, Type: params.Slice, Value: []string{"incoming.telemetry.mozilla.org", "a1887.dscq.akamai.net"}}, }, } @@ -1216,7 +1215,7 @@ func TestDNSFilter(t *testing.T) { if err != nil { t.Fatal(err) } - matches := f.Run(kevt) + matches := f.Run(evt) if matches != tt.matches { t.Errorf("%d. %q dns filter mismatch: exp=%t got=%t", i, tt.filter, tt.matches, matches) } @@ -1224,32 +1223,32 @@ func TestDNSFilter(t *testing.T) { } func TestThreadpoolFilter(t *testing.T) { - e := &kevent.Kevent{ - Type: ktypes.SubmitThreadpoolCallback, + e := &event.Event{ + Type: event.SubmitThreadpoolCallback, Tid: 2484, PID: 1023, CPU: 1, Seq: 2, Name: "SubmitThreadpoolCallback", Timestamp: time.Now(), - Category: ktypes.Threadpool, - Kparams: kevent.Kparams{ - kparams.ThreadpoolPoolID: {Name: kparams.ThreadpoolPoolID, Type: kparams.Address, Value: uint64(0x20f5fc02440)}, - kparams.ThreadpoolTaskID: {Name: kparams.ThreadpoolTaskID, Type: kparams.Address, Value: uint64(0x20f7ecd21f8)}, - kparams.ThreadpoolCallback: {Name: kparams.ThreadpoolCallback, Type: kparams.Address, Value: uint64(0x7ffb3138592e)}, - kparams.ThreadpoolContext: {Name: kparams.ThreadpoolContext, Type: kparams.Address, Value: uint64(0x14d0d16fed8)}, - kparams.ThreadpoolContextRip: {Name: kparams.ThreadpoolContextRip, Type: kparams.Address, Value: uint64(0x143c9b07bd0)}, - kparams.ThreadpoolSubprocessTag: {Name: kparams.ThreadpoolSubprocessTag, Type: kparams.Address, Value: uint64(0x10d)}, - kparams.ThreadpoolContextRipSymbol: {Name: kparams.ThreadpoolContextRipSymbol, Type: kparams.UnicodeString, Value: "VirtualProtect"}, - kparams.ThreadpoolContextRipModule: {Name: kparams.ThreadpoolContextRipModule, Type: kparams.UnicodeString, Value: "C:\\Windows\\System32\\kernelbase.dll"}, - kparams.ThreadpoolCallbackSymbol: {Name: kparams.ThreadpoolCallbackSymbol, Type: kparams.UnicodeString, Value: "RtlDestroyQueryDebugBuffer"}, - kparams.ThreadpoolCallbackModule: {Name: kparams.ThreadpoolCallbackModule, Type: kparams.UnicodeString, Value: "C:\\Windows\\System32\\ntdll.dll"}, - kparams.ThreadpoolTimerSubqueue: {Name: kparams.ThreadpoolTimerSubqueue, Type: kparams.Address, Value: uint64(0x1db401703e8)}, - kparams.ThreadpoolTimerDuetime: {Name: kparams.ThreadpoolTimerDuetime, Type: kparams.Uint64, Value: uint64(18446744073699551616)}, - kparams.ThreadpoolTimer: {Name: kparams.ThreadpoolTimer, Type: kparams.Address, Value: uint64(0x3e8)}, - kparams.ThreadpoolTimerPeriod: {Name: kparams.ThreadpoolTimerPeriod, Type: kparams.Uint32, Value: uint32(100)}, - kparams.ThreadpoolTimerWindow: {Name: kparams.ThreadpoolTimerWindow, Type: kparams.Uint32, Value: uint32(50)}, - kparams.ThreadpoolTimerAbsolute: {Name: kparams.ThreadpoolTimerAbsolute, Type: kparams.Bool, Value: true}, + Category: event.Threadpool, + Params: event.Params{ + params.ThreadpoolPoolID: {Name: params.ThreadpoolPoolID, Type: params.Address, Value: uint64(0x20f5fc02440)}, + params.ThreadpoolTaskID: {Name: params.ThreadpoolTaskID, Type: params.Address, Value: uint64(0x20f7ecd21f8)}, + params.ThreadpoolCallback: {Name: params.ThreadpoolCallback, Type: params.Address, Value: uint64(0x7ffb3138592e)}, + params.ThreadpoolContext: {Name: params.ThreadpoolContext, Type: params.Address, Value: uint64(0x14d0d16fed8)}, + params.ThreadpoolContextRip: {Name: params.ThreadpoolContextRip, Type: params.Address, Value: uint64(0x143c9b07bd0)}, + params.ThreadpoolSubprocessTag: {Name: params.ThreadpoolSubprocessTag, Type: params.Address, Value: uint64(0x10d)}, + params.ThreadpoolContextRipSymbol: {Name: params.ThreadpoolContextRipSymbol, Type: params.UnicodeString, Value: "VirtualProtect"}, + params.ThreadpoolContextRipModule: {Name: params.ThreadpoolContextRipModule, Type: params.UnicodeString, Value: "C:\\Windows\\System32\\kernelbase.dll"}, + params.ThreadpoolCallbackSymbol: {Name: params.ThreadpoolCallbackSymbol, Type: params.UnicodeString, Value: "RtlDestroyQueryDebugBuffer"}, + params.ThreadpoolCallbackModule: {Name: params.ThreadpoolCallbackModule, Type: params.UnicodeString, Value: "C:\\Windows\\System32\\ntdll.dll"}, + params.ThreadpoolTimerSubqueue: {Name: params.ThreadpoolTimerSubqueue, Type: params.Address, Value: uint64(0x1db401703e8)}, + params.ThreadpoolTimerDuetime: {Name: params.ThreadpoolTimerDuetime, Type: params.Uint64, Value: uint64(18446744073699551616)}, + params.ThreadpoolTimer: {Name: params.ThreadpoolTimer, Type: params.Address, Value: uint64(0x3e8)}, + params.ThreadpoolTimerPeriod: {Name: params.ThreadpoolTimerPeriod, Type: params.Uint32, Value: uint32(100)}, + params.ThreadpoolTimerWindow: {Name: params.ThreadpoolTimerWindow, Type: params.Uint32, Value: uint32(50)}, + params.ThreadpoolTimerAbsolute: {Name: params.ThreadpoolTimerAbsolute, Type: params.Bool, Value: true}, }, } @@ -1292,15 +1291,15 @@ func TestInterpolateFields(t *testing.T) { var tests = []struct { original string interpolated string - evts []*kevent.Kevent + evts []*event.Event }{ { - original: "Credential discovery via %ps.name (%kevt.arg[cmdline]) and user %ps.sid", + original: "Credential discovery via %ps.name (%evt.arg[cmdline]) and user %ps.sid", interpolated: "Credential discovery via VaultCmd.exe (VaultCmd.exe /listcreds:Windows Credentials /all) and user LOCAL\\tor", - evts: []*kevent.Kevent{ + evts: []*event.Event{ { - Type: ktypes.CreateProcess, - Category: ktypes.Process, + Type: event.CreateProcess, + Category: event.Process, Name: "CreateProcess", PID: 1023, PS: &pstypes.PS{ @@ -1308,19 +1307,19 @@ func TestInterpolateFields(t *testing.T) { Ppid: 345, SID: "LOCAL\\tor", }, - Kparams: kevent.Kparams{ - kparams.Cmdline: {Name: kparams.Cmdline, Type: kparams.UnicodeString, Value: `VaultCmd.exe /listcreds:Windows Credentials /all`}, + Params: event.Params{ + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: `VaultCmd.exe /listcreds:Windows Credentials /all`}, }, }, }, }, { - original: "Credential discovery via %ps.name and pid %kevt.pid", + original: "Credential discovery via %ps.name and pid %evt.pid", interpolated: "Credential discovery via N/A and pid 1023", - evts: []*kevent.Kevent{ + evts: []*event.Event{ { - Type: ktypes.CreateProcess, - Category: ktypes.Process, + Type: event.CreateProcess, + Category: event.Process, Name: "CreateProcess", PID: 1023, }, @@ -1329,14 +1328,14 @@ func TestInterpolateFields(t *testing.T) { { original: "Suspicious thread start module %thread.start_address.module", interpolated: "Suspicious thread start module C:\\Windows\\System32\\vault.dll", - evts: []*kevent.Kevent{ + evts: []*event.Event{ { - Type: ktypes.CreateThread, - Category: ktypes.Thread, + Type: event.CreateThread, + Category: event.Thread, Name: "CreateThread", PID: 1023, - Kparams: kevent.Kparams{ - kparams.StartAddressModule: {Name: kparams.StartAddressModule, Type: kparams.UnicodeString, Value: "C:\\Windows\\System32\\vault.dll"}, + Params: event.Params{ + params.StartAddressModule: {Name: params.StartAddressModule, Type: params.UnicodeString, Value: "C:\\Windows\\System32\\vault.dll"}, }, }, }, @@ -1349,10 +1348,10 @@ and subsequently write the %2.file.path dump file to the disk devic and read the memory of the Local Security And Authority Subsystem Service and subsequently write the C:\Users eo\Temp\lsass.dump dump file to the disk device`, - evts: []*kevent.Kevent{ + evts: []*event.Event{ { - Type: ktypes.OpenProcess, - Category: ktypes.Process, + Type: event.OpenProcess, + Category: event.Process, Name: "OpenProcess", PID: 1023, PS: &pstypes.PS{ @@ -1362,12 +1361,12 @@ eo\Temp\lsass.dump dump file to the disk device`, }, }, { - Type: ktypes.WriteFile, - Category: ktypes.File, + Type: event.WriteFile, + Category: event.File, Name: "WriteFile", PID: 1023, - Kparams: kevent.Kparams{ - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Users\neo\\Temp\\lsass.dump"}, + Params: event.Params{ + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Users\neo\\Temp\\lsass.dump"}, }, PS: &pstypes.PS{ Name: "taskmgr.exe", @@ -1385,10 +1384,10 @@ and subsequently write the %2.file.path dump file to the disk devic and read the memory of the Local Security And Authority Subsystem Service and subsequently write the C:\Users eo\Temp\lsass.dump dump file to the disk device`, - evts: []*kevent.Kevent{ + evts: []*event.Event{ { - Type: ktypes.OpenProcess, - Category: ktypes.Process, + Type: event.OpenProcess, + Category: event.Process, Name: "OpenProcess", PID: 1023, PS: &pstypes.PS{ @@ -1398,12 +1397,12 @@ eo\Temp\lsass.dump dump file to the disk device`, }, }, { - Type: ktypes.WriteFile, - Category: ktypes.File, + Type: event.WriteFile, + Category: event.File, Name: "WriteFile", PID: 1023, - Kparams: kevent.Kparams{ - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Users\neo\\Temp\\lsass.dump"}, + Params: event.Params{ + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Users\neo\\Temp\\lsass.dump"}, }, PS: &pstypes.PS{ Name: "taskmgr.exe", @@ -1428,21 +1427,21 @@ func BenchmarkFilterRun(b *testing.B) { f := New(`ps.name = 'mimikatz.exe' or ps.name contains 'svc'`, cfg) require.NoError(b, f.Compile()) - kpars := kevent.Kparams{ - kparams.Cmdline: {Name: kparams.Cmdline, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\svchost.exe -k RPCSS"}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.AnsiString, Value: "svchost.exe"}, - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.Uint32, Value: uint32(1234)}, - kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.Uint32, Value: uint32(345)}, + pars := event.Params{ + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\svchost.exe -k RPCSS"}, + params.ProcessName: {Name: params.ProcessName, Type: params.AnsiString, Value: "svchost.exe"}, + params.ProcessID: {Name: params.ProcessID, Type: params.Uint32, Value: uint32(1234)}, + params.ProcessParentID: {Name: params.ProcessParentID, Type: params.Uint32, Value: uint32(345)}, } - kevt := &kevent.Kevent{ - Type: ktypes.CreateProcess, - Kparams: kpars, - Name: "CreateProcess", + evt := &event.Event{ + Type: event.CreateProcess, + Params: pars, + Name: "CreateProcess", } for i := 0; i < b.N; i++ { - f.Run(kevt) + f.Run(evt) } } diff --git a/pkg/filter/filter_windows.go b/pkg/filter/filter_windows.go index 12cc19b17..a38b8b06d 100644 --- a/pkg/filter/filter_windows.go +++ b/pkg/filter/filter_windows.go @@ -51,7 +51,7 @@ func New(expr string, config *config.Config, options ...Option) Filter { } accessors := []Accessor{ // general event parameters - newKevtAccessor(), + newEventAccessor(), // process state and parameters newPSAccessor(opts.psnap), // PE metadata diff --git a/pkg/filter/ql/error_test.go b/pkg/filter/ql/error_test.go index 288eee27b..ca131c384 100644 --- a/pkg/filter/ql/error_test.go +++ b/pkg/filter/ql/error_test.go @@ -25,7 +25,7 @@ import ( ) func TestParseError(t *testing.T) { - expr := `kevt.name in ('RegCreateKey', 'RegDeleteKey', 'RegSetValue', 'RegDeleteValue') + expr := `evt.name in ('RegCreateKey', 'RegDeleteKey', 'RegSetValue', 'RegDeleteValue') and registry.key.name icontains ( @@ -54,7 +54,7 @@ func TestParseError(t *testing.T) { ( 'user shell folders\\startup' )` - expected := `kevt.name in ('RegCreateKey', 'RegDeleteKey', 'RegSetValue', 'RegDeleteValue') + expected := `evt.name in ('RegCreateKey', 'RegDeleteKey', 'RegSetValue', 'RegDeleteValue') and registry.key.name icontains ( diff --git a/pkg/filter/ql/function.go b/pkg/filter/ql/function.go index 799aac899..e4c0c5982 100644 --- a/pkg/filter/ql/function.go +++ b/pkg/filter/ql/function.go @@ -308,7 +308,7 @@ func (f *Foreach) Desc() functions.FunctionDesc { "$mem": true, "$handle": true, "$dns": true, - "$kevt": true, + "$evt": true, } if reserved[v] { @@ -349,7 +349,7 @@ func (f *Foreach) Desc() functions.FunctionDesc { return fmt.Errorf("unused bound variable %s in predicate %q", v, e) } - var fieldRegexp = regexp.MustCompile(`(ps|pe|file|image|thread|registry|net|mem|handle|dns|kevt)\.[a-zA-Z0-9_.$]+`) + var fieldRegexp = regexp.MustCompile(`(ps|pe|file|image|thread|registry|net|mem|handle|dns|evt)\.[a-zA-Z0-9_.$]+`) matches = fieldRegexp.FindAllStringSubmatch(e, -1) if len(args) > 3 { diff --git a/pkg/filter/ql/literal.go b/pkg/filter/ql/literal.go index 29c263b62..78ebf75ed 100644 --- a/pkg/filter/ql/literal.go +++ b/pkg/filter/ql/literal.go @@ -19,9 +19,8 @@ package ql import ( + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/filter/fields" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/rabbitstack/fibratus/pkg/util/hashers" "golang.org/x/sys/windows" "net" @@ -282,12 +281,12 @@ type SequenceExpr struct { Alias string buckets map[uint32]bool - ktypes []ktypes.Ktype + types []event.Type } func (e *SequenceExpr) init() { e.buckets = make(map[uint32]bool) - e.ktypes = make([]ktypes.Ktype, 0) + e.types = make([]event.Type, 0) e.BoundFields = make([]*BoundFieldLiteral, 0) } @@ -342,8 +341,8 @@ func (e *SequenceExpr) walk() { if name == fields.KevtName || name == fields.KevtCategory { for _, v := range values { e.buckets[hashers.FnvUint32([]byte(v))] = true - if ktyp := ktypes.KeventNameToKtype(v); ktyp.Exists() { - e.ktypes = append(e.ktypes, ktyp) + if etype := event.NameToType(v); etype.Exists() { + e.types = append(e.types, etype) } } } @@ -354,8 +353,8 @@ func (e *SequenceExpr) walk() { // the event type filter fields defined in the expression. We permit the expression // to be evaluated when the incoming event type or category pertains to the one // defined in the field literal. -func (e *SequenceExpr) IsEvaluable(kevt *kevent.Kevent) bool { - return e.buckets[kevt.Type.Hash()] || e.buckets[kevt.Category.Hash()] +func (e *SequenceExpr) IsEvaluable(evt *event.Event) bool { + return e.buckets[evt.Type.Hash()] || e.buckets[evt.Category.Hash()] } // HasBoundFields determines if this sequence expression references any bound field. @@ -384,11 +383,11 @@ func (s *Sequence) init() { // is guaranteed guids := make(map[windows.GUID]bool) for _, expr := range s.Expressions { - for _, k := range expr.ktypes { - if k.CanArriveOutOfOrder() { + for _, etype := range expr.types { + if etype.CanArriveOutOfOrder() { s.IsUnordered = true } - guids[k.GUID()] = true + guids[etype.GUID()] = true } } diff --git a/pkg/filter/ql/parser_test.go b/pkg/filter/ql/parser_test.go index ddf6afcc0..30105584d 100644 --- a/pkg/filter/ql/parser_test.go +++ b/pkg/filter/ql/parser_test.go @@ -64,9 +64,9 @@ func TestParser(t *testing.T) { {expr: `ps.envs imatches 'C:\\Program Files'`}, {expr: `ps.pid[1] = 'svchost.exe'`, err: errors.New("ps.pid[1] = 'svchost.exe'\n╭──────^\n|\n|\n╰─────────────────── expected field without argument")}, {expr: `ps.envs[ProgramFiles = 'svchost.exe'`, err: errors.New("ps.envs[ProgramFiles = 'svchost.exe'\n╭───────────────────^\n|\n|\n╰─────────────────── expected ]")}, - {expr: `kevt.arg = 'svchost.exe'`, err: errors.New("kevt.arg = 'svchost.exe'\n╭───────^\n|\n|\n╰─────────────────── expected field argument")}, - {expr: `kevt.arg[name] = 'svchost.exe'`}, - {expr: `kevt.arg[Name$] = 'svchost.exe'`, err: errors.New("kevt.arg[Name$] = 'svchost.exe'\n╭────────^\n|\n|\n╰─────────────────── expected a valid field argument matching the pattern [a-z0-9_]+")}, + {expr: `evt.arg = 'svchost.exe'`, err: errors.New("evt.arg = 'svchost.exe'\n╭───────^\n|\n|\n╰─────────────────── expected field argument")}, + {expr: `evt.arg[name] = 'svchost.exe'`}, + {expr: `evt.arg[Name$] = 'svchost.exe'`, err: errors.New("evt.arg[Name$] = 'svchost.exe'\n╭────────^\n|\n|\n╰─────────────────── expected a valid field argument matching the pattern [a-z0-9_]+")}, {expr: `ps.ancestor[0] = 'svchost.exe'`}, {expr: `ps.ancestor[l0l] = 'svchost.exe'`, err: errors.New("ps.ancestor[l0l] = 'svchost.exe'\n╭───────────^\n|\n|\n╰─────────────────── expected a valid field argument matching the pattern [0-9]+")}, } @@ -145,43 +145,43 @@ func TestExpandMacros(t *testing.T) { err error }{ { - config.FiltersWithMacros(map[string]*config.Macro{"spawn_process": {Expr: "kevt.name = 'CreateProcess'"}}), + config.FiltersWithMacros(map[string]*config.Macro{"spawn_process": {Expr: "evt.name = 'CreateProcess'"}}), "spawn_process and ps.name in ('cmd.exe', 'powershell.exe')", - "kevt.name = CreateProcess AND ps.name IN (cmd.exe, powershell.exe)", + "evt.name = CreateProcess AND ps.name IN (cmd.exe, powershell.exe)", nil, }, { - config.FiltersWithMacros(map[string]*config.Macro{"span_process": {Expr: "kevt.name = 'CreateProcess'"}}), + config.FiltersWithMacros(map[string]*config.Macro{"span_process": {Expr: "evt.name = 'CreateProcess'"}}), "spawn_process and ps.name in ('cmd.exe', 'powershell.exe')", "", errors.New("expected field, string, number, bool, ip, function, pattern binding"), }, { - config.FiltersWithMacros(map[string]*config.Macro{"spawn_process": {Expr: "kevt.name = 'CreateProcess'"}, "command_clients": {List: []string{"cmd.exe", "pwsh.exe"}}}), + config.FiltersWithMacros(map[string]*config.Macro{"spawn_process": {Expr: "evt.name = 'CreateProcess'"}, "command_clients": {List: []string{"cmd.exe", "pwsh.exe"}}}), "spawn_process and ps.name in command_clients", - "kevt.name = CreateProcess AND ps.name IN (cmd.exe, pwsh.exe)", + "evt.name = CreateProcess AND ps.name IN (cmd.exe, pwsh.exe)", nil, }, { - config.FiltersWithMacros(map[string]*config.Macro{"spawn_process": {Expr: "kevt.nnname = 'CreateProcess'"}, "command_clients": {List: []string{"cmd.exe", "pwsh.exe"}}}), + config.FiltersWithMacros(map[string]*config.Macro{"spawn_process": {Expr: "evt.nnname = 'CreateProcess'"}, "command_clients": {List: []string{"cmd.exe", "pwsh.exe"}}}), "spawn_process and ps.name in command_clients", "", errors.New("syntax error in \"spawn_process\" macro. expected field, string, number, bool, ip, function, pattern binding"), }, { config.FiltersWithMacros(map[string]*config.Macro{ - "rename": {Expr: "kevt.name = 'RenameFile'"}, - "remove": {Expr: "kevt.name = 'DeleteFile'"}, + "rename": {Expr: "evt.name = 'RenameFile'"}, + "remove": {Expr: "evt.name = 'DeleteFile'"}, "modify": {Expr: "rename or remove"}, "wcm_files": {List: []string{"?:\\Users\\*\\AppData\\*\\Microsoft\\Credentials\\*"}}}), "(modify) and file.name imatches wcm_files", - "(kevt.name = RenameFile OR kevt.name = DeleteFile) AND file.name IMATCHES (?:\\Users\\*\\AppData\\*\\Microsoft\\Credentials\\*)", + "(evt.name = RenameFile OR evt.name = DeleteFile) AND file.name IMATCHES (?:\\Users\\*\\AppData\\*\\Microsoft\\Credentials\\*)", nil, }, { config.FiltersWithMacros(map[string]*config.Macro{ - "rename": {Expr: "kevt.name = 'RenameFile'"}, - "remove": {Expr: "kevt.name = 'DeleteFile'"}, + "rename": {Expr: "evt.name = 'RenameFile'"}, + "remove": {Expr: "evt.name = 'DeleteFile'"}, "modify": {Expr: "rename or remove"}}), "entropy(file.name) > 0.22 and ren", "", @@ -189,22 +189,22 @@ func TestExpandMacros(t *testing.T) { }, { config.FiltersWithMacros(map[string]*config.Macro{ - "rename": {Expr: "kevt.name = 'RenameFile'"}, - "remove": {Expr: "kevt.name = 'DeleteFile'"}, + "rename": {Expr: "evt.name = 'RenameFile'"}, + "remove": {Expr: "evt.name = 'DeleteFile'"}, "modify": {Expr: "rename or remove"}}), "entropy(file.name) > 0.22 and rename", - "entropy(file.name) > 2.2e-01 AND kevt.name = RenameFile", + "entropy(file.name) > 2.2e-01 AND evt.name = RenameFile", nil, }, { config.FiltersWithMacros(map[string]*config.Macro{ - "rename": {Expr: "kevt.name = 'RenameFile'"}, - "remove": {Expr: "kevt.name = 'DeleteFile'"}, - "create": {Expr: "kevt.name = 'CreateFile' and file.operation = 'create'"}, + "rename": {Expr: "evt.name = 'RenameFile'"}, + "remove": {Expr: "evt.name = 'DeleteFile'"}, + "create": {Expr: "evt.name = 'CreateFile' and file.operation = 'create'"}, "modify": {Expr: "rename or remove"}, "change_fs": {Expr: "modify or (create)"}}), "change_fs", - "kevt.name = RenameFile OR kevt.name = DeleteFile OR (kevt.name = CreateFile AND file.operation = create)", + "evt.name = RenameFile OR evt.name = DeleteFile OR (evt.name = CreateFile AND file.operation = create)", nil, }, } @@ -231,40 +231,40 @@ func TestParseSequence(t *testing.T) { isConstrained bool }{ { - `kevt.name = 'CreateProcess'| - |kevt.name = 'CreateFile'| + `evt.name = 'CreateProcess'| + |evt.name = 'CreateFile'| `, errors.New("expected |"), time.Duration(0), false, }, { - `|kevt.name = 'CreateProcess' - kevt.name = 'CreateFile'| + `|evt.name = 'CreateProcess' + evt.name = 'CreateFile'| `, errors.New("expected operator, ')', ',', '|'"), time.Duration(0), false, }, { - `|kevt.name = 'CreateProcess'| - |kevt.name = 'CreateFile' + `|evt.name = 'CreateProcess'| + |evt.name = 'CreateFile' `, errors.New("expected |"), time.Duration(0), false, }, { - `|kevt.name = 'CreateProcess'| - |kevt.name = 'CreateFile'| + `|evt.name = 'CreateProcess'| + |evt.name = 'CreateFile'| `, nil, time.Duration(0), false, }, { - `|kevt.name = 'CreateProcess'| by ps.exe - |kevt.name = 'CreateFile'| by file.name + `|evt.name = 'CreateProcess'| by ps.exe + |evt.name = 'CreateFile'| by file.name `, nil, time.Duration(0), @@ -273,8 +273,8 @@ func TestParseSequence(t *testing.T) { { `by ps.pid - |kevt.name = 'CreateProcess'| - |kevt.name = 'CreateFile'| + |evt.name = 'CreateProcess'| + |evt.name = 'CreateFile'| `, nil, time.Duration(0), @@ -283,8 +283,8 @@ func TestParseSequence(t *testing.T) { { `by ps.pid - |kevt.name = 'CreateProcess'| by ps.pid - |kevt.name = 'CreateFile'| + |evt.name = 'CreateProcess'| by ps.pid + |evt.name = 'CreateFile'| `, errors.New("all expressions require the 'by' statement"), time.Duration(0), @@ -292,8 +292,8 @@ func TestParseSequence(t *testing.T) { }, { - `|kevt.name = 'CreateProcess'| by ps.pid - |kevt.name = 'CreateFile'| + `|evt.name = 'CreateProcess'| by ps.pid + |evt.name = 'CreateFile'| `, errors.New("all expressions require the 'by' statement"), time.Duration(0), @@ -302,8 +302,8 @@ func TestParseSequence(t *testing.T) { { `maxspan 20s - |kevt.name = 'CreateProcess'| by ps.pid - |kevt.name = 'CreateFile'| by ps.pid + |evt.name = 'CreateProcess'| by ps.pid + |evt.name = 'CreateFile'| by ps.pid `, nil, time.Second * 20, @@ -312,8 +312,8 @@ func TestParseSequence(t *testing.T) { { `maxspan 30s - |kevt.name = 'CreateProcess'| - |kevt.name = 'CreateFile'| + |evt.name = 'CreateProcess'| + |evt.name = 'CreateFile'| `, nil, time.Second * 30, @@ -322,8 +322,8 @@ func TestParseSequence(t *testing.T) { { `maxspan 30s - |kevt.name = 'CreateProcess'| as e1 - |kevt.name = 'CreateFile' and $e1.ps.name = file.name | + |evt.name = 'CreateProcess'| as e1 + |evt.name = 'CreateFile' and $e1.ps.name = file.name | `, nil, time.Second * 30, @@ -332,8 +332,8 @@ func TestParseSequence(t *testing.T) { { `maxspan 30s - |kevt.name = 'CreateProcess'| as e1 - |kevt.name = 'CreateFile' and $e1.ps.ame = file.name | + |evt.name = 'CreateProcess'| as e1 + |evt.name = 'CreateFile' and $e1.ps.ame = file.name | `, errors.New("expected field after bound ref"), time.Second * 30, @@ -342,8 +342,8 @@ func TestParseSequence(t *testing.T) { { `maxspan 40h - |kevt.name = 'CreateProcess'| as e1 - |kevt.name = 'CreateFile' and $e1.ps.ame = file.name | + |evt.name = 'CreateProcess'| as e1 + |evt.name = 'CreateFile' and $e1.ps.ame = file.name | `, errors.New("maximum span 40h0m0s cannot be greater than 4h"), time.Hour * 40, @@ -353,8 +353,8 @@ func TestParseSequence(t *testing.T) { `by ps.uuid maxspan 2m - |kevt.name = 'CreateProcess'| by ps.child.uuid - |kevt.name = 'CreateFile'| by ps.uuid + |evt.name = 'CreateProcess'| by ps.child.uuid + |evt.name = 'CreateFile'| by ps.uuid `, errors.New("sequence mixes global and per-expression 'by' statements"), time.Minute * 2, @@ -388,51 +388,51 @@ func TestIsSequenceUnordered(t *testing.T) { isUnordered bool }{ { - `|kevt.name = 'CreateProcess'| by ps.uuid - |kevt.name = 'OpenProcess'| by ps.uuid + `|evt.name = 'CreateProcess'| by ps.uuid + |evt.name = 'OpenProcess'| by ps.uuid `, true, }, { - `|kevt.name = 'CreateProcess'| - |kevt.name = 'CreateFile'| + `|evt.name = 'CreateProcess'| + |evt.name = 'CreateFile'| `, false, }, { - `|kevt.name = 'CreateProcess'| - |kevt.name = 'UnmapViewFile'| - |kevt.name = 'LoadImage'| + `|evt.name = 'CreateProcess'| + |evt.name = 'UnmapViewFile'| + |evt.name = 'LoadImage'| `, false, }, { - `|kevt.name = 'CreateProcess'| - |kevt.name = 'SetThreadContext'| + `|evt.name = 'CreateProcess'| + |evt.name = 'SetThreadContext'| `, true, }, { - `|kevt.name = 'OpenThread'| by ps.uuid - |kevt.name = 'OpenProcess'| by ps.uuid + `|evt.name = 'OpenThread'| by ps.uuid + |evt.name = 'OpenProcess'| by ps.uuid `, false, }, { - `|kevt.name = 'OpenThread' or kevt.name = 'OpenProcess'| by ps.uuid - |kevt.name = 'SetThreadContext'| by ps.uuid + `|evt.name = 'OpenThread' or evt.name = 'OpenProcess'| by ps.uuid + |evt.name = 'SetThreadContext'| by ps.uuid `, false, }, { - `|kevt.name = 'RegSetValue'| by ps.uuid - |kevt.name = 'SetThreadContext'| by ps.uuid + `|evt.name = 'RegSetValue'| by ps.uuid + |evt.name = 'SetThreadContext'| by ps.uuid `, true, }, { - `|kevt.name = 'RegSetValue'| by ps.uuid - |kevt.name = 'RegDeleteValue'| by ps.uuid + `|evt.name = 'RegSetValue'| by ps.uuid + |evt.name = 'RegDeleteValue'| by ps.uuid `, false, }, diff --git a/pkg/filter/util.go b/pkg/filter/util.go index 8c44fe02e..04dc37dd3 100644 --- a/pkg/filter/util.go +++ b/pkg/filter/util.go @@ -19,10 +19,9 @@ package filter import ( + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/filter/fields" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/rabbitstack/fibratus/pkg/util/loldrivers" "github.com/rabbitstack/fibratus/pkg/util/signature" "github.com/rabbitstack/fibratus/pkg/util/va" @@ -31,16 +30,16 @@ import ( // isLOLDriver interacts with the loldrivers client to determine // whether the loaded/dropped driver is malicious or vulnerable. -func isLOLDriver(f fields.Field, e *kevent.Kevent) (kparams.Value, error) { +func isLOLDriver(f fields.Field, e *event.Event) (params.Value, error) { var filename string - if e.Category == ktypes.File { - filename = e.GetParamAsString(kparams.FilePath) + if e.Category == event.File { + filename = e.GetParamAsString(params.FilePath) } else { - filename = e.GetParamAsString(kparams.ImagePath) + filename = e.GetParamAsString(params.ImagePath) } - isDriver := filepath.Ext(filename) == ".sys" || e.Kparams.TryGetBool(kparams.FileIsDriver) + isDriver := filepath.Ext(filename) == ".sys" || e.Params.TryGetBool(params.FileIsDriver) if !isDriver { return nil, nil } diff --git a/pkg/handle/snapshotter.go b/pkg/handle/snapshotter.go index 047a17210..172905d3c 100644 --- a/pkg/handle/snapshotter.go +++ b/pkg/handle/snapshotter.go @@ -33,9 +33,9 @@ import ( "unsafe" "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" htypes "github.com/rabbitstack/fibratus/pkg/handle/types" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" log "github.com/sirupsen/logrus" ) @@ -69,10 +69,10 @@ type SnapshotBuildCompleted func(total uint64, named uint64) type Snapshotter interface { // Write updates the snapshotter state by storing a new entry for the inbound create handle event. It also notifies // the registered callback that a new handle has been created. - Write(kevt *kevent.Kevent) error + Write(evt *event.Event) error // Remove destroys the handle state for the specified handle object. The removal callback is triggered when an item // is deleted from the store. - Remove(kevt *kevent.Kevent) error + Remove(evt *event.Event) error // FindHandles returns a list of all known handles for the specified process identifier. FindHandles(pid uint32) ([]htypes.Handle, error) // FindByObject returns the handle for the given handle object reference. @@ -127,7 +127,7 @@ func NewSnapshotter(config *config.Config, fn SnapshotBuildCompleted) Snapshotte return s } -// NewFromKcap builds the handle snapshotter from kcap state. +// NewFromKcap builds the handle snapshotter from cap state. func NewFromKcap(handles []htypes.Handle) Snapshotter { s := &snapshotter{ handlesByObject: make(map[uint64]htypes.Handle), @@ -397,12 +397,12 @@ func (s *snapshotter) GetSnapshot() []htypes.Handle { return handles } -func (s *snapshotter) Write(e *kevent.Kevent) error { +func (s *snapshotter) Write(e *event.Event) error { if !e.IsCreateHandle() { return fmt.Errorf("expected CreateHandle event but got %s", e.Type) } h := unwrapHandle(e) - obj, err := e.Kparams.GetUint64(kparams.HandleObject) + obj, err := e.Params.GetUint64(params.HandleObject) if err != nil { return err } @@ -412,11 +412,11 @@ func (s *snapshotter) Write(e *kevent.Kevent) error { return nil } -func (s *snapshotter) Remove(e *kevent.Kevent) error { +func (s *snapshotter) Remove(e *event.Event) error { if !e.IsCloseHandle() { return fmt.Errorf("expected CloseHandle event but got %s", e.Type) } - obj, err := e.Kparams.GetUint64(kparams.HandleObject) + obj, err := e.Params.GetUint64(params.HandleObject) if err != nil { return err } @@ -433,10 +433,10 @@ func (s *snapshotter) Close() error { return nil } -func unwrapHandle(e *kevent.Kevent) htypes.Handle { +func unwrapHandle(e *event.Event) htypes.Handle { h := htypes.Handle{} - h.Type = e.GetParamAsString(kparams.HandleObjectTypeID) - h.Object, _ = e.Kparams.GetUint64(kparams.HandleObject) - h.Name, _ = e.Kparams.GetString(kparams.HandleObjectName) + h.Type = e.GetParamAsString(params.HandleObjectTypeID) + h.Object, _ = e.Params.GetUint64(params.HandleObject) + h.Name, _ = e.Params.GetString(params.HandleObjectName) return h } diff --git a/pkg/handle/snapshotter_mock.go b/pkg/handle/snapshotter_mock.go index 906712671..8b7241b01 100644 --- a/pkg/handle/snapshotter_mock.go +++ b/pkg/handle/snapshotter_mock.go @@ -22,8 +22,8 @@ package handle import ( + "github.com/rabbitstack/fibratus/pkg/event" htypes "github.com/rabbitstack/fibratus/pkg/handle/types" - "github.com/rabbitstack/fibratus/pkg/kevent" "github.com/stretchr/testify/mock" ) @@ -33,14 +33,14 @@ type SnapshotterMock struct { } // Write method -func (s *SnapshotterMock) Write(kevt *kevent.Kevent) error { - args := s.Called(kevt) +func (s *SnapshotterMock) Write(evt *event.Event) error { + args := s.Called(evt) return args.Error(0) } // Remove method -func (s *SnapshotterMock) Remove(kevt *kevent.Kevent) error { - args := s.Called(kevt) +func (s *SnapshotterMock) Remove(evt *event.Event) error { + args := s.Called(evt) return args.Error(0) } diff --git a/pkg/handle/types/marshaller.go b/pkg/handle/types/marshaller.go index 3c84dbfc4..5e450b260 100644 --- a/pkg/handle/types/marshaller.go +++ b/pkg/handle/types/marshaller.go @@ -55,7 +55,7 @@ func (h Handle) Offset() uint16 { return offset } -// Marshal dumps the state of the handle to byte slice that is suitable for serializing to kcap file. +// Marshal dumps the state of the handle to byte slice that is suitable for serializing to cap file. func (h *Handle) Marshal() []byte { b := make([]byte, 0) diff --git a/pkg/handle/types/types.go b/pkg/handle/types/types.go index 3af407e4f..2d98349f2 100644 --- a/pkg/handle/types/types.go +++ b/pkg/handle/types/types.go @@ -87,7 +87,7 @@ func (h Handle) Len() int { return l } -// NewFromKcap restores handle state from the kcap buffer. +// NewFromKcap restores handle state from the cap buffer. func NewFromKcap(buf []byte) (Handle, error) { h := Handle{} err := h.Unmarshal(buf) diff --git a/pkg/kevent/README.md b/pkg/kevent/README.md deleted file mode 100644 index e053887dd..000000000 --- a/pkg/kevent/README.md +++ /dev/null @@ -1,27 +0,0 @@ -`Kevent` is the fundamental data structure for transporting kernel events. - -Each kernel event structure contains a series of canonical fields that describe the nature of the event such as its name, the process identifier that generated the event and such. The following is the list of all canonical fields. - -- **Sequence** is a monotonically increasing integer value that uniquely identifies an event. The sequence value is guaranteed to increment monotonically as long as the machine is not rebooted. After the restart, the sequence is restored to the zero value. -- **PID** represents the process identifier that triggered the kernel event. -- **TID** is the thread identifier connected to the kernel event. -- **CPU** designates the logical CPU core on which the event was originated. -- **Name** is the human-readable event name such as `CreateProcess` or `RegOpenKey`. -- **Timestamp** denotes the timestamp expressed in nanosecond precision as the instant the event occurred. -- **Category** designates the category to which the event pertains, e.g. `file` or `thread`. Each particular category is explained thoroughly in the next - sections. -- **Description** is a short explanation about the purpose of the event. For example, `CreateFile` kernel event creates or opens a file, directory, I/O device, pipe, console buffer or other block/pseudo device. -- **Host** represents the host name where the event was produced. - -### Parameters - -Also called as `kparams` in Fibratus parlance, contain each of the event's parameters. Internally, they are modeled as a collection of key/value pairs where the key is mapped to the structure consisting of parameter name, parameter type and the value. An example of the parameter tuple could be the `dip` parameter -that denotes a destination IP address with value `172.17.0.2` and therefore `IPv4` type. Additionally, parameter types can be scalar values, strings, slices, enumerations, and timestamps among others. - -### Process state - -Each event stores the process state that represents an extended information about the process including its allocated resources such as handles, dynamically-linked libraries, exported environment variables and other attributes. The process state internals are thoroughly explained in the [Process](/kevents/process) events section. - -### Metadata - -Metadata are an arbitrary sequence of tags in form of key/value pairs that you can squash into the event on behalf of [transformers](/transformers/introduction). A tag can be virtually any string data that you find meaningful to either identify the event or apply filtering/grouping once event is persisted in the data store. diff --git a/pkg/kevent/kparam_test.go b/pkg/kevent/kparam_test.go deleted file mode 100644 index b7b168f7b..000000000 --- a/pkg/kevent/kparam_test.go +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2019-2020 by Nedim Sabic Sabic - * https://www.fibratus.io - * All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package kevent - -import ( - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "testing" -) - -func TestKparams(t *testing.T) { - kpars := Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(18446738026482168384)}, - kparams.ThreadID: {Name: kparams.ThreadID, Type: kparams.Uint32, Value: uint32(1484)}, - kparams.FileCreateOptions: {Name: kparams.FileCreateOptions, Type: kparams.Uint32, Value: uint32(1223456)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\kernel32.dll"}, - kparams.FileShareMask: {Name: kparams.FileShareMask, Type: kparams.Uint32, Value: uint32(5)}, - } - - assert.True(t, kpars.Contains(kparams.FileObject)) - assert.False(t, kpars.Contains(kparams.FileOffset)) - - filename, err := kpars.GetString(kparams.FilePath) - require.NoError(t, err) - assert.Equal(t, "\\Device\\HarddiskVolume2\\Windows\\system32\\kernel32.dll", filename) - - _, err = kpars.GetString(kparams.FileObject) - require.Error(t, err) - - assert.Equal(t, 5, kpars.Len()) - - kpars.Remove(kparams.ThreadID) - - assert.False(t, kpars.Contains(kparams.ThreadID)) - assert.Equal(t, 4, kpars.Len()) - - require.NoError(t, kpars.Set(kparams.FileShareMask, uint32(5), kparams.Enum)) - - require.NoError(t, kpars.SetValue(kparams.FilePath, "\\Device\\HarddiskVolume2\\Windows\\system32\\KERNEL32.dll")) - filename1, err := kpars.GetString(kparams.FilePath) - require.NoError(t, err) - assert.Equal(t, "\\Device\\HarddiskVolume2\\Windows\\system32\\KERNEL32.dll", filename1) -} diff --git a/pkg/kevent/stackwalk_test.go b/pkg/kevent/stackwalk_test.go deleted file mode 100644 index cdccf0dee..000000000 --- a/pkg/kevent/stackwalk_test.go +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2021-present by Nedim Sabic Sabic - * https://www.fibratus.io - * All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package kevent - -import ( - "github.com/rabbitstack/fibratus/pkg/fs" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" - "github.com/rabbitstack/fibratus/pkg/util/va" - "github.com/stretchr/testify/assert" - "testing" - "time" -) - -func TestStackwalkDecorator(t *testing.T) { - q := NewQueue(50, false, true) - cd := NewStackwalkDecorator(q) - - e := &Kevent{ - Type: ktypes.CreateFile, - Tid: 2484, - PID: 859, - CPU: 1, - Seq: 2, - Name: "CreateFile", - Timestamp: time.Now(), - Category: ktypes.File, - Kparams: Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.Enum, Value: uint32(1), Enum: fs.FileCreateDispositions}, - }, - } - - e1 := &Kevent{ - Type: ktypes.CreateFile, - Tid: 2484, - PID: 859, - CPU: 1, - Seq: 3, - Name: "CreateFile", - Timestamp: time.Now(), - Category: ktypes.File, - Kparams: Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\kernel32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.Enum, Value: uint32(1), Enum: fs.FileCreateDispositions}, - }, - } - - cd.Push(e) - cd.Push(e1) - - assert.Len(t, cd.buckets[e.StackID()], 2) - - sw := &Kevent{ - Type: ktypes.StackWalk, - Tid: 2484, - PID: 859, - CPU: 1, - Seq: 4, - Name: "StackWalk", - Timestamp: time.Now(), - Kparams: Kparams{ - kparams.Callstack: {Name: kparams.Callstack, Type: kparams.Slice, Value: []va.Address{0x7ffb5eb70dc4, 0x7ffb5c191deb, 0x7ffb3138592e}}, - }, - } - - evt := cd.Pop(sw) - assert.Len(t, cd.buckets[e.StackID()], 1) - assert.Equal(t, ktypes.CreateFile, evt.Type) - assert.True(t, evt.Kparams.Contains(kparams.Callstack)) - assert.Equal(t, "C:\\Windows\\system32\\user32.dll", evt.GetParamAsString(kparams.FilePath)) -} - -func init() { - maxQueueTTLPeriod = time.Second * 2 - flusherInterval = time.Second -} - -func TestStackwalkDecoratorFlush(t *testing.T) { - q := NewQueue(50, false, true) - q.RegisterListener(&DummyListener{}) - cd := NewStackwalkDecorator(q) - defer cd.Stop() - - e := &Kevent{ - Type: ktypes.CreateFile, - Tid: 2484, - PID: 859, - CPU: 1, - Seq: 2, - Name: "CreateFile", - Timestamp: time.Now(), - Category: ktypes.File, - Kparams: Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.Enum, Value: uint32(1), Enum: fs.FileCreateDispositions}, - }, - } - - cd.Push(e) - assert.Len(t, cd.buckets[e.StackID()], 1) - time.Sleep(time.Millisecond * 3100) - - evt := <-q.Events() - assert.Len(t, cd.buckets[e.StackID()], 0) - assert.Equal(t, ktypes.CreateFile, evt.Type) - assert.False(t, evt.Kparams.Contains(kparams.Callstack)) -} diff --git a/pkg/outputs/amqp/amqp.go b/pkg/outputs/amqp/amqp.go index df4056063..1a62f09d9 100644 --- a/pkg/outputs/amqp/amqp.go +++ b/pkg/outputs/amqp/amqp.go @@ -21,7 +21,7 @@ package amqp import ( "expvar" - "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/outputs" ) @@ -66,7 +66,7 @@ func (q *rabbitmq) Close() error { return q.client.close() } -func (q *rabbitmq) Publish(batch *kevent.Batch) error { +func (q *rabbitmq) Publish(batch *event.Batch) error { body := batch.MarshalJSON() err := q.client.publish(body) diff --git a/pkg/outputs/amqp/amqp_test.go b/pkg/outputs/amqp/amqp_test.go index db44d1ccc..6181b95ab 100644 --- a/pkg/outputs/amqp/amqp_test.go +++ b/pkg/outputs/amqp/amqp_test.go @@ -27,10 +27,9 @@ import ( "time" "github.com/phayes/freeport" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" htypes "github.com/rabbitstack/fibratus/pkg/handle/types" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/config" broker "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/server" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" @@ -66,7 +65,7 @@ func TestPublishAmqpOutput(t *testing.T) { require.NoError(t, q.Connect()) defer q.Close() - err = consumeKevents(t, amqpURL(port), done) + err = consumeEvents(t, amqpURL(port), done) require.NoError(t, err) err = q.Publish(getBatch()) @@ -118,7 +117,7 @@ func TestHealthcheck(t *testing.T) { } //nolint:unused -func consumeKevents(t *testing.T, amqpURI string, done chan struct{}) error { +func consumeEvents(t *testing.T, amqpURI string, done chan struct{}) error { conn, err := amqp.Dial(amqpURI) if err != nil { return err @@ -166,7 +165,7 @@ func consumeKevents(t *testing.T, amqpURI string, done chan struct{}) error { done <- struct{}{} t.Error("got empty AMQP message") } - var kevents []*kevent.Kevent + var kevents []*event.Event err := json.Unmarshal(body, &kevents) if err != nil { done <- struct{}{} @@ -193,27 +192,27 @@ func amqpURL(port int) string { } //nolint:unused -func getBatch() *kevent.Batch { - kevt := &kevent.Kevent{ - Type: ktypes.CreateFile, +func getBatch() *event.Batch { + evt := &event.Event{ + Type: event.CreateFile, Tid: 2484, PID: 859, CPU: 1, Seq: 2, Name: "CreateFile", Timestamp: time.Now(), - Category: ktypes.File, + Category: event.File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", - Kparams: kevent.Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, - kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, - kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.AnsiString, Value: "open"}, + params.BasePrio: {Name: params.BasePrio, Type: params.Int8, Value: int8(2)}, + params.PagePrio: {Name: params.PagePrio, Type: params.Uint8, Value: uint8(2)}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "baarz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "baarz"}, PS: &pstypes.PS{ PID: 2436, Ppid: 6304, @@ -262,26 +261,26 @@ func getBatch() *kevent.Batch { }, } - kevt1 := &kevent.Kevent{ - Type: ktypes.CreateFile, + evt1 := &event.Event{ + Type: event.CreateFile, Tid: 2484, PID: 459, CPU: 1, Seq: 2, Name: "CreateFile", Timestamp: time.Now(), - Category: ktypes.File, + Category: event.File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", - Kparams: kevent.Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, - kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, - kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.AnsiString, Value: "open"}, + params.BasePrio: {Name: params.BasePrio, Type: params.Int8, Value: int8(2)}, + params.PagePrio: {Name: params.PagePrio, Type: params.Uint8, Value: uint8(2)}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "baarz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "baarz"}, PS: &pstypes.PS{ PID: 2436, Ppid: 6304, @@ -330,26 +329,26 @@ func getBatch() *kevent.Batch { }, } - kevt2 := &kevent.Kevent{ - Type: ktypes.CreateFile, + evt2 := &event.Event{ + Type: event.CreateFile, Tid: 2484, PID: 829, CPU: 1, Seq: 2, Name: "CreateFile", Timestamp: time.Now(), - Category: ktypes.File, + Category: event.File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", - Kparams: kevent.Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, - kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, - kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.AnsiString, Value: "open"}, + params.BasePrio: {Name: params.BasePrio, Type: params.Int8, Value: int8(2)}, + params.PagePrio: {Name: params.PagePrio, Type: params.Uint8, Value: uint8(2)}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "baarz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "baarz"}, PS: &pstypes.PS{ PID: 829, Ppid: 6304, @@ -398,5 +397,5 @@ func getBatch() *kevent.Batch { }, } - return kevent.NewBatch(kevt, kevt1, kevt2) + return event.NewBatch(evt, evt1, evt2) } diff --git a/pkg/outputs/client.go b/pkg/outputs/client.go index be98bdea8..7999056be 100644 --- a/pkg/outputs/client.go +++ b/pkg/outputs/client.go @@ -19,12 +19,12 @@ package outputs import ( - "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/event" ) // Client represents the minimal interface all output implementors have to satisfy. type Client interface { Close() error - Publish(*kevent.Batch) error + Publish(*event.Batch) error Connect() error } diff --git a/pkg/outputs/console/config.go b/pkg/outputs/console/config.go index 583b894bd..d39a577b9 100644 --- a/pkg/outputs/console/config.go +++ b/pkg/outputs/console/config.go @@ -38,7 +38,7 @@ type Config struct { // AddFlags registers persistent flags. func AddFlags(flags *pflag.FlagSet) { flags.String(frmt, string(pretty), "Specifies the output format. Choose between pretty|json") - flags.String(paramKVDelimiter, "", "The delimiter symbol for the kparams key/value pairs") + flags.String(paramKVDelimiter, "", "The delimiter symbol for the params key/value pairs") flags.String(tmpl, "", "Event formatting template") flags.Bool(enabled, true, "Indicates if the console output is enabled") } diff --git a/pkg/outputs/console/console.go b/pkg/outputs/console/console.go index 829cf4cc8..9eca570fb 100644 --- a/pkg/outputs/console/console.go +++ b/pkg/outputs/console/console.go @@ -21,7 +21,7 @@ package console import ( "bufio" "expvar" - "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/outputs" "os" ) @@ -36,12 +36,12 @@ const ( pretty format = "pretty" json format = "json" // template represents the default template used in pretty rendering mode - template = "{{ .Seq }} {{ .Timestamp }} - {{ .CPU }} {{ .Process }} ({{ .Pid }}) - {{ .Type }} ({{ .Kparams }})" + template = "{{ .Seq }} {{ .Timestamp }} - {{ .CPU }} {{ .Process }} ({{ .Pid }}) - {{ .Type }} ({{ .Params }})" ) type console struct { writer *bufio.Writer - formatter *kevent.Formatter + formatter *event.Formatter format format } @@ -59,12 +59,12 @@ func initConsole(config outputs.Config) (outputs.OutputGroup, error) { if tmpl == "" { tmpl = template } - formatter, err := kevent.NewFormatter(tmpl) + formatter, err := event.NewFormatter(tmpl) if err != nil { return outputs.Fail(err) } if cfg.ParamKVDelimiter != "" { - kevent.ParamKVDelimiter = cfg.ParamKVDelimiter + event.ParamKVDelimiter = cfg.ParamKVDelimiter } c := &console{ @@ -77,14 +77,14 @@ func initConsole(config outputs.Config) (outputs.OutputGroup, error) { func (c *console) Close() error { return c.writer.Flush() } func (c *console) Connect() error { return nil } -func (c *console) Publish(batch *kevent.Batch) error { - for _, kevt := range batch.Events { +func (c *console) Publish(batch *event.Batch) error { + for _, evt := range batch.Events { var buf []byte switch c.format { case json: - buf = kevt.MarshalJSON() + buf = evt.MarshalJSON() case pretty: - buf = c.formatter.Format(kevt) + buf = c.formatter.Format(evt) default: return nil } diff --git a/pkg/outputs/elasticsearch/elasticsearch.go b/pkg/outputs/elasticsearch/elasticsearch.go index bbca52bcd..230a3a8d4 100644 --- a/pkg/outputs/elasticsearch/elasticsearch.go +++ b/pkg/outputs/elasticsearch/elasticsearch.go @@ -25,7 +25,7 @@ import ( "fmt" "github.com/hashicorp/go-version" "github.com/olivere/elastic/v7" - "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/outputs" "github.com/rabbitstack/fibratus/pkg/util/tls" log "github.com/sirupsen/logrus" @@ -170,20 +170,20 @@ func (e *elasticsearch) Connect() error { return nil } -func (e *elasticsearch) Publish(batch *kevent.Batch) error { - for _, kevt := range batch.Events { - indexName := e.index.getName(kevt) +func (e *elasticsearch) Publish(batch *event.Batch) error { + for _, evt := range batch.Events { + indexName := e.index.getName(evt) // create the bulk index request for each event in the batch. // We already have a valid JSON body, so just pass the raw // JSON message as request document - e.bulkProcessor.Add(newBulkIndexRequest(indexName, kevt)) + e.bulkProcessor.Add(newBulkIndexRequest(indexName, evt)) totalBulkedDocs.Add(1) } return nil } -func newBulkIndexRequest(indexName string, kevt *kevent.Kevent) *elastic.BulkIndexRequest { - kjson := kevt.MarshalJSON() +func newBulkIndexRequest(indexName string, evt *event.Event) *elastic.BulkIndexRequest { + kjson := evt.MarshalJSON() return elastic.NewBulkIndexRequest().Index(indexName).Doc(json.RawMessage(kjson)) } diff --git a/pkg/outputs/elasticsearch/elasticsearch_test.go b/pkg/outputs/elasticsearch/elasticsearch_test.go index 635ad24e1..b956e8aba 100644 --- a/pkg/outputs/elasticsearch/elasticsearch_test.go +++ b/pkg/outputs/elasticsearch/elasticsearch_test.go @@ -31,10 +31,9 @@ import ( "time" "github.com/olivere/elastic/v7" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" htypes "github.com/rabbitstack/fibratus/pkg/handle/types" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -97,7 +96,7 @@ func TestElasticsearchPublish(t *testing.T) { defer r.Body.Close() // check we have the correct index name assert.True(t, bytes.Contains(body, []byte("fibratus-2018-03"))) - // check kevent name is present + // check event name is present assert.True(t, bytes.Contains(body, []byte("CreateFile"))) // create the bulk response @@ -133,7 +132,7 @@ func TestElasticsearchPublish(t *testing.T) { })) defer srv.Close() - kevent.SerializeHandles = true + event.SerializeHandles = true cfg := Config{ Servers: []string{srv.URL}, @@ -158,29 +157,29 @@ func TestElasticsearchPublish(t *testing.T) { assert.Equal(t, int64(0), failedDocs.Value()) } -func getBatch() *kevent.Batch { +func getBatch() *event.Batch { ts, _ := time.Parse(time.RFC3339, "2018-05-03T15:04:05.323Z") - kevt := &kevent.Kevent{ - Type: ktypes.CreateFile, + evt := &event.Event{ + Type: event.CreateFile, Tid: 2484, PID: 859, CPU: 1, Seq: 2, Name: "CreateFile", Timestamp: ts, - Category: ktypes.File, + Category: event.File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", - Kparams: kevent.Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, - kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, - kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.AnsiString, Value: "open"}, + params.BasePrio: {Name: params.BasePrio, Type: params.Int8, Value: int8(2)}, + params.PagePrio: {Name: params.PagePrio, Type: params.Uint8, Value: uint8(2)}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "baarz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "baarz"}, PS: &pstypes.PS{ PID: 2436, Ppid: 6304, @@ -229,26 +228,26 @@ func getBatch() *kevent.Batch { }, } - kevt1 := &kevent.Kevent{ - Type: ktypes.CreateFile, + evt1 := &event.Event{ + Type: event.CreateFile, Tid: 2484, PID: 459, CPU: 1, Seq: 2, Name: "CreateFile", Timestamp: ts, - Category: ktypes.File, + Category: event.File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", - Kparams: kevent.Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, - kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, - kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.AnsiString, Value: "open"}, + params.BasePrio: {Name: params.BasePrio, Type: params.Int8, Value: int8(2)}, + params.PagePrio: {Name: params.PagePrio, Type: params.Uint8, Value: uint8(2)}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "baarz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "baarz"}, PS: &pstypes.PS{ PID: 2436, Ppid: 6304, @@ -297,26 +296,26 @@ func getBatch() *kevent.Batch { }, } - kevt2 := &kevent.Kevent{ - Type: ktypes.CreateFile, + evt2 := &event.Event{ + Type: event.CreateFile, Tid: 2484, PID: 829, CPU: 1, Seq: 2, Name: "CreateFile", Timestamp: ts, - Category: ktypes.File, + Category: event.File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", - Kparams: kevent.Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, - kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, - kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.AnsiString, Value: "open"}, + params.BasePrio: {Name: params.BasePrio, Type: params.Int8, Value: int8(2)}, + params.PagePrio: {Name: params.PagePrio, Type: params.Uint8, Value: uint8(2)}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "baarz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "baarz"}, PS: &pstypes.PS{ PID: 829, Ppid: 6304, @@ -365,5 +364,5 @@ func getBatch() *kevent.Batch { }, } - return kevent.NewBatch(kevt, kevt1, kevt2) + return event.NewBatch(evt, evt1, evt2) } diff --git a/pkg/outputs/elasticsearch/index.go b/pkg/outputs/elasticsearch/index.go index 698032c52..8b5fb3522 100644 --- a/pkg/outputs/elasticsearch/index.go +++ b/pkg/outputs/elasticsearch/index.go @@ -23,7 +23,7 @@ import ( "context" "fmt" "github.com/olivere/elastic/v7" - "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/event" "html/template" "strings" "time" @@ -77,12 +77,12 @@ func (i index) putTemplate() error { // getName creates an index name by replacing specifiers to create time frame indices. If no time specifiers are // used this method returns a fixed index name. -func (i index) getName(kevt *kevent.Kevent) string { +func (i index) getName(evt *event.Event) string { indexName := i.config.IndexName if !strings.Contains(indexName, "%") { return indexName } - return i.replace(kevt.Timestamp) + return i.replace(evt.Timestamp) } func (i index) replace(timestamp time.Time) string { diff --git a/pkg/outputs/elasticsearch/index_test.go b/pkg/outputs/elasticsearch/index_test.go index cb41f284b..ce1fa35c9 100644 --- a/pkg/outputs/elasticsearch/index_test.go +++ b/pkg/outputs/elasticsearch/index_test.go @@ -19,7 +19,7 @@ package elasticsearch import ( - "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/stretchr/testify/assert" "testing" "time" @@ -30,21 +30,21 @@ func TestProduceIndexName(t *testing.T) { ts, _ := time.Parse(time.RFC3339, "2011-05-03T15:04:05.323Z") - indexName := i.getName(&kevent.Kevent{Timestamp: ts}) + indexName := i.getName(&event.Event{Timestamp: ts}) assert.Equal(t, "fibratus-2011-05", indexName) i = index{config: Config{IndexName: "fibratus-%y-%d"}} - indexName = i.getName(&kevent.Kevent{Timestamp: ts}) + indexName = i.getName(&event.Event{Timestamp: ts}) assert.Equal(t, "fibratus-11-03", indexName) i = index{config: Config{IndexName: "fibratus-%d-%H"}} - indexName = i.getName(&kevent.Kevent{Timestamp: ts}) + indexName = i.getName(&event.Event{Timestamp: ts}) assert.Equal(t, "fibratus-03-15", indexName) i = index{config: Config{IndexName: "fibratus-events"}} - indexName = i.getName(&kevent.Kevent{Timestamp: ts}) + indexName = i.getName(&event.Event{Timestamp: ts}) assert.Equal(t, "fibratus-events", indexName) } diff --git a/pkg/outputs/elasticsearch/template.go b/pkg/outputs/elasticsearch/template.go index bb154f9ec..e6bf0244b 100644 --- a/pkg/outputs/elasticsearch/template.go +++ b/pkg/outputs/elasticsearch/template.go @@ -46,7 +46,7 @@ const indexTemplate = ` "timestamp": { "type": "date" }, - "kparams": { + "params": { "type": "nested", "properties": { "dip": { "type": "ip" }, diff --git a/pkg/outputs/eventlog/config.go b/pkg/outputs/eventlog/config.go index 045b6befe..d67576cfb 100644 --- a/pkg/outputs/eventlog/config.go +++ b/pkg/outputs/eventlog/config.go @@ -19,7 +19,7 @@ package eventlog import ( - "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/event" "text/template" "github.com/spf13/pflag" @@ -47,7 +47,7 @@ type Config struct { func (c Config) parseTemplate() (*template.Template, error) { if c.Template == "" { // use built-in template - return template.New("evtlog").Parse(kevent.Template) + return template.New("evtlog").Parse(event.Template) } return template.New("evtlog").Parse(c.Template) } diff --git a/pkg/outputs/eventlog/eventlog.go b/pkg/outputs/eventlog/eventlog.go index 6912a1b6e..941ca7014 100644 --- a/pkg/outputs/eventlog/eventlog.go +++ b/pkg/outputs/eventlog/eventlog.go @@ -26,9 +26,7 @@ import ( "golang.org/x/sys/windows" "text/template" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" - - "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/outputs" ) @@ -45,7 +43,7 @@ type evtlog struct { evtlog *Eventlog // eventlog writer config Config tmpl *template.Template - events []ktypes.KeventInfo + events []event.Info cats []string } @@ -67,8 +65,8 @@ func initEventlog(config outputs.Config) (outputs.OutputGroup, error) { } evtlog := &evtlog{ config: cfg, - events: ktypes.GetKtypesMetaIndexed(), - cats: ktypes.Categories(), + events: event.GetTypesMetaIndexed(), + cats: event.Categories(), } evtlog.tmpl, err = cfg.parseTemplate() if err != nil { @@ -101,22 +99,22 @@ func (e *evtlog) Close() error { return nil } -func (e *evtlog) Publish(batch *kevent.Batch) error { - for _, kevt := range batch.Events { - if err := e.publish(kevt); err != nil { +func (e *evtlog) Publish(batch *event.Batch) error { + for _, evt := range batch.Events { + if err := e.publish(evt); err != nil { return err } } return nil } -func (e *evtlog) publish(kevt *kevent.Kevent) error { - buf, err := kevt.RenderCustomTemplate(e.tmpl) +func (e *evtlog) publish(evt *event.Event) error { + buf, err := evt.RenderCustomTemplate(e.tmpl) if err != nil { return err } - categoryID := e.categoryID(kevt) - eventID := eventlog.EventID(windows.EVENTLOG_INFORMATION_TYPE, uint16(e.eventCode(kevt))) + categoryID := e.categoryID(evt) + eventID := eventlog.EventID(windows.EVENTLOG_INFORMATION_TYPE, uint16(e.eventCode(evt))) if eventID == 0 { return ErrUnknownEventID } @@ -128,9 +126,9 @@ func (e *evtlog) publish(kevt *kevent.Kevent) error { } // categoryID maps category name to eventlog identifier. -func (e *evtlog) categoryID(kevt *kevent.Kevent) uint16 { +func (e *evtlog) categoryID(evt *event.Event) uint16 { for i, cat := range e.cats { - if cat == string(kevt.Category) { + if cat == string(evt.Category) { return uint16(i + 1) } } @@ -138,9 +136,9 @@ func (e *evtlog) categoryID(kevt *kevent.Kevent) uint16 { } // eventCode returns the event ID from the event type. -func (e *evtlog) eventCode(kevt *kevent.Kevent) uint32 { - for i, evt := range e.events { - if evt.Name == kevt.Name { +func (e *evtlog) eventCode(evt *event.Event) uint32 { + for i, e := range e.events { + if evt.Name == e.Name { return uint32(i + categoryOffset) } } diff --git a/pkg/outputs/eventlog/eventlog_test.go b/pkg/outputs/eventlog/eventlog_test.go index c8a8ccbe0..2528d45e1 100644 --- a/pkg/outputs/eventlog/eventlog_test.go +++ b/pkg/outputs/eventlog/eventlog_test.go @@ -26,10 +26,9 @@ import ( "testing" "time" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" htypes "github.com/rabbitstack/fibratus/pkg/handle/types" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/rabbitstack/fibratus/pkg/pe" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" "github.com/stretchr/testify/require" @@ -44,8 +43,8 @@ func TestEvtlogPublish(t *testing.T) { el := &evtlog{ config: c, tmpl: tmpl, - events: ktypes.GetKtypesMetaIndexed(), - cats: ktypes.Categories(), + events: event.GetTypesMetaIndexed(), + cats: event.Categories(), } err = eventlog.Install(eventlog.Levels) if err != nil && !errors.Is(err, eventlog.ErrKeyExists) { @@ -55,27 +54,27 @@ func TestEvtlogPublish(t *testing.T) { require.NoError(t, el.Publish(getBatch())) } -func getBatch() *kevent.Batch { - kevt := &kevent.Kevent{ - Type: ktypes.CreateFile, +func getBatch() *event.Batch { + evt := &event.Event{ + Type: event.CreateFile, Tid: 2484, PID: 859, CPU: 1, Seq: 2, Name: "CreateFile", Timestamp: time.Now(), - Category: ktypes.File, + Category: event.File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", - Kparams: kevent.Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, - kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, - kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.AnsiString, Value: "open"}, + params.BasePrio: {Name: params.BasePrio, Type: params.Int8, Value: int8(2)}, + params.PagePrio: {Name: params.PagePrio, Type: params.Uint8, Value: uint8(2)}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "baarz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "baarz"}, PS: &pstypes.PS{ PID: 2436, Ppid: 6304, @@ -142,5 +141,5 @@ func getBatch() *kevent.Batch { }, } - return kevent.NewBatch(kevt) + return event.NewBatch(evt) } diff --git a/pkg/outputs/eventlog/mc/gen.go b/pkg/outputs/eventlog/mc/gen.go index 4c142a075..0663c9f5b 100644 --- a/pkg/outputs/eventlog/mc/gen.go +++ b/pkg/outputs/eventlog/mc/gen.go @@ -22,7 +22,7 @@ import ( "bytes" "fmt" "github.com/Masterminds/sprig/v3" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/event" "io" "log" "math" @@ -33,13 +33,13 @@ import ( // Source contains the required data for producing the input for the message compiler. type Source struct { Categories []string - Events []ktypes.KeventInfo + Events []event.Info MaxEvents uint16 } func (s *Source) Generate(w io.Writer) error { funcmap := sprig.TxtFuncMap() - funcmap["length"] = func(evts []ktypes.KeventInfo) int { + funcmap["length"] = func(evts []event.Info) int { return len(evts) } funcmap["N"] = func(start, end int) (stream chan int) { @@ -64,8 +64,8 @@ func main() { var buf bytes.Buffer src := &Source{ - Categories: ktypes.Categories(), - Events: ktypes.GetKtypesMetaIndexed(), + Categories: event.Categories(), + Events: event.GetTypesMetaIndexed(), MaxEvents: math.MaxUint16, } diff --git a/pkg/outputs/http/http.go b/pkg/outputs/http/http.go index 726a1a788..3b8bab183 100644 --- a/pkg/outputs/http/http.go +++ b/pkg/outputs/http/http.go @@ -29,7 +29,7 @@ import ( "github.com/rabbitstack/fibratus/pkg/util/version" - "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/outputs" ) @@ -79,7 +79,7 @@ func initHTTP(config outputs.Config) (outputs.OutputGroup, error) { func (h *_http) Connect() error { return nil } func (h *_http) Close() error { return nil } -func (h *_http) Publish(batch *kevent.Batch) error { +func (h *_http) Publish(batch *event.Batch) error { var buf []byte switch h.config.Serializer { case outputs.JSON: diff --git a/pkg/outputs/http/http_test.go b/pkg/outputs/http/http_test.go index 875b904fd..82c113ef7 100644 --- a/pkg/outputs/http/http_test.go +++ b/pkg/outputs/http/http_test.go @@ -34,10 +34,9 @@ import ( "github.com/stretchr/testify/assert" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" htypes "github.com/rabbitstack/fibratus/pkg/handle/types" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" "github.com/stretchr/testify/require" ) @@ -55,7 +54,7 @@ func TestHttpPublish(t *testing.T) { http.Error(w, err.Error(), http.StatusInternalServerError) return } - var kevents []*kevent.Kevent + var kevents []*event.Event if err := json.Unmarshal(body, &kevents); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -115,7 +114,7 @@ func TestHttpGzipPublish(t *testing.T) { http.Error(w, err.Error(), http.StatusInternalServerError) return } - var kevents []*kevent.Kevent + var kevents []*event.Event if err := json.Unmarshal(body, &kevents); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -147,27 +146,27 @@ func TestHttpGzipPublish(t *testing.T) { require.NoError(t, err) } -func getBatch() *kevent.Batch { - kevt := &kevent.Kevent{ - Type: ktypes.CreateFile, +func getBatch() *event.Batch { + evt := &event.Event{ + Type: event.CreateFile, Tid: 2484, PID: 859, CPU: 1, Seq: 2, Name: "CreateFile", Timestamp: time.Now(), - Category: ktypes.File, + Category: event.File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", - Kparams: kevent.Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, - kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, - kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.AnsiString, Value: "open"}, + params.BasePrio: {Name: params.BasePrio, Type: params.Int8, Value: int8(2)}, + params.PagePrio: {Name: params.PagePrio, Type: params.Uint8, Value: uint8(2)}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "baarz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "baarz"}, PS: &pstypes.PS{ PID: 2436, Ppid: 6304, @@ -216,26 +215,26 @@ func getBatch() *kevent.Batch { }, } - kevt1 := &kevent.Kevent{ - Type: ktypes.CreateFile, + evt1 := &event.Event{ + Type: event.CreateFile, Tid: 2484, PID: 459, CPU: 1, Seq: 2, Name: "CreateFile", Timestamp: time.Now(), - Category: ktypes.File, + Category: event.File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", - Kparams: kevent.Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, - kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, - kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.AnsiString, Value: "open"}, + params.BasePrio: {Name: params.BasePrio, Type: params.Int8, Value: int8(2)}, + params.PagePrio: {Name: params.PagePrio, Type: params.Uint8, Value: uint8(2)}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "baarz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "baarz"}, PS: &pstypes.PS{ PID: 2436, Ppid: 6304, @@ -284,26 +283,26 @@ func getBatch() *kevent.Batch { }, } - kevt2 := &kevent.Kevent{ - Type: ktypes.CreateFile, + evt2 := &event.Event{ + Type: event.CreateFile, Tid: 2484, PID: 829, CPU: 1, Seq: 2, Name: "CreateFile", Timestamp: time.Now(), - Category: ktypes.File, + Category: event.File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", - Kparams: kevent.Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, - kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, - kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.AnsiString, Value: "open"}, + params.BasePrio: {Name: params.BasePrio, Type: params.Int8, Value: int8(2)}, + params.PagePrio: {Name: params.PagePrio, Type: params.Uint8, Value: uint8(2)}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "baarz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "baarz"}, PS: &pstypes.PS{ PID: 829, Ppid: 6304, @@ -352,5 +351,5 @@ func getBatch() *kevent.Batch { }, } - return kevent.NewBatch(kevt, kevt1, kevt2) + return event.NewBatch(evt, evt1, evt2) } diff --git a/pkg/outputs/null/null.go b/pkg/outputs/null/null.go index 16afcc7e9..8a0782656 100644 --- a/pkg/outputs/null/null.go +++ b/pkg/outputs/null/null.go @@ -20,7 +20,7 @@ package null import ( "expvar" - "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/outputs" ) @@ -39,7 +39,7 @@ func initNull(config outputs.Config) (outputs.OutputGroup, error) { func (null) Close() error { return nil } func (null) Connect() error { return nil } -func (null) Publish(batch *kevent.Batch) error { +func (null) Publish(batch *event.Batch) error { blackholeEventsCount.Add(batch.Len()) return nil } diff --git a/pkg/pe/marshaller.go b/pkg/pe/marshaller.go index 6e542d1c5..c4944afac 100644 --- a/pkg/pe/marshaller.go +++ b/pkg/pe/marshaller.go @@ -23,7 +23,7 @@ package pe import ( "fmt" - kcapver "github.com/rabbitstack/fibratus/pkg/kcap/version" + capver "github.com/rabbitstack/fibratus/pkg/cap/version" "github.com/rabbitstack/fibratus/pkg/sys" "github.com/rabbitstack/fibratus/pkg/util/bytes" "github.com/rabbitstack/fibratus/pkg/util/convert" @@ -124,7 +124,7 @@ func (pe *PE) Marshal() []byte { } // Unmarshal recovers the PE metadata from the byte stream. -func (pe *PE) Unmarshal(b []byte, ver kcapver.Version) error { +func (pe *PE) Unmarshal(b []byte, ver capver.Version) error { if len(b) < 6 { return fmt.Errorf("expected at least 6 bytes but got %d bytes", len(b)) } @@ -241,7 +241,7 @@ func (pe *PE) Unmarshal(b []byte, ver kcapver.Version) error { offset += roffset - if ver >= kcapver.PESecV2 { + if ver >= capver.PESecV2 { pe.IsSigned = convert.Itob(b[20+offset]) pe.IsTrusted = convert.Itob(b[21+offset]) @@ -283,7 +283,7 @@ func (pe *PE) Unmarshal(b []byte, ver kcapver.Version) error { } // NewFromKcap restores the PE metadata from the byte stream. -func NewFromKcap(b []byte, ver kcapver.Version) (*PE, error) { +func NewFromKcap(b []byte, ver capver.Version) (*PE, error) { pe := &PE{ Sections: make([]Sec, 0), Symbols: make([]string, 0), diff --git a/pkg/pe/marshaller_test.go b/pkg/pe/marshaller_test.go index 3943f533e..07ac3f8a0 100644 --- a/pkg/pe/marshaller_test.go +++ b/pkg/pe/marshaller_test.go @@ -22,7 +22,7 @@ package pe import ( - kcapver "github.com/rabbitstack/fibratus/pkg/kcap/version" + kcapver "github.com/rabbitstack/fibratus/pkg/cap/version" "github.com/rabbitstack/fibratus/pkg/sys" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/pkg/ps/snapshotter.go b/pkg/ps/snapshotter.go index 17a93279c..1eec7f18e 100644 --- a/pkg/ps/snapshotter.go +++ b/pkg/ps/snapshotter.go @@ -19,7 +19,7 @@ package ps import ( - "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/event" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" "github.com/rabbitstack/fibratus/pkg/util/va" ) @@ -31,23 +31,23 @@ type Snapshotter interface { // Write appends a new process state to the snapshotter. It takes as an input the inbound event to fetch // the basic data, but also enriches the process' state with extra metadata such as process' env variables, PE // metadata for Windows binaries and so on. - Write(*kevent.Kevent) error + Write(*event.Event) error // AddThread builds thread state from the event representation. - AddThread(*kevent.Kevent) error + AddThread(*event.Event) error // AddModule builds module state from the event representation. - AddModule(*kevent.Kevent) error + AddModule(*event.Event) error // RemoveThread removes the thread from the given process. RemoveThread(pid uint32, tid uint32) error // RemoveModule removes the module the given process. RemoveModule(pid uint32, addr va.Address) error // AddMmap adds a new memory mapping (data memory-mapped file, image, or pagefile) to this process state. - AddMmap(*kevent.Kevent) error + AddMmap(*event.Event) error // RemoveMmap removes memory mapping at the given base address. RemoveMmap(pid uint32, addr va.Address) error - // WriteFromKcap appends a new process state to the snapshotter from the captured kernel event. - WriteFromKcap(kevt *kevent.Kevent) error + // WriteFromCapture appends a new process state to the snapshotter from the captured event. + WriteFromCapture(evt *event.Event) error // Remove deletes process's state from the snapshotter. - Remove(kevt *kevent.Kevent) error + Remove(evt *event.Event) error // Find attempts to retrieve process' state for the specified process identifier. Returns true // if the process was find in the state. Otherwise, returns false and constructs a fresh process // state by querying the OS via API functions. diff --git a/pkg/ps/snapshotter_mock.go b/pkg/ps/snapshotter_mock.go index ee4458efd..f0268da93 100644 --- a/pkg/ps/snapshotter_mock.go +++ b/pkg/ps/snapshotter_mock.go @@ -19,7 +19,7 @@ package ps import ( - "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/event" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" "github.com/rabbitstack/fibratus/pkg/util/va" "github.com/stretchr/testify/mock" @@ -31,14 +31,14 @@ type SnapshotterMock struct { } // Write method -func (s *SnapshotterMock) Write(kevt *kevent.Kevent) error { - args := s.Called(kevt) +func (s *SnapshotterMock) Write(evt *event.Event) error { + args := s.Called(evt) return args.Error(0) } // Remove method -func (s *SnapshotterMock) Remove(kevt *kevent.Kevent) error { - args := s.Called(kevt) +func (s *SnapshotterMock) Remove(evt *event.Event) error { + args := s.Called(evt) return args.Error(0) } @@ -79,14 +79,14 @@ func (s *SnapshotterMock) GetSnapshot() []*pstypes.PS { } // AddThread method -func (s *SnapshotterMock) AddThread(kevt *kevent.Kevent) error { - args := s.Called(kevt) +func (s *SnapshotterMock) AddThread(evt *event.Event) error { + args := s.Called(evt) return args.Error(0) } // AddModule method -func (s *SnapshotterMock) AddModule(kevt *kevent.Kevent) error { - args := s.Called(kevt) +func (s *SnapshotterMock) AddModule(evt *event.Event) error { + args := s.Called(evt) return args.Error(0) } @@ -102,16 +102,16 @@ func (s *SnapshotterMock) RemoveModule(pid uint32, addr va.Address) error { return args.Error(0) } -// WriteFromKcap method -func (s *SnapshotterMock) WriteFromKcap(kevt *kevent.Kevent) error { return nil } +// WriteFromCapture method +func (s *SnapshotterMock) WriteFromCapture(evt *event.Event) error { return nil } -// AddFileMapping method -func (s *SnapshotterMock) AddMmap(kevt *kevent.Kevent) error { - args := s.Called(kevt) +// AddMmap method +func (s *SnapshotterMock) AddMmap(evt *event.Event) error { + args := s.Called(evt) return args.Error(0) } -// RemoveFileMapping method +// RemoveMmap method func (s *SnapshotterMock) RemoveMmap(pid uint32, address va.Address) error { args := s.Called(pid, address) return args.Error(0) diff --git a/pkg/ps/snapshotter_windows.go b/pkg/ps/snapshotter_windows.go index dc785375f..0080d8b17 100644 --- a/pkg/ps/snapshotter_windows.go +++ b/pkg/ps/snapshotter_windows.go @@ -30,11 +30,10 @@ import ( "time" "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/handle" htypes "github.com/rabbitstack/fibratus/pkg/handle/types" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/rabbitstack/fibratus/pkg/pe" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" log "github.com/sirupsen/logrus" @@ -88,8 +87,8 @@ func NewSnapshotter(hsnap handle.Snapshotter, config *config.Config) Snapshotter return s } -// NewSnapshotterFromKcap restores the snapshotter state from the kcap file. -func NewSnapshotterFromKcap(hsnap handle.Snapshotter, config *config.Config) Snapshotter { +// NewSnapshotterFromCapture restores the snapshotter state from the cap file. +func NewSnapshotterFromCapture(hsnap handle.Snapshotter, config *config.Config) Snapshotter { s := &snapshotter{ procs: make(map[uint32]*pstypes.PS), dirty: make(map[uint32]*pstypes.PS), @@ -105,20 +104,20 @@ func NewSnapshotterFromKcap(hsnap handle.Snapshotter, config *config.Config) Sna return s } -func (s *snapshotter) WriteFromKcap(e *kevent.Kevent) error { +func (s *snapshotter) WriteFromCapture(e *event.Event) error { switch e.Type { - case ktypes.CreateProcess, ktypes.ProcessRundown: + case event.CreateProcess, event.ProcessRundown: s.mu.Lock() defer s.mu.Unlock() proc := e.PS if proc == nil { return nil } - pid, err := e.Kparams.GetPid() + pid, err := e.Params.GetPid() if err != nil { return err } - ppid, err := e.Kparams.GetPpid() + ppid, err := e.Params.GetPpid() if err != nil { return err } @@ -135,23 +134,23 @@ func (s *snapshotter) WriteFromKcap(e *kevent.Kevent) error { } } s.procs[pid] = proc - case ktypes.CreateThread, ktypes.ThreadRundown: + case event.CreateThread, event.ThreadRundown: return s.AddThread(e) - case ktypes.LoadImage, ktypes.ImageRundown: + case event.LoadImage, event.ImageRundown: return s.AddModule(e) } return nil } -func (s *snapshotter) Write(e *kevent.Kevent) error { +func (s *snapshotter) Write(e *event.Event) error { s.mu.Lock() defer s.mu.Unlock() processCount.Add(1) - pid, err := e.Kparams.GetPid() + pid, err := e.Params.GetPid() if err != nil { return err } - ppid, err := e.Kparams.GetPpid() + ppid, err := e.Params.GetPpid() if err != nil { return err } @@ -172,8 +171,8 @@ func (s *snapshotter) Write(e *kevent.Kevent) error { return err } -func (s *snapshotter) AddThread(e *kevent.Kevent) error { - pid, err := e.Kparams.GetPid() +func (s *snapshotter) AddThread(e *event.Event) error { + pid, err := e.Params.GetPid() if err != nil { return err } @@ -187,23 +186,23 @@ func (s *snapshotter) AddThread(e *kevent.Kevent) error { } thread := pstypes.Thread{} - thread.Tid, _ = e.Kparams.GetTid() - thread.UstackBase = e.Kparams.TryGetAddress(kparams.UstackBase) - thread.UstackLimit = e.Kparams.TryGetAddress(kparams.UstackLimit) - thread.KstackBase = e.Kparams.TryGetAddress(kparams.KstackBase) - thread.KstackLimit = e.Kparams.TryGetAddress(kparams.KstackLimit) - thread.IOPrio, _ = e.Kparams.GetUint8(kparams.IOPrio) - thread.BasePrio, _ = e.Kparams.GetUint8(kparams.BasePrio) - thread.PagePrio, _ = e.Kparams.GetUint8(kparams.PagePrio) - thread.StartAddress = e.Kparams.TryGetAddress(kparams.StartAddress) + thread.Tid, _ = e.Params.GetTid() + thread.UstackBase = e.Params.TryGetAddress(params.UstackBase) + thread.UstackLimit = e.Params.TryGetAddress(params.UstackLimit) + thread.KstackBase = e.Params.TryGetAddress(params.KstackBase) + thread.KstackLimit = e.Params.TryGetAddress(params.KstackLimit) + thread.IOPrio, _ = e.Params.GetUint8(params.IOPrio) + thread.BasePrio, _ = e.Params.GetUint8(params.BasePrio) + thread.PagePrio, _ = e.Params.GetUint8(params.PagePrio) + thread.StartAddress = e.Params.TryGetAddress(params.StartAddress) proc.AddThread(thread) return nil } -func (s *snapshotter) AddModule(e *kevent.Kevent) error { - pid, err := e.Kparams.GetPid() +func (s *snapshotter) AddModule(e *event.Event) error { + pid, err := e.Params.GetPid() if err != nil { return err } @@ -221,13 +220,13 @@ func (s *snapshotter) AddModule(e *kevent.Kevent) error { } module := pstypes.Module{} - module.Size, _ = e.Kparams.GetUint64(kparams.ImageSize) - module.Checksum, _ = e.Kparams.GetUint32(kparams.ImageCheckSum) - module.Name = e.GetParamAsString(kparams.ImagePath) - module.BaseAddress = e.Kparams.TryGetAddress(kparams.ImageBase) - module.DefaultBaseAddress = e.Kparams.TryGetAddress(kparams.ImageDefaultBase) - module.SignatureLevel, _ = e.Kparams.GetUint32(kparams.ImageSignatureLevel) - module.SignatureType, _ = e.Kparams.GetUint32(kparams.ImageSignatureType) + module.Size, _ = e.Params.GetUint64(params.ImageSize) + module.Checksum, _ = e.Params.GetUint32(params.ImageCheckSum) + module.Name = e.GetParamAsString(params.ImagePath) + module.BaseAddress = e.Params.TryGetAddress(params.ImageBase) + module.DefaultBaseAddress = e.Params.TryGetAddress(params.ImageDefaultBase) + module.SignatureLevel, _ = e.Params.GetUint32(params.ImageSignatureLevel) + module.SignatureType, _ = e.Params.GetUint32(params.ImageSignatureType) if strings.EqualFold(proc.Name, filepath.Base(module.Name)) && len(proc.Exe) < len(module.Name) { // if the module is loaded for the process executable, and @@ -278,7 +277,7 @@ func (s *snapshotter) FindModule(addr va.Address) (bool, *pstypes.Module) { return false, nil } -func (s *snapshotter) AddMmap(e *kevent.Kevent) error { +func (s *snapshotter) AddMmap(e *event.Event) error { s.mu.Lock() defer s.mu.Unlock() proc, ok := s.procs[e.PID] @@ -289,11 +288,11 @@ func (s *snapshotter) AddMmap(e *kevent.Kevent) error { mmapCount.Add(1) mmap := pstypes.Mmap{} - mmap.File = e.GetParamAsString(kparams.FilePath) - mmap.BaseAddress = e.Kparams.TryGetAddress(kparams.FileViewBase) - mmap.Size, _ = e.Kparams.GetUint64(kparams.FileViewSize) - mmap.Protection, _ = e.Kparams.GetUint32(kparams.MemProtect) - mmap.Type = e.GetParamAsString(kparams.FileViewSectionType) + mmap.File = e.GetParamAsString(params.FilePath) + mmap.BaseAddress = e.Params.TryGetAddress(params.FileViewBase) + mmap.Size, _ = e.Params.GetUint64(params.FileViewSize) + mmap.Protection, _ = e.Params.GetUint32(params.MemProtect) + mmap.Type = e.GetParamAsString(params.FileViewSectionType) proc.AddMmap(mmap) @@ -317,29 +316,29 @@ func (s *snapshotter) Close() error { return nil } -func (s *snapshotter) newProcState(pid, ppid uint32, e *kevent.Kevent) (*pstypes.PS, error) { +func (s *snapshotter) newProcState(pid, ppid uint32, e *event.Event) (*pstypes.PS, error) { proc := pstypes.New( pid, ppid, - e.GetParamAsString(kparams.ProcessName), - e.GetParamAsString(kparams.Cmdline), - e.GetParamAsString(kparams.Exe), - e.Kparams.MustGetSID(), - e.Kparams.MustGetUint32(kparams.SessionID), + e.GetParamAsString(params.ProcessName), + e.GetParamAsString(params.Cmdline), + e.GetParamAsString(params.Exe), + e.Params.MustGetSID(), + e.Params.MustGetUint32(params.SessionID), ) proc.Parent = s.procs[ppid] - proc.StartTime, _ = e.Kparams.GetTime(kparams.StartTime) - proc.IsWOW64 = (e.Kparams.MustGetUint32(kparams.ProcessFlags) & kevent.PsWOW64) != 0 - proc.IsPackaged = (e.Kparams.MustGetUint32(kparams.ProcessFlags) & kevent.PsPackaged) != 0 - proc.IsProtected = (e.Kparams.MustGetUint32(kparams.ProcessFlags) & kevent.PsProtected) != 0 + proc.StartTime, _ = e.Params.GetTime(params.StartTime) + proc.IsWOW64 = (e.Params.MustGetUint32(params.ProcessFlags) & event.PsWOW64) != 0 + proc.IsPackaged = (e.Params.MustGetUint32(params.ProcessFlags) & event.PsPackaged) != 0 + proc.IsProtected = (e.Params.MustGetUint32(params.ProcessFlags) & event.PsProtected) != 0 if !s.capture { if proc.Username != "" { - e.AppendParam(kparams.Username, kparams.UnicodeString, proc.Username) + e.AppendParam(params.Username, params.UnicodeString, proc.Username) } if proc.Domain != "" { - e.AppendParam(kparams.Domain, kparams.UnicodeString, proc.Domain) + e.AppendParam(params.Domain, params.UnicodeString, proc.Domain) } // retrieve process handles var err error @@ -352,8 +351,8 @@ func (s *snapshotter) newProcState(pid, ppid uint32, e *kevent.Kevent) (*pstypes // return early if we're reading from the capture file if s.capture { // reset username/domain from captured event parameters - proc.Domain = e.GetParamAsString(kparams.Domain) - proc.Username = e.GetParamAsString(kparams.Username) + proc.Domain = e.GetParamAsString(params.Domain) + proc.Username = e.GetParamAsString(params.Username) return proc, nil } @@ -387,10 +386,10 @@ func (s *snapshotter) newProcState(pid, ppid uint32, e *kevent.Kevent) (*pstypes return proc, nil } -func (s *snapshotter) Remove(e *kevent.Kevent) error { +func (s *snapshotter) Remove(e *event.Event) error { s.mu.Lock() defer s.mu.Unlock() - pid, err := e.Kparams.GetPid() + pid, err := e.Params.GetPid() if err != nil { return err } diff --git a/pkg/ps/snapshotter_windows_test.go b/pkg/ps/snapshotter_windows_test.go index 69f81740a..3ec17f499 100644 --- a/pkg/ps/snapshotter_windows_test.go +++ b/pkg/ps/snapshotter_windows_test.go @@ -20,11 +20,10 @@ package ps import ( "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/handle" htypes "github.com/rabbitstack/fibratus/pkg/handle/types" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" "github.com/rabbitstack/fibratus/pkg/sys" "github.com/rabbitstack/fibratus/pkg/util/va" @@ -47,22 +46,22 @@ func TestWrite(t *testing.T) { var tests = []struct { name string - evt *kevent.Kevent + evt *event.Event want *pstypes.PS }{ {"write state from spawned process", - &kevent.Kevent{ - Type: ktypes.CreateProcess, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(os.Getpid())}, - kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.PID, Value: uint32(os.Getppid())}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.UnicodeString, Value: "spotify.exe"}, - kparams.Cmdline: {Name: kparams.Cmdline, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`}, - kparams.Exe: {Name: kparams.Exe, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --parent`}, - kparams.UserSID: {Name: kparams.UserSID, Type: kparams.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, - kparams.StartTime: {Name: kparams.StartTime, Type: kparams.Time, Value: time.Now()}, - kparams.SessionID: {Name: kparams.SessionID, Type: kparams.Uint32, Value: uint32(1)}, - kparams.ProcessFlags: {Name: kparams.ProcessFlags, Type: kparams.Flags, Value: uint32(0x00000010)}, + &event.Event{ + Type: event.CreateProcess, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(os.Getpid())}, + params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(os.Getppid())}, + params.ProcessName: {Name: params.ProcessName, Type: params.UnicodeString, Value: "spotify.exe"}, + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`}, + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --parent`}, + params.UserSID: {Name: params.UserSID, Type: params.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, + params.StartTime: {Name: params.StartTime, Type: params.Time, Value: time.Now()}, + params.SessionID: {Name: params.SessionID, Type: params.Uint32, Value: uint32(1)}, + params.ProcessFlags: {Name: params.ProcessFlags, Type: params.Flags, Value: uint32(0x00000010)}, }, }, &pstypes.PS{ @@ -82,18 +81,18 @@ func TestWrite(t *testing.T) { }, }, {"write state from spawned process with parent", - &kevent.Kevent{ - Type: ktypes.CreateProcess, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(os.Getppid())}, - kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.PID, Value: uint32(os.Getpid())}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.UnicodeString, Value: "spotify.exe"}, - kparams.Cmdline: {Name: kparams.Cmdline, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`}, - kparams.Exe: {Name: kparams.Exe, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --parent`}, - kparams.UserSID: {Name: kparams.UserSID, Type: kparams.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, - kparams.StartTime: {Name: kparams.StartTime, Type: kparams.Time, Value: time.Now()}, - kparams.SessionID: {Name: kparams.SessionID, Type: kparams.Uint32, Value: uint32(1)}, - kparams.ProcessFlags: {Name: kparams.ProcessFlags, Type: kparams.Flags, Value: uint32(0x00000010)}, + &event.Event{ + Type: event.CreateProcess, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(os.Getppid())}, + params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(os.Getpid())}, + params.ProcessName: {Name: params.ProcessName, Type: params.UnicodeString, Value: "spotify.exe"}, + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`}, + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --parent`}, + params.UserSID: {Name: params.UserSID, Type: params.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, + params.StartTime: {Name: params.StartTime, Type: params.Time, Value: time.Now()}, + params.SessionID: {Name: params.SessionID, Type: params.Uint32, Value: uint32(1)}, + params.ProcessFlags: {Name: params.ProcessFlags, Type: params.Flags, Value: uint32(0x00000010)}, }, PID: uint32(os.Getpid()), }, @@ -117,18 +116,18 @@ func TestWrite(t *testing.T) { }, }, {"write state from rundown event", - &kevent.Kevent{ - Type: ktypes.ProcessRundown, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(os.Getpid())}, - kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.PID, Value: uint32(8390)}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.UnicodeString, Value: "spotify.exe"}, - kparams.Cmdline: {Name: kparams.Cmdline, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`}, - kparams.Exe: {Name: kparams.Exe, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --parent`}, - kparams.UserSID: {Name: kparams.UserSID, Type: kparams.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, - kparams.StartTime: {Name: kparams.StartTime, Type: kparams.Time, Value: time.Now()}, - kparams.SessionID: {Name: kparams.SessionID, Type: kparams.Uint32, Value: uint32(1)}, - kparams.ProcessFlags: {Name: kparams.ProcessFlags, Type: kparams.Flags, Value: uint32(0x00000010)}, + &event.Event{ + Type: event.ProcessRundown, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(os.Getpid())}, + params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(8390)}, + params.ProcessName: {Name: params.ProcessName, Type: params.UnicodeString, Value: "spotify.exe"}, + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`}, + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --parent`}, + params.UserSID: {Name: params.UserSID, Type: params.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, + params.StartTime: {Name: params.StartTime, Type: params.Time, Value: time.Now()}, + params.SessionID: {Name: params.SessionID, Type: params.Uint32, Value: uint32(1)}, + params.ProcessFlags: {Name: params.ProcessFlags, Type: params.Flags, Value: uint32(0x00000010)}, }, }, &pstypes.PS{ @@ -157,7 +156,7 @@ func TestWrite(t *testing.T) { require.NoError(t, psnap.Write(evt)) require.True(t, psnap.Size() > 0) - ok, proc := psnap.Find(evt.Kparams.MustGetPid()) + ok, proc := psnap.Find(evt.Params.MustGetPid()) require.True(t, ok) require.NotNil(t, proc) assert.Equal(t, ps.PID, proc.PID) @@ -194,22 +193,22 @@ func TestRemove(t *testing.T) { var tests = []struct { name string - evt *kevent.Kevent + evt *event.Event want bool }{ {"write and remove process state from snapshotter", - &kevent.Kevent{ - Type: ktypes.CreateProcess, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(os.Getpid())}, - kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.PID, Value: uint32(os.Getppid())}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.UnicodeString, Value: "spotify.exe"}, - kparams.Cmdline: {Name: kparams.Cmdline, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`}, - kparams.Exe: {Name: kparams.Exe, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --parent`}, - kparams.UserSID: {Name: kparams.UserSID, Type: kparams.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, - kparams.StartTime: {Name: kparams.StartTime, Type: kparams.Time, Value: time.Now()}, - kparams.SessionID: {Name: kparams.SessionID, Type: kparams.Uint32, Value: uint32(1)}, - kparams.ProcessFlags: {Name: kparams.ProcessFlags, Type: kparams.Flags, Value: uint32(0x00000010)}, + &event.Event{ + Type: event.CreateProcess, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(os.Getpid())}, + params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(os.Getppid())}, + params.ProcessName: {Name: params.ProcessName, Type: params.UnicodeString, Value: "spotify.exe"}, + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`}, + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --parent`}, + params.UserSID: {Name: params.UserSID, Type: params.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, + params.StartTime: {Name: params.StartTime, Type: params.Time, Value: time.Now()}, + params.SessionID: {Name: params.SessionID, Type: params.Uint32, Value: uint32(1)}, + params.ProcessFlags: {Name: params.ProcessFlags, Type: params.Flags, Value: uint32(0x00000010)}, }, }, false, @@ -226,7 +225,7 @@ func TestRemove(t *testing.T) { require.NoError(t, psnap.Remove(evt)) // process in dirty map, wait 6 seconds before lookup time.Sleep(time.Second * 6) - ok, _ := psnap.Find(evt.Kparams.MustGetPid()) + ok, _ := psnap.Find(evt.Params.MustGetPid()) assert.Equal(t, exists, ok) }) } @@ -238,59 +237,59 @@ func TestAddThread(t *testing.T) { psnap := NewSnapshotter(hsnap, &config.Config{}) defer psnap.Close() - evt := &kevent.Kevent{ - Type: ktypes.CreateProcess, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(os.Getpid())}, - kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.PID, Value: uint32(os.Getppid())}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.UnicodeString, Value: "spotify.exe"}, - kparams.Cmdline: {Name: kparams.Cmdline, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`}, - kparams.Exe: {Name: kparams.Exe, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --parent`}, - kparams.UserSID: {Name: kparams.UserSID, Type: kparams.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, - kparams.StartTime: {Name: kparams.StartTime, Type: kparams.Time, Value: time.Now()}, - kparams.SessionID: {Name: kparams.SessionID, Type: kparams.Uint32, Value: uint32(1)}, - kparams.ProcessFlags: {Name: kparams.ProcessFlags, Type: kparams.Flags, Value: uint32(0x00000010)}, + evt := &event.Event{ + Type: event.CreateProcess, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(os.Getpid())}, + params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(os.Getppid())}, + params.ProcessName: {Name: params.ProcessName, Type: params.UnicodeString, Value: "spotify.exe"}, + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`}, + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --parent`}, + params.UserSID: {Name: params.UserSID, Type: params.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, + params.StartTime: {Name: params.StartTime, Type: params.Time, Value: time.Now()}, + params.SessionID: {Name: params.SessionID, Type: params.Uint32, Value: uint32(1)}, + params.ProcessFlags: {Name: params.ProcessFlags, Type: params.Flags, Value: uint32(0x00000010)}, }, } require.NoError(t, psnap.Write(evt)) var tests = []struct { name string - evt *kevent.Kevent + evt *event.Event want bool }{ {"add thread to existing process", - &kevent.Kevent{ - Type: ktypes.CreateThread, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(os.Getpid())}, - kparams.ThreadID: {Name: kparams.ThreadID, Type: kparams.TID, Value: uint32(3453)}, - kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Uint8, Value: uint8(13)}, - kparams.StartAddress: {Name: kparams.StartAddress, Type: kparams.Address, Value: uint64(140729524944768)}, - kparams.IOPrio: {Name: kparams.IOPrio, Type: kparams.Uint8, Value: uint8(2)}, - kparams.KstackBase: {Name: kparams.KstackBase, Type: kparams.Address, Value: uint64(18446677035730165760)}, - kparams.KstackLimit: {Name: kparams.KstackLimit, Type: kparams.Address, Value: uint64(18446677035730137088)}, - kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(5)}, - kparams.UstackBase: {Name: kparams.UstackBase, Type: kparams.Address, Value: uint64(86376448)}, - kparams.UstackLimit: {Name: kparams.UstackLimit, Type: kparams.Address, Value: uint64(86372352)}, + &event.Event{ + Type: event.CreateThread, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(os.Getpid())}, + params.ThreadID: {Name: params.ThreadID, Type: params.TID, Value: uint32(3453)}, + params.BasePrio: {Name: params.BasePrio, Type: params.Uint8, Value: uint8(13)}, + params.StartAddress: {Name: params.StartAddress, Type: params.Address, Value: uint64(140729524944768)}, + params.IOPrio: {Name: params.IOPrio, Type: params.Uint8, Value: uint8(2)}, + params.KstackBase: {Name: params.KstackBase, Type: params.Address, Value: uint64(18446677035730165760)}, + params.KstackLimit: {Name: params.KstackLimit, Type: params.Address, Value: uint64(18446677035730137088)}, + params.PagePrio: {Name: params.PagePrio, Type: params.Uint8, Value: uint8(5)}, + params.UstackBase: {Name: params.UstackBase, Type: params.Address, Value: uint64(86376448)}, + params.UstackLimit: {Name: params.UstackLimit, Type: params.Address, Value: uint64(86372352)}, }, }, true, }, {"add thread to absent process", - &kevent.Kevent{ - Type: ktypes.CreateThread, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(os.Getpid() + 1)}, - kparams.ThreadID: {Name: kparams.ThreadID, Type: kparams.TID, Value: uint32(3453)}, - kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Uint8, Value: uint8(13)}, - kparams.StartAddress: {Name: kparams.StartAddress, Type: kparams.Address, Value: uint64(140729524944768)}, - kparams.IOPrio: {Name: kparams.IOPrio, Type: kparams.Uint8, Value: uint8(2)}, - kparams.KstackBase: {Name: kparams.KstackBase, Type: kparams.Address, Value: uint64(18446677035730165760)}, - kparams.KstackLimit: {Name: kparams.KstackLimit, Type: kparams.Address, Value: uint64(18446677035730137088)}, - kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(5)}, - kparams.UstackBase: {Name: kparams.UstackBase, Type: kparams.Address, Value: uint64(86376448)}, - kparams.UstackLimit: {Name: kparams.UstackLimit, Type: kparams.Address, Value: uint64(86372352)}, + &event.Event{ + Type: event.CreateThread, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(os.Getpid() + 1)}, + params.ThreadID: {Name: params.ThreadID, Type: params.TID, Value: uint32(3453)}, + params.BasePrio: {Name: params.BasePrio, Type: params.Uint8, Value: uint8(13)}, + params.StartAddress: {Name: params.StartAddress, Type: params.Address, Value: uint64(140729524944768)}, + params.IOPrio: {Name: params.IOPrio, Type: params.Uint8, Value: uint8(2)}, + params.KstackBase: {Name: params.KstackBase, Type: params.Address, Value: uint64(18446677035730165760)}, + params.KstackLimit: {Name: params.KstackLimit, Type: params.Address, Value: uint64(18446677035730137088)}, + params.PagePrio: {Name: params.PagePrio, Type: params.Uint8, Value: uint8(5)}, + params.UstackBase: {Name: params.UstackBase, Type: params.Address, Value: uint64(86376448)}, + params.UstackLimit: {Name: params.UstackLimit, Type: params.Address, Value: uint64(86372352)}, }, }, false, @@ -303,11 +302,11 @@ func TestAddThread(t *testing.T) { exists := tt.want require.NoError(t, psnap.AddThread(evt)) - ok, proc := psnap.Find(evt.Kparams.MustGetPid()) + ok, proc := psnap.Find(evt.Params.MustGetPid()) require.Equal(t, exists, ok) if ok { - assert.Contains(t, proc.Threads, evt.Kparams.MustGetTid()) - assert.Equal(t, va.Address(140729524944768), proc.Threads[evt.Kparams.MustGetTid()].StartAddress) + assert.Contains(t, proc.Threads, evt.Params.MustGetTid()) + assert.Equal(t, va.Address(140729524944768), proc.Threads[evt.Params.MustGetTid()].StartAddress) } }) } @@ -319,35 +318,35 @@ func TestRemoveThread(t *testing.T) { hsnap.On("FindHandles", mock.Anything).Return([]htypes.Handle{}, nil) defer psnap.Close() - pevt := &kevent.Kevent{ - Type: ktypes.CreateProcess, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(os.Getpid())}, - kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.PID, Value: uint32(os.Getppid())}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.UnicodeString, Value: "spotify.exe"}, - kparams.Cmdline: {Name: kparams.Cmdline, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`}, - kparams.Exe: {Name: kparams.Exe, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --parent`}, - kparams.UserSID: {Name: kparams.UserSID, Type: kparams.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, - kparams.StartTime: {Name: kparams.StartTime, Type: kparams.Time, Value: time.Now()}, - kparams.SessionID: {Name: kparams.SessionID, Type: kparams.Uint32, Value: uint32(1)}, - kparams.ProcessFlags: {Name: kparams.ProcessFlags, Type: kparams.Flags, Value: uint32(0x00000010)}, + pevt := &event.Event{ + Type: event.CreateProcess, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(os.Getpid())}, + params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(os.Getppid())}, + params.ProcessName: {Name: params.ProcessName, Type: params.UnicodeString, Value: "spotify.exe"}, + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`}, + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --parent`}, + params.UserSID: {Name: params.UserSID, Type: params.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, + params.StartTime: {Name: params.StartTime, Type: params.Time, Value: time.Now()}, + params.SessionID: {Name: params.SessionID, Type: params.Uint32, Value: uint32(1)}, + params.ProcessFlags: {Name: params.ProcessFlags, Type: params.Flags, Value: uint32(0x00000010)}, }, } require.NoError(t, psnap.Write(pevt)) - tevt := &kevent.Kevent{ - Type: ktypes.CreateThread, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(os.Getpid())}, - kparams.ThreadID: {Name: kparams.ThreadID, Type: kparams.TID, Value: uint32(3453)}, - kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Uint8, Value: uint8(13)}, - kparams.StartAddress: {Name: kparams.StartAddress, Type: kparams.Address, Value: uint64(140729524944768)}, - kparams.IOPrio: {Name: kparams.IOPrio, Type: kparams.Uint8, Value: uint8(2)}, - kparams.KstackBase: {Name: kparams.KstackBase, Type: kparams.Address, Value: uint64(18446677035730165760)}, - kparams.KstackLimit: {Name: kparams.KstackLimit, Type: kparams.Address, Value: uint64(18446677035730137088)}, - kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(5)}, - kparams.UstackBase: {Name: kparams.UstackBase, Type: kparams.Address, Value: uint64(86376448)}, - kparams.UstackLimit: {Name: kparams.UstackLimit, Type: kparams.Address, Value: uint64(86372352)}, + tevt := &event.Event{ + Type: event.CreateThread, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(os.Getpid())}, + params.ThreadID: {Name: params.ThreadID, Type: params.TID, Value: uint32(3453)}, + params.BasePrio: {Name: params.BasePrio, Type: params.Uint8, Value: uint8(13)}, + params.StartAddress: {Name: params.StartAddress, Type: params.Address, Value: uint64(140729524944768)}, + params.IOPrio: {Name: params.IOPrio, Type: params.Uint8, Value: uint8(2)}, + params.KstackBase: {Name: params.KstackBase, Type: params.Address, Value: uint64(18446677035730165760)}, + params.KstackLimit: {Name: params.KstackLimit, Type: params.Address, Value: uint64(18446677035730137088)}, + params.PagePrio: {Name: params.PagePrio, Type: params.Uint8, Value: uint8(5)}, + params.UstackBase: {Name: params.UstackBase, Type: params.Address, Value: uint64(86376448)}, + params.UstackLimit: {Name: params.UstackLimit, Type: params.Address, Value: uint64(86372352)}, }, } @@ -367,43 +366,43 @@ func TestAddModule(t *testing.T) { psnap := NewSnapshotter(hsnap, &config.Config{}) defer psnap.Close() - evt := &kevent.Kevent{ - Type: ktypes.CreateProcess, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(os.Getpid())}, - kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.PID, Value: uint32(os.Getppid())}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.UnicodeString, Value: "spotify.exe"}, - kparams.Cmdline: {Name: kparams.Cmdline, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`}, - kparams.Exe: {Name: kparams.Exe, Type: kparams.UnicodeString, Value: `Spotify.exe`}, - kparams.UserSID: {Name: kparams.UserSID, Type: kparams.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, - kparams.StartTime: {Name: kparams.StartTime, Type: kparams.Time, Value: time.Now()}, - kparams.SessionID: {Name: kparams.SessionID, Type: kparams.Uint32, Value: uint32(1)}, - kparams.ProcessFlags: {Name: kparams.ProcessFlags, Type: kparams.Flags, Value: uint32(0x00000010)}, + evt := &event.Event{ + Type: event.CreateProcess, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(os.Getpid())}, + params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(os.Getppid())}, + params.ProcessName: {Name: params.ProcessName, Type: params.UnicodeString, Value: "spotify.exe"}, + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`}, + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: `Spotify.exe`}, + params.UserSID: {Name: params.UserSID, Type: params.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, + params.StartTime: {Name: params.StartTime, Type: params.Time, Value: time.Now()}, + params.SessionID: {Name: params.SessionID, Type: params.Uint32, Value: uint32(1)}, + params.ProcessFlags: {Name: params.ProcessFlags, Type: params.Flags, Value: uint32(0x00000010)}, }, } require.NoError(t, psnap.Write(evt)) var tests = []struct { name string - evt *kevent.Kevent + evt *event.Event want bool }{ {"add module to existing process", - &kevent.Kevent{ - Type: ktypes.LoadImage, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(os.Getpid())}, - kparams.ImagePath: {Name: kparams.ImagePath, Type: kparams.UnicodeString, Value: "C:\\Users\\admin\\AppData\\Roaming\\Spotify\\Spotify.exe"}, + &event.Event{ + Type: event.LoadImage, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(os.Getpid())}, + params.ImagePath: {Name: params.ImagePath, Type: params.UnicodeString, Value: "C:\\Users\\admin\\AppData\\Roaming\\Spotify\\Spotify.exe"}, }, }, true, }, {"add module to absent process", - &kevent.Kevent{ - Type: ktypes.LoadImage, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(os.Getpid() + 1)}, - kparams.ImagePath: {Name: kparams.ImagePath, Type: kparams.UnicodeString, Value: "C:\\Windows\\System32\\notepad.exe"}, + &event.Event{ + Type: event.LoadImage, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(os.Getpid() + 1)}, + params.ImagePath: {Name: params.ImagePath, Type: params.UnicodeString, Value: "C:\\Windows\\System32\\notepad.exe"}, }, }, false, @@ -416,10 +415,10 @@ func TestAddModule(t *testing.T) { exists := tt.want require.NoError(t, psnap.AddModule(evt)) - ok, proc := psnap.Find(evt.Kparams.MustGetPid()) + ok, proc := psnap.Find(evt.Params.MustGetPid()) require.Equal(t, exists, ok) if ok { - require.NotNil(t, proc.FindModule(evt.GetParamAsString(kparams.ImagePath))) + require.NotNil(t, proc.FindModule(evt.GetParamAsString(params.ImagePath))) assert.Equal(t, "C:\\Users\\admin\\AppData\\Roaming\\Spotify\\Spotify.exe", proc.Exe) } }) @@ -432,28 +431,28 @@ func TestRemoveModule(t *testing.T) { psnap := NewSnapshotter(hsnap, &config.Config{}) defer psnap.Close() - pevt := &kevent.Kevent{ - Type: ktypes.CreateProcess, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(os.Getpid())}, - kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.PID, Value: uint32(os.Getppid())}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.UnicodeString, Value: "spotify.exe"}, - kparams.Cmdline: {Name: kparams.Cmdline, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`}, - kparams.Exe: {Name: kparams.Exe, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --parent`}, - kparams.UserSID: {Name: kparams.UserSID, Type: kparams.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, - kparams.StartTime: {Name: kparams.StartTime, Type: kparams.Time, Value: time.Now()}, - kparams.SessionID: {Name: kparams.SessionID, Type: kparams.Uint32, Value: uint32(1)}, - kparams.ProcessFlags: {Name: kparams.ProcessFlags, Type: kparams.Flags, Value: uint32(0x00000010)}, + pevt := &event.Event{ + Type: event.CreateProcess, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(os.Getpid())}, + params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(os.Getppid())}, + params.ProcessName: {Name: params.ProcessName, Type: params.UnicodeString, Value: "spotify.exe"}, + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`}, + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --parent`}, + params.UserSID: {Name: params.UserSID, Type: params.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, + params.StartTime: {Name: params.StartTime, Type: params.Time, Value: time.Now()}, + params.SessionID: {Name: params.SessionID, Type: params.Uint32, Value: uint32(1)}, + params.ProcessFlags: {Name: params.ProcessFlags, Type: params.Flags, Value: uint32(0x00000010)}, }, } require.NoError(t, psnap.Write(pevt)) - mevt := &kevent.Kevent{ - Type: ktypes.LoadImage, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(os.Getpid())}, - kparams.ImagePath: {Name: kparams.ImagePath, Type: kparams.UnicodeString, Value: "C:\\Windows\\System32\\notepad.exe"}, - kparams.ImageBase: {Name: kparams.ImageBase, Type: kparams.Address, Value: uint64(0xffff7656)}, + mevt := &event.Event{ + Type: event.LoadImage, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(os.Getpid())}, + params.ImagePath: {Name: params.ImagePath, Type: params.UnicodeString, Value: "C:\\Windows\\System32\\notepad.exe"}, + params.ImageBase: {Name: params.ImageBase, Type: params.Address, Value: uint64(0xffff7656)}, }, } @@ -473,50 +472,50 @@ func TestOverrideProcExecutable(t *testing.T) { psnap := NewSnapshotter(hsnap, &config.Config{}) defer psnap.Close() - evt := &kevent.Kevent{ - Type: ktypes.CreateProcess, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(os.Getpid())}, - kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.PID, Value: uint32(os.Getppid())}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.UnicodeString, Value: "spotify.exe"}, - kparams.Cmdline: {Name: kparams.Cmdline, Type: kparams.UnicodeString, Value: `Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`}, - kparams.Exe: {Name: kparams.Exe, Type: kparams.UnicodeString, Value: `Spotify.exe`}, - kparams.UserSID: {Name: kparams.UserSID, Type: kparams.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, - kparams.StartTime: {Name: kparams.StartTime, Type: kparams.Time, Value: time.Now()}, - kparams.SessionID: {Name: kparams.SessionID, Type: kparams.Uint32, Value: uint32(1)}, - kparams.ProcessFlags: {Name: kparams.ProcessFlags, Type: kparams.Flags, Value: uint32(0x00000010)}, + evt := &event.Event{ + Type: event.CreateProcess, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(os.Getpid())}, + params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(os.Getppid())}, + params.ProcessName: {Name: params.ProcessName, Type: params.UnicodeString, Value: "spotify.exe"}, + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: `Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`}, + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: `Spotify.exe`}, + params.UserSID: {Name: params.UserSID, Type: params.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, + params.StartTime: {Name: params.StartTime, Type: params.Time, Value: time.Now()}, + params.SessionID: {Name: params.SessionID, Type: params.Uint32, Value: uint32(1)}, + params.ProcessFlags: {Name: params.ProcessFlags, Type: params.Flags, Value: uint32(0x00000010)}, }, } require.NoError(t, psnap.Write(evt)) var tests = []struct { expectedExe string - evt *kevent.Kevent + evt *event.Event }{ {`Spotify.exe`, - &kevent.Kevent{ - Type: ktypes.LoadImage, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(os.Getpid())}, - kparams.ImagePath: {Name: kparams.ImagePath, Type: kparams.UnicodeString, Value: "C:\\Windows\\assembly\\NativeImages_v4.0.30319_32\\Microsoft.Dee252aac#\\707569faabe821b47fa4f59ecd9eb6ea\\Microsoft.Developer.IdentityService.ni.exe"}, + &event.Event{ + Type: event.LoadImage, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(os.Getpid())}, + params.ImagePath: {Name: params.ImagePath, Type: params.UnicodeString, Value: "C:\\Windows\\assembly\\NativeImages_v4.0.30319_32\\Microsoft.Dee252aac#\\707569faabe821b47fa4f59ecd9eb6ea\\Microsoft.Developer.IdentityService.ni.exe"}, }, }, }, {`Spotify.exe`, - &kevent.Kevent{ - Type: ktypes.LoadImage, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(os.Getpid())}, - kparams.ImagePath: {Name: kparams.ImagePath, Type: kparams.UnicodeString, Value: "C:\\Windows\\System32\\notepad.exe"}, + &event.Event{ + Type: event.LoadImage, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(os.Getpid())}, + params.ImagePath: {Name: params.ImagePath, Type: params.UnicodeString, Value: "C:\\Windows\\System32\\notepad.exe"}, }, }, }, {`C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe`, - &kevent.Kevent{ - Type: ktypes.LoadImage, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(os.Getpid())}, - kparams.ImagePath: {Name: kparams.ImagePath, Type: kparams.UnicodeString, Value: "C:\\Users\\admin\\AppData\\Roaming\\Spotify\\Spotify.exe"}, + &event.Event{ + Type: event.LoadImage, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(os.Getpid())}, + params.ImagePath: {Name: params.ImagePath, Type: params.UnicodeString, Value: "C:\\Users\\admin\\AppData\\Roaming\\Spotify\\Spotify.exe"}, }, }, }, @@ -548,33 +547,33 @@ func TestReapDeadProcesses(t *testing.T) { t.Fatal("unable to spawn notepad process") } - evts := []*kevent.Kevent{ + evts := []*event.Event{ { - Type: ktypes.CreateProcess, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: notepadPID}, - kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.PID, Value: uint32(8390)}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.UnicodeString, Value: "notepad.exe"}, - kparams.Cmdline: {Name: kparams.Cmdline, Type: kparams.UnicodeString, Value: `c:\\windows\\system32\\notepad.exe`}, - kparams.Exe: {Name: kparams.Exe, Type: kparams.UnicodeString, Value: `c:\\windows\\system32\\notepad.exe`}, - kparams.UserSID: {Name: kparams.UserSID, Type: kparams.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, - kparams.StartTime: {Name: kparams.StartTime, Type: kparams.Time, Value: time.Now()}, - kparams.SessionID: {Name: kparams.SessionID, Type: kparams.Uint32, Value: uint32(1)}, - kparams.ProcessFlags: {Name: kparams.ProcessFlags, Type: kparams.Flags, Value: uint32(0x00000010)}, + Type: event.CreateProcess, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: notepadPID}, + params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(8390)}, + params.ProcessName: {Name: params.ProcessName, Type: params.UnicodeString, Value: "notepad.exe"}, + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: `c:\\windows\\system32\\notepad.exe`}, + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: `c:\\windows\\system32\\notepad.exe`}, + params.UserSID: {Name: params.UserSID, Type: params.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, + params.StartTime: {Name: params.StartTime, Type: params.Time, Value: time.Now()}, + params.SessionID: {Name: params.SessionID, Type: params.Uint32, Value: uint32(1)}, + params.ProcessFlags: {Name: params.ProcessFlags, Type: params.Flags, Value: uint32(0x00000010)}, }, }, { - Type: ktypes.CreateProcess, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(os.Getpid())}, - kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.PID, Value: uint32(8390)}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.UnicodeString, Value: "spotify.exe"}, - kparams.Cmdline: {Name: kparams.Cmdline, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`}, - kparams.Exe: {Name: kparams.Exe, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe`}, - kparams.UserSID: {Name: kparams.UserSID, Type: kparams.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, - kparams.StartTime: {Name: kparams.StartTime, Type: kparams.Time, Value: time.Now()}, - kparams.SessionID: {Name: kparams.SessionID, Type: kparams.Uint32, Value: uint32(1)}, - kparams.ProcessFlags: {Name: kparams.ProcessFlags, Type: kparams.Flags, Value: uint32(0x00000010)}, + Type: event.CreateProcess, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(os.Getpid())}, + params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(8390)}, + params.ProcessName: {Name: params.ProcessName, Type: params.UnicodeString, Value: "spotify.exe"}, + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`}, + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe`}, + params.UserSID: {Name: params.UserSID, Type: params.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, + params.StartTime: {Name: params.StartTime, Type: params.Time, Value: time.Now()}, + params.SessionID: {Name: params.SessionID, Type: params.Uint32, Value: uint32(1)}, + params.ProcessFlags: {Name: params.ProcessFlags, Type: params.Flags, Value: uint32(0x00000010)}, }, }, } diff --git a/pkg/ps/types/marshaller_windows.go b/pkg/ps/types/marshaller_windows.go index 2b3d1f41e..613107525 100644 --- a/pkg/ps/types/marshaller_windows.go +++ b/pkg/ps/types/marshaller_windows.go @@ -20,9 +20,9 @@ package types import ( "fmt" + "github.com/rabbitstack/fibratus/pkg/cap/section" + capver "github.com/rabbitstack/fibratus/pkg/cap/version" htypes "github.com/rabbitstack/fibratus/pkg/handle/types" - "github.com/rabbitstack/fibratus/pkg/kcap/section" - kcapver "github.com/rabbitstack/fibratus/pkg/kcap/version" "github.com/rabbitstack/fibratus/pkg/pe" "github.com/rabbitstack/fibratus/pkg/util/bytes" "github.com/rabbitstack/fibratus/pkg/util/convert" @@ -74,7 +74,7 @@ func (ps *PS) Marshal() []byte { } // write handles - sec := section.New(section.Handle, kcapver.HandleSecV1, uint32(len(ps.Handles)), 0) + sec := section.New(section.Handle, capver.HandleSecV1, uint32(len(ps.Handles)), 0) b = append(b, sec[:]...) for _, handle := range ps.Handles { buf := handle.Marshal() @@ -85,11 +85,11 @@ func (ps *PS) Marshal() []byte { // write the PE metadata if ps.PE != nil { buf := ps.PE.Marshal() - sec := section.New(section.PE, kcapver.PESecV2, 0, uint32(len(buf))) + sec := section.New(section.PE, capver.PESecV2, 0, uint32(len(buf))) b = append(b, sec[:]...) b = append(b, buf...) } else { - sec := section.New(section.PE, kcapver.PESecV2, 0, 0) + sec := section.New(section.PE, capver.PESecV2, 0, 0) b = append(b, sec[:]...) } @@ -174,7 +174,7 @@ func (ps *PS) Unmarshal(b []byte, psec section.Section) error { offset += uint32(aoffset) idx := uint32(20) // read session ID - if psec.Version() >= kcapver.ProcessSecV3 { + if psec.Version() >= capver.ProcessSecV3 { // session identifier was changed from uint8 to uint32 ps.SessionID = bytes.ReadUint32(b[idx+offset:]) idx += 4 @@ -226,7 +226,7 @@ readpe: sec = section.Read(b[idx+offset:]) idx += 10 if sec.Size() == 0 { - if psec.Version() >= kcapver.ProcessSecV2 { + if psec.Version() >= capver.ProcessSecV2 { // read start time l := uint32(bytes.ReadUint16(b[idx+offset:])) idx += 2 @@ -243,7 +243,7 @@ readpe: // read UUID ps.uuid = bytes.ReadUint64(b[idx+offset:]) } - if psec.Version() >= kcapver.ProcessSecV3 { + if psec.Version() >= capver.ProcessSecV3 { idx += 8 // read username l := bytes.ReadUint16(b[idx+offset:]) @@ -259,7 +259,7 @@ readpe: offset += uint32(l) ps.Domain = string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l]) } - if psec.Version() >= kcapver.ProcessSecV4 { + if psec.Version() >= capver.ProcessSecV4 { // process flags ps.IsWOW64 = convert.Itob(b[idx+offset]) idx++ @@ -278,7 +278,7 @@ readpe: } offset += sec.Size() - if psec.Version() >= kcapver.ProcessSecV2 { + if psec.Version() >= capver.ProcessSecV2 { // read start time l := uint32(bytes.ReadUint16(b[idx+offset:])) idx += 2 @@ -290,7 +290,7 @@ readpe: // read UUID ps.uuid = bytes.ReadUint64(b[idx+offset:]) } - if psec.Version() >= kcapver.ProcessSecV3 { + if psec.Version() >= capver.ProcessSecV3 { idx += 8 // read username l := bytes.ReadUint16(b[idx+offset:]) @@ -306,7 +306,7 @@ readpe: offset += uint32(l) ps.Domain = string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l]) } - if psec.Version() >= kcapver.ProcessSecV4 { + if psec.Version() >= capver.ProcessSecV4 { // process flags ps.IsWOW64 = convert.Itob(b[idx+offset]) idx++ diff --git a/pkg/ps/types/marshaller_windows_test.go b/pkg/ps/types/marshaller_windows_test.go index e039ae7ca..83f279ebd 100644 --- a/pkg/ps/types/marshaller_windows_test.go +++ b/pkg/ps/types/marshaller_windows_test.go @@ -22,8 +22,8 @@ import ( htypes "github.com/rabbitstack/fibratus/pkg/handle/types" "golang.org/x/sys/windows" - "github.com/rabbitstack/fibratus/pkg/kcap/section" - kcapver "github.com/rabbitstack/fibratus/pkg/kcap/version" + "github.com/rabbitstack/fibratus/pkg/cap/section" + kcapver "github.com/rabbitstack/fibratus/pkg/cap/version" "github.com/rabbitstack/fibratus/pkg/pe" "github.com/stretchr/testify/assert" diff --git a/pkg/ps/types/types_windows.go b/pkg/ps/types/types_windows.go index 02a0ff29e..e837646fc 100644 --- a/pkg/ps/types/types_windows.go +++ b/pkg/ps/types/types_windows.go @@ -29,8 +29,8 @@ import ( "strings" "sync" + "github.com/rabbitstack/fibratus/pkg/cap/section" htypes "github.com/rabbitstack/fibratus/pkg/handle/types" - "github.com/rabbitstack/fibratus/pkg/kcap/section" "github.com/rabbitstack/fibratus/pkg/pe" "github.com/rabbitstack/fibratus/pkg/util/bootid" diff --git a/pkg/rules/_fixtures/default/microsoft_edge.yml b/pkg/rules/_fixtures/default/microsoft_edge.yml index 796424149..d349eb7cf 100644 --- a/pkg/rules/_fixtures/default/microsoft_edge.yml +++ b/pkg/rules/_fixtures/default/microsoft_edge.yml @@ -1,5 +1,5 @@ name: Microsoft edge id: c3a50242-7161-43d5-a198-051021056195 version: 1.0.0 -condition: kevt.name = 'CreateProcess' and ps.comm startswith '\"C:\\Program Files (x86)\\Microsoft\\Edge Dev\\Application\\msedge.exe\" --type=' +condition: evt.name = 'CreateProcess' and ps.comm startswith '\"C:\\Program Files (x86)\\Microsoft\\Edge Dev\\Application\\msedge.exe\" --type=' min-engine-version: 2.0.0 diff --git a/pkg/rules/_fixtures/default/sequence_rule_simple.yml b/pkg/rules/_fixtures/default/sequence_rule_simple.yml index f3db5a11d..b157f594f 100644 --- a/pkg/rules/_fixtures/default/sequence_rule_simple.yml +++ b/pkg/rules/_fixtures/default/sequence_rule_simple.yml @@ -4,8 +4,8 @@ version: 1.0.0 condition: > sequence maxspan 100ms - |kevt.name = 'CreateProcess' and ps.name = 'cmd.exe'| by ps.exe - |kevt.name = 'CreateFile' + |evt.name = 'CreateProcess' and ps.name = 'cmd.exe'| by ps.exe + |evt.name = 'CreateFile' and file.path icontains 'temp' | by file.path diff --git a/pkg/rules/_fixtures/default/suspicious_domains.yml b/pkg/rules/_fixtures/default/suspicious_domains.yml index fbe32d9e5..31216a994 100644 --- a/pkg/rules/_fixtures/default/suspicious_domains.yml +++ b/pkg/rules/_fixtures/default/suspicious_domains.yml @@ -1,5 +1,5 @@ name: Suspicious domains id: c4cd547f-d64a-4452-abe8-846410617c06 version: 1.0.0 -condition: kevt.name = 'QueryDns' and dns.name = 'fishy.domain.dot' +condition: evt.name = 'QueryDns' and dns.name = 'fishy.domain.dot' min-engine-version: 2.0.0 diff --git a/pkg/rules/_fixtures/default/suspicious_module_loaded.yml b/pkg/rules/_fixtures/default/suspicious_module_loaded.yml index 483294c47..28889e204 100644 --- a/pkg/rules/_fixtures/default/suspicious_module_loaded.yml +++ b/pkg/rules/_fixtures/default/suspicious_module_loaded.yml @@ -1,5 +1,5 @@ name: Loaded suspicious module id: 1e4c3fbf-ed3a-4df4-9c54-8693d7397a75 version: 1.0.0 -condition: kevt.name = 'LoadImage' and image.name = 'svchost.dll' +condition: evt.name = 'LoadImage' and image.name = 'svchost.dll' min-engine-version: 2.0.0 diff --git a/pkg/rules/_fixtures/default/suspicious_network_connecting_binaries.yml b/pkg/rules/_fixtures/default/suspicious_network_connecting_binaries.yml index cddaf9d78..e209515a9 100644 --- a/pkg/rules/_fixtures/default/suspicious_network_connecting_binaries.yml +++ b/pkg/rules/_fixtures/default/suspicious_network_connecting_binaries.yml @@ -1,7 +1,7 @@ name: Suspicious sources for network-connecting binaries id: 0a8c6a06-eaf2-48c1-9b05-d0e706142311 version: 1.0.0 -condition: kevt.name = 'Connect' and ps.exe startswith +condition: evt.name = 'Connect' and ps.exe startswith ( 'C:\\Users', 'C:\\Recycle', diff --git a/pkg/rules/_fixtures/default/windows_error_reporting_and_wmi_provider_host.yml b/pkg/rules/_fixtures/default/windows_error_reporting_and_wmi_provider_host.yml index c6f5a1aca..f6455eb7f 100644 --- a/pkg/rules/_fixtures/default/windows_error_reporting_and_wmi_provider_host.yml +++ b/pkg/rules/_fixtures/default/windows_error_reporting_and_wmi_provider_host.yml @@ -1,7 +1,7 @@ name: Windows error reporting/telemetry, WMI provider host id: 0fe67e44-94e2-44cb-bc40-052bc2e0fdb2 version: 1.0.0 -condition: kevt.name = 'CreateProcess' and ps.comm startswith +condition: evt.name = 'CreateProcess' and ps.comm startswith ( ' \"C:\\Windows\\system32\\wermgr.exe\\" \"-queuereporting_svc\" ', 'C:\\Windows\\system32\\DllHost.exe /Processid', diff --git a/pkg/rules/_fixtures/kill_action.yml b/pkg/rules/_fixtures/kill_action.yml index 1c43a9f6c..a00879d0e 100644 --- a/pkg/rules/_fixtures/kill_action.yml +++ b/pkg/rules/_fixtures/kill_action.yml @@ -1,7 +1,7 @@ name: Kill calc.exe process id: 172902be-76e9-4ee7-a48a-6275fa571cf4 version: 1.0.0 -condition: kevt.name = 'CreateProcess' and ps.child.name = 'calc.exe' +condition: evt.name = 'CreateProcess' and ps.child.name = 'calc.exe' severity: critical action: - name: kill diff --git a/pkg/rules/_fixtures/merged_filters/filter1.yml b/pkg/rules/_fixtures/merged_filters/filter1.yml index 14aec5214..113d9daf0 100644 --- a/pkg/rules/_fixtures/merged_filters/filter1.yml +++ b/pkg/rules/_fixtures/merged_filters/filter1.yml @@ -1,6 +1,6 @@ name: match https connections id: 8f36f8e0-a5c2-498f-9563-eea306daa586 version: 1.0.0 -condition: kevt.name = 'Recv' and net.dport = 443 +condition: evt.name = 'Recv' and net.dport = 443 min-engine-version: 2.0.0 diff --git a/pkg/rules/_fixtures/merged_filters/filter2.yml b/pkg/rules/_fixtures/merged_filters/filter2.yml index 0b3ebba28..3c404521d 100644 --- a/pkg/rules/_fixtures/merged_filters/filter2.yml +++ b/pkg/rules/_fixtures/merged_filters/filter2.yml @@ -1,5 +1,5 @@ name: match http connections id: 7f36f8e0-a5c2-498f-9563-eea306daa586 version: 1.0.0 -condition: kevt.name = 'Recv' and net.dport = 80 +condition: evt.name = 'Recv' and net.dport = 80 min-engine-version: 2.0.0 diff --git a/pkg/rules/_fixtures/merged_filters/filter3.yml b/pkg/rules/_fixtures/merged_filters/filter3.yml index ed558f844..66ac3f5e1 100644 --- a/pkg/rules/_fixtures/merged_filters/filter3.yml +++ b/pkg/rules/_fixtures/merged_filters/filter3.yml @@ -1,5 +1,5 @@ name: match http connections id: 6f36f8e0-a5c2-498f-9563-eea306daa586 version: 1.0.0 -condition: kevt.category = 'net' and net.dport = 80 +condition: evt.category = 'net' and net.dport = 80 min-engine-version: 2.0.0 diff --git a/pkg/rules/_fixtures/merged_filters/filter4.yml b/pkg/rules/_fixtures/merged_filters/filter4.yml index 943275933..faf8f70e4 100644 --- a/pkg/rules/_fixtures/merged_filters/filter4.yml +++ b/pkg/rules/_fixtures/merged_filters/filter4.yml @@ -1,5 +1,5 @@ name: match ssh connections id: 5f36f8e0-a5c2-498f-9563-eea306daa586 version: 1.0.0 -condition: kevt.name = 'Recv' and net.dport = 22 +condition: evt.name = 'Recv' and net.dport = 22 min-engine-version: 2.0.0 diff --git a/pkg/rules/_fixtures/min_engine_version/fail/filter1.yml b/pkg/rules/_fixtures/min_engine_version/fail/filter1.yml index d8b78abb8..28a3af442 100644 --- a/pkg/rules/_fixtures/min_engine_version/fail/filter1.yml +++ b/pkg/rules/_fixtures/min_engine_version/fail/filter1.yml @@ -1,5 +1,5 @@ name: match https connections id: 1f36f8e0-a5c2-498f-9563-eea306daa586 version: 1.0.0 -condition: kevt.name = 'Recv' and net.dport = 443 +condition: evt.name = 'Recv' and net.dport = 443 min-engine-version: 1.0.0 diff --git a/pkg/rules/_fixtures/min_engine_version/fail/filter2.yml b/pkg/rules/_fixtures/min_engine_version/fail/filter2.yml index 565424e0f..525469324 100644 --- a/pkg/rules/_fixtures/min_engine_version/fail/filter2.yml +++ b/pkg/rules/_fixtures/min_engine_version/fail/filter2.yml @@ -1,5 +1,5 @@ name: accept events where source port = 44123 id: 2f36f8e0-a5c2-498f-9563-eea306daa586 version: 1.0.0 -condition: kevt.name = 'Recv' and net.sport = 44123 +condition: evt.name = 'Recv' and net.sport = 44123 min-engine-version: 2.2.0 diff --git a/pkg/rules/_fixtures/min_engine_version/fail/filter3.yml b/pkg/rules/_fixtures/min_engine_version/fail/filter3.yml index ace3c32f5..66f649fac 100644 --- a/pkg/rules/_fixtures/min_engine_version/fail/filter3.yml +++ b/pkg/rules/_fixtures/min_engine_version/fail/filter3.yml @@ -1,5 +1,5 @@ name: src ip address is not a loopback address id: 3f36f8e0-a5c2-498f-9563-eea306daa586 version: 1.0.0 -condition: kevt.name = 'Recv' and net.sip != 127.0.0.1 +condition: evt.name = 'Recv' and net.sip != 127.0.0.1 min-engine-version: 1.5.0 diff --git a/pkg/rules/_fixtures/min_engine_version/ok/filter1.yml b/pkg/rules/_fixtures/min_engine_version/ok/filter1.yml index 88a006c7d..9797acb26 100644 --- a/pkg/rules/_fixtures/min_engine_version/ok/filter1.yml +++ b/pkg/rules/_fixtures/min_engine_version/ok/filter1.yml @@ -1,5 +1,5 @@ name: match https connections id: a155539d-31bd-429e-81f9-c17ee1c01f93 version: 1.0.0 -condition: kevt.name = 'Recv' and net.dport = 443 +condition: evt.name = 'Recv' and net.dport = 443 min-engine-version: 2.0.0 diff --git a/pkg/rules/_fixtures/min_engine_version/ok/filter2.yml b/pkg/rules/_fixtures/min_engine_version/ok/filter2.yml index 2b6790f6e..3f8ca4f2e 100644 --- a/pkg/rules/_fixtures/min_engine_version/ok/filter2.yml +++ b/pkg/rules/_fixtures/min_engine_version/ok/filter2.yml @@ -1,5 +1,5 @@ name: accept events where source port = 44123 id: 1155539d-31bd-429e-81f9-c17ee1c01f93 version: 1.0.0 -condition: kevt.name = 'Recv' and net.sport = 44123 +condition: evt.name = 'Recv' and net.sport = 44123 min-engine-version: 1.8.0 diff --git a/pkg/rules/_fixtures/min_engine_version/ok/filter3.yml b/pkg/rules/_fixtures/min_engine_version/ok/filter3.yml index 560c58902..377483de0 100644 --- a/pkg/rules/_fixtures/min_engine_version/ok/filter3.yml +++ b/pkg/rules/_fixtures/min_engine_version/ok/filter3.yml @@ -1,5 +1,5 @@ name: src ip address is not a loopback address id: 2155539d-31bd-429e-81f9-c17ee1c01f93 version: 1.0.0 -condition: kevt.name = 'Recv' and net.sip != 127.0.0.1 +condition: evt.name = 'Recv' and net.sip != 127.0.0.1 min-engine-version: 1.5.0 diff --git a/pkg/rules/_fixtures/sequence_rule_complex.yml b/pkg/rules/_fixtures/sequence_rule_complex.yml index 0939e0d3e..1a3dad8c2 100644 --- a/pkg/rules/_fixtures/sequence_rule_complex.yml +++ b/pkg/rules/_fixtures/sequence_rule_complex.yml @@ -4,17 +4,17 @@ version: 1.0.0 condition: > sequence maxspan 1h - |kevt.name = 'CreateProcess' and ps.sibling.name + |evt.name = 'CreateProcess' and ps.sibling.name in ('firefox.exe', 'chrome.exe', 'edge.exe') | by ps.sibling.pid - |kevt.name = 'CreateFile' and file.operation = 'CREATE' + |evt.name = 'CreateFile' and file.operation = 'CREATE' and file.extension = '.exe' | by ps.pid | - kevt.name in ('Send', 'Connect') + evt.name in ('Send', 'Connect') | by ps.pid output: "%2.ps.name process initiated outbound communication to %3.net.dip" min-engine-version: 2.0.0 diff --git a/pkg/rules/_fixtures/sequence_rule_ps_uuid.yml b/pkg/rules/_fixtures/sequence_rule_ps_uuid.yml index 6b5c8b823..f1fb655d0 100644 --- a/pkg/rules/_fixtures/sequence_rule_ps_uuid.yml +++ b/pkg/rules/_fixtures/sequence_rule_ps_uuid.yml @@ -5,11 +5,11 @@ condition: > sequence maxspan 1h by ps.uuid - |kevt.name = 'CreateProcess' and ps.child.name + |evt.name = 'CreateProcess' and ps.child.name in ('firefox.exe', 'chrome.exe', 'edge.exe') | - |kevt.name = 'CreateFile' and file.operation = 'CREATE' + |evt.name = 'CreateFile' and file.operation = 'CREATE' and file.extension = '.exe' | diff --git a/pkg/rules/_fixtures/simple_and_sequence_rules/command_shell_spawned_chrome_browser.yml b/pkg/rules/_fixtures/simple_and_sequence_rules/command_shell_spawned_chrome_browser.yml index e32402167..45a501e88 100644 --- a/pkg/rules/_fixtures/simple_and_sequence_rules/command_shell_spawned_chrome_browser.yml +++ b/pkg/rules/_fixtures/simple_and_sequence_rules/command_shell_spawned_chrome_browser.yml @@ -3,6 +3,6 @@ id: 2155539d-31bd-429e-81f9-c17ee1c01f93 version: 1.0.0 condition: > sequence maxspan 1s - |kevt.name = 'CreateProcess' and ps.name = 'powershell.exe'| by ps.pid - |kevt.name = 'CreateProcess' and ps.sibling.name = 'chrome.exe'| by ps.pid + |evt.name = 'CreateProcess' and ps.name = 'powershell.exe'| by ps.pid + |evt.name = 'CreateProcess' and ps.sibling.name = 'chrome.exe'| by ps.pid min-engine-version: 2.0.0 diff --git a/pkg/rules/_fixtures/simple_and_sequence_rules/powershell_created_temp_file.yml b/pkg/rules/_fixtures/simple_and_sequence_rules/powershell_created_temp_file.yml index 36a906513..375e8105b 100644 --- a/pkg/rules/_fixtures/simple_and_sequence_rules/powershell_created_temp_file.yml +++ b/pkg/rules/_fixtures/simple_and_sequence_rules/powershell_created_temp_file.yml @@ -4,8 +4,8 @@ version: 1.0.0 condition: > sequence maxspan 100ms - |kevt.name = 'CreateProcess' and ps.name = 'powershell.exe'| by ps.pid - |kevt.name = 'CreateFile' + |evt.name = 'CreateProcess' and ps.name = 'powershell.exe'| by ps.pid + |evt.name = 'CreateFile' and file.path icontains 'temp' | by ps.pid diff --git a/pkg/rules/_fixtures/simple_and_sequence_rules/process_spawned_by_powershell.yml b/pkg/rules/_fixtures/simple_and_sequence_rules/process_spawned_by_powershell.yml index e3f9a64da..2ded698bd 100644 --- a/pkg/rules/_fixtures/simple_and_sequence_rules/process_spawned_by_powershell.yml +++ b/pkg/rules/_fixtures/simple_and_sequence_rules/process_spawned_by_powershell.yml @@ -2,5 +2,5 @@ name: Process spawned by powershell id: 4155539d-31bd-429e-81f9-c17ee1c01f93 version: 1.0.0 condition: > - kevt.name = 'CreateProcess' and ps.name = 'powershell.exe' + evt.name = 'CreateProcess' and ps.name = 'powershell.exe' min-engine-version: 2.0.0 diff --git a/pkg/rules/_fixtures/simple_and_sequence_rules/spawn_chrome_browser.yml b/pkg/rules/_fixtures/simple_and_sequence_rules/spawn_chrome_browser.yml index fcf76d31d..02a91e9ee 100644 --- a/pkg/rules/_fixtures/simple_and_sequence_rules/spawn_chrome_browser.yml +++ b/pkg/rules/_fixtures/simple_and_sequence_rules/spawn_chrome_browser.yml @@ -2,5 +2,5 @@ name: Spawn Chrome browser id: 5155539d-31bd-429e-81f9-c17ee1c01f93 version: 1.0.0 condition: > - kevt.name = 'CreateProcess' and ps.sibling.name = 'chrome.exe' + evt.name = 'CreateProcess' and ps.sibling.name = 'chrome.exe' min-engine-version: 2.0.0 diff --git a/pkg/rules/_fixtures/simple_emit_alert.yml b/pkg/rules/_fixtures/simple_emit_alert.yml index 4adf619e2..d5c9703fe 100644 --- a/pkg/rules/_fixtures/simple_emit_alert.yml +++ b/pkg/rules/_fixtures/simple_emit_alert.yml @@ -1,7 +1,7 @@ name: match https connections id: 50ffc2a8-0bde-45c4-9e20-46158250fa91 version: 1.0.0 -condition: kevt.name = 'Recv' and net.dport = 443 +condition: evt.name = 'Recv' and net.dport = 443 output: "%ps.name process received data on port %net.dport" severity: critical min-engine-version: 2.0.0 diff --git a/pkg/rules/_fixtures/simple_matches.yml b/pkg/rules/_fixtures/simple_matches.yml index 0e40c4985..6892acb3d 100644 --- a/pkg/rules/_fixtures/simple_matches.yml +++ b/pkg/rules/_fixtures/simple_matches.yml @@ -1,5 +1,5 @@ name: match https connections id: 60ffc2a8-0bde-45c4-9e20-46158250fa91 version: 1.0.0 -condition: kevt.name = 'Recv' and net.dport = 443 +condition: evt.name = 'Recv' and net.dport = 443 min-engine-version: 2.0.0 diff --git a/pkg/rules/_fixtures/simple_matches/filter1.yml b/pkg/rules/_fixtures/simple_matches/filter1.yml index 5d195efa9..a381967f3 100644 --- a/pkg/rules/_fixtures/simple_matches/filter1.yml +++ b/pkg/rules/_fixtures/simple_matches/filter1.yml @@ -1,5 +1,5 @@ name: match https connections id: 5155539d-31bd-429e-81f9-c17ee1c01f93 version: 1.0.0 -condition: kevt.name = 'Recv' and net.dport = 443 +condition: evt.name = 'Recv' and net.dport = 443 min-engine-version: 2.0.0 diff --git a/pkg/rules/_fixtures/simple_matches/filter2.yml b/pkg/rules/_fixtures/simple_matches/filter2.yml index 67e23d7ee..aff9bda65 100644 --- a/pkg/rules/_fixtures/simple_matches/filter2.yml +++ b/pkg/rules/_fixtures/simple_matches/filter2.yml @@ -1,5 +1,5 @@ name: accept events where source port = 44123 id: 6155539d-31bd-429e-81f9-c17ee1c01f93 version: 1.0.0 -condition: kevt.name = 'Recv' and net.sport = 44123 +condition: evt.name = 'Recv' and net.sport = 44123 min-engine-version: 2.0.0 diff --git a/pkg/rules/_fixtures/simple_matches/filter3.yml b/pkg/rules/_fixtures/simple_matches/filter3.yml index 08602edef..b98789fd6 100644 --- a/pkg/rules/_fixtures/simple_matches/filter3.yml +++ b/pkg/rules/_fixtures/simple_matches/filter3.yml @@ -1,5 +1,5 @@ name: src ip address is not a loopback address id: 7155539d-31bd-429e-81f9-c17ee1c01f93 version: 1.0.0 -condition: kevt.name = 'Recv' and net.sip != 127.0.0.1 +condition: evt.name = 'Recv' and net.sip != 127.0.0.1 min-engine-version: 2.0.0 diff --git a/pkg/rules/_fixtures/simple_matches/filter4.yml b/pkg/rules/_fixtures/simple_matches/filter4.yml index 18466d252..3db0742f2 100644 --- a/pkg/rules/_fixtures/simple_matches/filter4.yml +++ b/pkg/rules/_fixtures/simple_matches/filter4.yml @@ -1,5 +1,5 @@ name: src ip address is id: 8155539d-31bd-429e-81f9-c17ee1c01f93 version: 1.0.0 -condition: kevt.name = 'Recv' and net.sip = 172.0.0.1 +condition: evt.name = 'Recv' and net.sip = 172.0.0.1 min-engine-version: 2.0.0 diff --git a/pkg/rules/compiler.go b/pkg/rules/compiler.go index 35b8919f6..7016341e5 100644 --- a/pkg/rules/compiler.go +++ b/pkg/rules/compiler.go @@ -23,9 +23,9 @@ import ( "fmt" semver "github.com/hashicorp/go-version" "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/filter" "github.com/rabbitstack/fibratus/pkg/filter/fields" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/rabbitstack/fibratus/pkg/ps" "github.com/rabbitstack/fibratus/pkg/util/version" log "github.com/sirupsen/logrus" @@ -114,44 +114,44 @@ func (c *compiler) compile() (map[*config.FilterConfig]filter.Filter, *config.Ru func (c *compiler) buildCompileResult(filters map[*config.FilterConfig]filter.Filter) *config.RulesCompileResult { rs := &config.RulesCompileResult{} - m := make(map[ktypes.Ktype]bool) - events := make([]ktypes.Ktype, 0) + m := make(map[event.Type]bool) + events := make([]event.Type, 0) for _, f := range filters { rs.NumberRules++ for name, values := range f.GetStringFields() { for _, v := range values { if name == fields.KevtName || name == fields.KevtCategory { - types := ktypes.KeventNameToKtypes(v) + types := event.NameToTypes(v) for _, typ := range types { switch typ.Category() { - case ktypes.Process: + case event.Process: rs.HasProcEvents = true - case ktypes.Thread: + case event.Thread: rs.HasThreadEvents = true - case ktypes.Image: + case event.Image: rs.HasImageEvents = true - case ktypes.File: + case event.File: rs.HasFileEvents = true - case ktypes.Net: + case event.Net: rs.HasNetworkEvents = true - case ktypes.Registry: + case event.Registry: rs.HasRegistryEvents = true - case ktypes.Mem: + case event.Mem: rs.HasMemEvents = true - case ktypes.Handle: + case event.Handle: rs.HasHandleEvents = true - case ktypes.Threadpool: + case event.Threadpool: rs.HasThreadpoolEvents = true } - if typ.Subcategory() == ktypes.DNS { + if typ.Subcategory() == event.DNS { rs.HasDNSEvents = true } - if typ == ktypes.MapViewFile || typ == ktypes.UnmapViewFile { + if typ == event.MapViewFile || typ == event.UnmapViewFile { rs.HasVAMapEvents = true } - if typ == ktypes.OpenProcess || typ == ktypes.OpenThread || typ == ktypes.SetThreadContext || - typ == ktypes.CreateSymbolicLinkObject { + if typ == event.OpenProcess || typ == event.OpenThread || typ == event.SetThreadContext || + typ == event.CreateSymbolicLinkObject { rs.HasAuditAPIEvents = true } diff --git a/pkg/rules/compiler_test.go b/pkg/rules/compiler_test.go index f093713fd..2bc6ac531 100644 --- a/pkg/rules/compiler_test.go +++ b/pkg/rules/compiler_test.go @@ -19,7 +19,7 @@ package rules import ( - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/ps" "github.com/rabbitstack/fibratus/pkg/util/version" "github.com/stretchr/testify/assert" @@ -39,11 +39,11 @@ func TestCompile(t *testing.T) { assert.False(t, rs.HasMemEvents) assert.False(t, rs.HasAuditAPIEvents) assert.True(t, rs.HasDNSEvents) - assert.Contains(t, rs.UsedEvents, ktypes.CreateProcess) - assert.Contains(t, rs.UsedEvents, ktypes.LoadImage) - assert.Contains(t, rs.UsedEvents, ktypes.QueryDNS) - assert.Contains(t, rs.UsedEvents, ktypes.ConnectTCPv4) - assert.Contains(t, rs.UsedEvents, ktypes.ConnectTCPv6) + assert.Contains(t, rs.UsedEvents, event.CreateProcess) + assert.Contains(t, rs.UsedEvents, event.LoadImage) + assert.Contains(t, rs.UsedEvents, event.QueryDNS) + assert.Contains(t, rs.UsedEvents, event.ConnectTCPv4) + assert.Contains(t, rs.UsedEvents, event.ConnectTCPv6) } func TestCompileMinEngineVersion(t *testing.T) { diff --git a/pkg/rules/engine.go b/pkg/rules/engine.go index b73fcff2a..2cd74569e 100644 --- a/pkg/rules/engine.go +++ b/pkg/rules/engine.go @@ -22,10 +22,9 @@ import ( "expvar" "fmt" "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/filter" "github.com/rabbitstack/fibratus/pkg/filter/fields" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/rabbitstack/fibratus/pkg/ps" "github.com/rabbitstack/fibratus/pkg/rules/action" "github.com/rabbitstack/fibratus/pkg/util/hashers" @@ -37,7 +36,7 @@ import ( // RuleMatchFunc is rule match function definition. It accepts // the filter (rule) config and the group of events that fired // the rule -type RuleMatchFunc func(f *config.FilterConfig, evts ...*kevent.Kevent) +type RuleMatchFunc func(f *config.FilterConfig, evts ...*event.Event) var ( // sequenceGcInterval determines how often sequence GC kicks in @@ -78,28 +77,28 @@ type ruleMatch struct { // hashCache caches the event type/category FNV hashes type hashCache struct { mu sync.RWMutex - types map[ktypes.Ktype]uint32 - cats map[ktypes.Category]uint32 + types map[event.Type]uint32 + cats map[event.Category]uint32 lookupCategory bool } func newHashCache() *hashCache { - return &hashCache{types: make(map[ktypes.Ktype]uint32), cats: make(map[ktypes.Category]uint32)} + return &hashCache{types: make(map[event.Type]uint32), cats: make(map[event.Category]uint32)} } -func (c *hashCache) typeHash(e *kevent.Kevent) uint32 { +func (c *hashCache) typeHash(e *event.Event) uint32 { c.mu.RLock() defer c.mu.RUnlock() return c.types[e.Type] } -func (c *hashCache) categoryHash(e *kevent.Kevent) uint32 { +func (c *hashCache) categoryHash(e *event.Event) uint32 { c.mu.RLock() defer c.mu.RUnlock() return c.cats[e.Category] } -func (c *hashCache) addTypeHash(e *kevent.Kevent) uint32 { +func (c *hashCache) addTypeHash(e *event.Event) uint32 { c.mu.Lock() defer c.mu.Unlock() h := e.Type.Hash() @@ -107,7 +106,7 @@ func (c *hashCache) addTypeHash(e *kevent.Kevent) uint32 { return h } -func (c *hashCache) addCategoryHash(e *kevent.Kevent) uint32 { +func (c *hashCache) addCategoryHash(e *event.Event) uint32 { c.mu.Lock() defer c.mu.Unlock() h := e.Category.Hash() @@ -127,7 +126,7 @@ type compiledFilters map[uint32][]*compiledFilter // particular event type or category. If no filters // are found, the event is not asserted against the // ruleset. -func (filters compiledFilters) collect(hashCache *hashCache, e *kevent.Kevent) []*compiledFilter { +func (filters compiledFilters) collect(hashCache *hashCache, e *event.Event) []*compiledFilter { h := hashCache.typeHash(e) if h == 0 { h = hashCache.addTypeHash(e) @@ -163,7 +162,7 @@ func (f *compiledFilter) isSequence() bool { return f.ss != nil } -func (f *compiledFilter) run(e *kevent.Kevent) bool { +func (f *compiledFilter) run(e *event.Event) bool { if f.ss != nil { return f.ss.runSequence(e) } @@ -223,8 +222,8 @@ func (e *Engine) Compile() (*config.RulesCompileResult, error) { "event type or event category condition! "+ "This rule is being discarded by "+ "the engine. Please consider narrowing the "+ - "scope of the rule by including the `kevt.name` "+ - "or `kevt.category` condition", + "scope of the rule by including the `evt.name` "+ + "or `evt.category` condition", c.Name) continue } @@ -258,7 +257,7 @@ func (*Engine) CanEnqueue() bool { return true } // Filter is the internal lingo that designates a rule condition. // Filters can be simple direct-event matchers or sequence states that // track an ordered series of events over a short period of time. -func (e *Engine) ProcessEvent(evt *kevent.Kevent) (bool, error) { +func (e *Engine) ProcessEvent(evt *event.Event) (bool, error) { if len(e.filters) == 0 { return true, nil } @@ -342,11 +341,11 @@ func (e *Engine) processActions() error { return nil } -func (e *Engine) appendMatch(f *config.FilterConfig, evts ...*kevent.Kevent) { +func (e *Engine) appendMatch(f *config.FilterConfig, evts ...*event.Event) { for _, evt := range evts { - evt.AddMeta(kevent.RuleNameKey, f.Name) + evt.AddMeta(event.RuleNameKey, f.Name) for k, v := range f.Labels { - evt.AddMeta(kevent.MetadataKey(k), v) + evt.AddMeta(event.MetadataKey(k), v) } } ctx := &config.ActionContext{ diff --git a/pkg/rules/engine_test.go b/pkg/rules/engine_test.go index 37782053a..7c70f3a4e 100644 --- a/pkg/rules/engine_test.go +++ b/pkg/rules/engine_test.go @@ -21,10 +21,9 @@ package rules import ( "github.com/rabbitstack/fibratus/pkg/alertsender" "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/fs" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/rabbitstack/fibratus/pkg/ps" "github.com/rabbitstack/fibratus/pkg/ps/types" "github.com/rabbitstack/fibratus/pkg/sys" @@ -107,7 +106,7 @@ func compileRules(t *testing.T, e *Engine) { require.NotNil(t, rs) } -func wrapProcessEvent(e *kevent.Kevent, fn func(*kevent.Kevent) (bool, error)) bool { +func wrapProcessEvent(e *event.Event, fn func(*event.Event) (bool, error)) bool { match, err := fn(e) if err != nil { panic(err) @@ -117,19 +116,19 @@ func wrapProcessEvent(e *kevent.Kevent, fn func(*kevent.Kevent) (bool, error)) b func fireRules(t *testing.T, c *config.Config) bool { e := NewEngine(new(ps.SnapshotterMock), c) - evt := &kevent.Kevent{ - Type: ktypes.RecvTCPv4, + evt := &event.Event{ + Type: event.RecvTCPv4, Name: "Recv", Tid: 2484, PID: 859, - Category: ktypes.Net, - Kparams: kevent.Kparams{ - kparams.NetDport: {Name: kparams.NetDport, Type: kparams.Uint16, Value: uint16(443)}, - kparams.NetSport: {Name: kparams.NetSport, Type: kparams.Uint16, Value: uint16(43123)}, - kparams.NetSIP: {Name: kparams.NetSIP, Type: kparams.IPv4, Value: net.ParseIP("127.0.0.1")}, - kparams.NetDIP: {Name: kparams.NetDIP, Type: kparams.IPv4, Value: net.ParseIP("216.58.201.174")}, + Category: event.Net, + Params: event.Params{ + params.NetDport: {Name: params.NetDport, Type: params.Uint16, Value: uint16(443)}, + params.NetSport: {Name: params.NetSport, Type: params.Uint16, Value: uint16(43123)}, + params.NetSIP: {Name: params.NetSIP, Type: params.IPv4, Value: net.ParseIP("127.0.0.1")}, + params.NetDIP: {Name: params.NetDIP, Type: params.IPv4, Value: net.ParseIP("216.58.201.174")}, }, - Metadata: make(map[kevent.MetadataKey]any), + Metadata: make(map[event.MetadataKey]any), } compileRules(t, e) return wrapProcessEvent(evt, e.ProcessEvent) @@ -146,14 +145,14 @@ func TestCompileIndexableFilters(t *testing.T) { assert.Len(t, e.filters, 3) var tests = []struct { - evt *kevent.Kevent + evt *event.Event wants int }{ - {&kevent.Kevent{Type: ktypes.CreateProcess}, 2}, - {&kevent.Kevent{Type: ktypes.RecvUDPv6}, 3}, - {&kevent.Kevent{Type: ktypes.RecvTCPv4}, 3}, - {&kevent.Kevent{Type: ktypes.RecvTCPv4, Category: ktypes.Net}, 4}, - {&kevent.Kevent{Category: ktypes.Net}, 1}, + {&event.Event{Type: event.CreateProcess}, 2}, + {&event.Event{Type: event.RecvUDPv6}, 3}, + {&event.Event{Type: event.RecvTCPv4}, 3}, + {&event.Event{Type: event.RecvTCPv4, Category: event.Net}, 4}, + {&event.Event{Category: event.Net}, 1}, } for _, tt := range tests { @@ -164,7 +163,7 @@ func TestCompileIndexableFilters(t *testing.T) { assert.Len(t, e.hashCache.types, 4) - evt := &kevent.Kevent{Type: ktypes.RecvTCPv4} + evt := &event.Event{Type: event.RecvTCPv4} h1, h2 := e.hashCache.typeHash(evt), e.hashCache.categoryHash(evt) assert.Equal(t, uint32(0xfa4dab59), h1) @@ -194,11 +193,11 @@ func TestRunSequenceRule(t *testing.T) { e := NewEngine(new(ps.SnapshotterMock), newConfig("_fixtures/sequence_rule_complex.yml")) compileRules(t, e) - e1 := &kevent.Kevent{ + e1 := &event.Event{ Seq: 1, - Type: ktypes.CreateProcess, + Type: event.CreateProcess, Timestamp: time.Now(), - Category: ktypes.Process, + Category: event.Process, Name: "CreateProcess", Tid: 2484, PID: 859, @@ -206,38 +205,38 @@ func TestRunSequenceRule(t *testing.T) { Name: "explorer.exe", Exe: "C:\\Windows\\system32\\explorer.exe", }, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(2243)}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.UnicodeString, Value: "firefox.exe"}, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(2243)}, + params.ProcessName: {Name: params.ProcessName, Type: params.UnicodeString, Value: "firefox.exe"}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, } - e2 := &kevent.Kevent{ + e2 := &event.Event{ Seq: 2, - Type: ktypes.CreateFile, + Type: event.CreateFile, Timestamp: time.Now().Add(time.Millisecond * 250), Name: "CreateFile", Tid: 2484, PID: 2243, - Category: ktypes.File, + Category: event.File, PS: &types.PS{ Name: "firefox.exe", Exe: "C:\\Program Files\\Mozilla Firefox\\firefox.exe", Cmdline: "C:\\Program Files\\Mozilla Firefox\\firefox.exe\" -contentproc --channel=\"10464.7.539748228\\1366525930\" -childID 6 -isF", }, - Kparams: kevent.Kparams{ - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Temp\\dropper.exe"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.Enum, Value: uint32(2), Enum: fs.FileCreateDispositions}, + Params: event.Params{ + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Temp\\dropper.exe"}, + params.FileOperation: {Name: params.FileOperation, Type: params.Enum, Value: uint32(2), Enum: fs.FileCreateDispositions}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, } - e3 := &kevent.Kevent{ + e3 := &event.Event{ Seq: 4, - Type: ktypes.ConnectTCPv4, + Type: event.ConnectTCPv4, Timestamp: time.Now().Add(time.Second), - Category: ktypes.Net, + Category: event.Net, Name: "Connect", Tid: 244, PID: 2243, @@ -246,10 +245,10 @@ func TestRunSequenceRule(t *testing.T) { Exe: "C:\\Program Files\\Mozilla Firefox\\firefox.exe", Cmdline: "C:\\Program Files\\Mozilla Firefox\\firefox.exe\" -contentproc --channel=\"10464.7.539748228\\1366525930\" -childID 6 -isF", }, - Kparams: kevent.Kparams{ - kparams.NetDIP: {Name: kparams.NetDIP, Type: kparams.IPv4, Value: net.ParseIP("10.0.2.3")}, + Params: event.Params{ + params.NetDIP: {Name: params.NetDIP, Type: params.IPv4, Value: net.ParseIP("10.0.2.3")}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, } // register alert sender @@ -284,11 +283,11 @@ func TestRunSequenceRuleWithPsUUIDLink(t *testing.T) { e := NewEngine(new(ps.SnapshotterMock), newConfig("_fixtures/sequence_rule_ps_uuid.yml")) compileRules(t, e) - e1 := &kevent.Kevent{ + e1 := &event.Event{ Seq: 1, - Type: ktypes.CreateProcess, + Type: event.CreateProcess, Timestamp: time.Now(), - Category: ktypes.Process, + Category: event.Process, Name: "CreateProcess", Tid: 2484, PID: uint32(os.Getpid()), @@ -297,32 +296,32 @@ func TestRunSequenceRuleWithPsUUIDLink(t *testing.T) { Name: "explorer.exe", Exe: "C:\\Windows\\system32\\explorer.exe", }, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(2243)}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.UnicodeString, Value: "firefox.exe"}, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(2243)}, + params.ProcessName: {Name: params.ProcessName, Type: params.UnicodeString, Value: "firefox.exe"}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, } - e2 := &kevent.Kevent{ + e2 := &event.Event{ Seq: 2, - Type: ktypes.CreateFile, + Type: event.CreateFile, Timestamp: time.Now(), Name: "CreateFile", Tid: 2484, PID: uint32(os.Getpid()), - Category: ktypes.File, + Category: event.File, PS: &types.PS{ PID: uint32(os.Getpid()), Name: "firefox.exe", Exe: "C:\\Program Files\\Mozilla Firefox\\firefox.exe", Cmdline: "C:\\Program Files\\Mozilla Firefox\\firefox.exe\" -contentproc --channel=\"10464.7.539748228\\1366525930\" -childID 6 -isF", }, - Kparams: kevent.Kparams{ - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Temp\\dropper.exe"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.Enum, Value: uint32(2), Enum: fs.FileCreateDispositions}, + Params: event.Params{ + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Temp\\dropper.exe"}, + params.FileOperation: {Name: params.FileOperation, Type: params.Enum, Value: uint32(2), Enum: fs.FileCreateDispositions}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, } require.False(t, wrapProcessEvent(e1, e.ProcessEvent)) @@ -336,7 +335,7 @@ func TestRunSimpleAndSequenceRules(t *testing.T) { c := newConfig("_fixtures/simple_and_sequence_rules/*.yml") c.Filters.MatchAll = true e := NewEngine(new(ps.SnapshotterMock), c) - e.RegisterMatchFunc(func(f *config.FilterConfig, evts ...*kevent.Kevent) { + e.RegisterMatchFunc(func(f *config.FilterConfig, evts ...*event.Event) { ids := make([]uint64, 0) for _, evt := range evts { ids = append(ids, evt.Seq) @@ -346,12 +345,12 @@ func TestRunSimpleAndSequenceRules(t *testing.T) { compileRules(t, e) - evts := []*kevent.Kevent{ + evts := []*event.Event{ { Seq: 1, - Type: ktypes.CreateProcess, + Type: event.CreateProcess, Timestamp: time.Now(), - Category: ktypes.Process, + Category: event.Process, Name: "CreateProcess", Tid: 2484, PID: 2243, @@ -359,35 +358,35 @@ func TestRunSimpleAndSequenceRules(t *testing.T) { Name: "powershell.exe", Exe: "C:\\Windows\\system32\\powershell.exe", }, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(2243)}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.UnicodeString, Value: "firefox.exe"}, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(2243)}, + params.ProcessName: {Name: params.ProcessName, Type: params.UnicodeString, Value: "firefox.exe"}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, }, { Seq: 2, - Type: ktypes.CreateFile, + Type: event.CreateFile, Timestamp: time.Now().Add(time.Millisecond * 544), Name: "CreateFile", Tid: 2484, PID: 2243, - Category: ktypes.File, + Category: event.File, PS: &types.PS{ Name: "cmd.exe", Exe: "C:\\Windows\\system32\\cmd.exe", }, - Kparams: kevent.Kparams{ - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Temp\\dropper.exe"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.Enum, Value: uint32(2)}, + Params: event.Params{ + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Temp\\dropper.exe"}, + params.FileOperation: {Name: params.FileOperation, Type: params.Enum, Value: uint32(2)}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, }, { Seq: 10, - Type: ktypes.CreateProcess, + Type: event.CreateProcess, Timestamp: time.Now().Add(time.Second * 2), - Category: ktypes.Process, + Category: event.Process, Name: "CreateProcess", Tid: 2484, PID: 2243, @@ -395,11 +394,11 @@ func TestRunSimpleAndSequenceRules(t *testing.T) { Name: "cmd.exe", Exe: "C:\\Windows\\system32\\cmd.exe", }, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(2243)}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.UnicodeString, Value: "chrome.exe"}, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(2243)}, + params.ProcessName: {Name: params.ProcessName, Type: params.UnicodeString, Value: "chrome.exe"}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, }, } @@ -431,22 +430,22 @@ func TestAlertAction(t *testing.T) { e := NewEngine(new(ps.SnapshotterMock), newConfig("_fixtures/simple_emit_alert.yml")) compileRules(t, e) - evt := &kevent.Kevent{ - Type: ktypes.RecvTCPv4, + evt := &event.Event{ + Type: event.RecvTCPv4, Name: "Recv", Tid: 2484, PID: 859, - Category: ktypes.Net, + Category: event.Net, PS: &types.PS{ Name: "cmd.exe", }, - Kparams: kevent.Kparams{ - kparams.NetDport: {Name: kparams.NetDport, Type: kparams.Uint16, Value: uint16(443)}, - kparams.NetSport: {Name: kparams.NetSport, Type: kparams.Uint16, Value: uint16(43123)}, - kparams.NetSIP: {Name: kparams.NetSIP, Type: kparams.IPv4, Value: net.ParseIP("127.0.0.1")}, - kparams.NetDIP: {Name: kparams.NetDIP, Type: kparams.IPv4, Value: net.ParseIP("216.58.201.174")}, + Params: event.Params{ + params.NetDport: {Name: params.NetDport, Type: params.Uint16, Value: uint16(443)}, + params.NetSport: {Name: params.NetSport, Type: params.Uint16, Value: uint16(43123)}, + params.NetSIP: {Name: params.NetSIP, Type: params.IPv4, Value: net.ParseIP("127.0.0.1")}, + params.NetDIP: {Name: params.NetDIP, Type: params.IPv4, Value: net.ParseIP("216.58.201.174")}, }, - Metadata: make(map[kevent.MetadataKey]any), + Metadata: make(map[event.MetadataKey]any), } require.True(t, wrapProcessEvent(evt, e.ProcessEvent)) @@ -491,22 +490,22 @@ func TestKillAction(t *testing.T) { time.Sleep(time.Millisecond * 100 * time.Duration(i)) } - evt := &kevent.Kevent{ - Type: ktypes.CreateProcess, + evt := &event.Event{ + Type: event.CreateProcess, Timestamp: time.Now(), Name: "CreateProcess", Tid: 2484, PID: 859, - Category: ktypes.Process, + Category: event.Process, PS: &types.PS{ Name: "cmd.exe", Exe: "C:\\Windows\\system32\\svchost-temp.exe", }, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: pi.ProcessId}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.UnicodeString, Value: "calc.exe"}, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: pi.ProcessId}, + params.ProcessName: {Name: params.ProcessName, Type: params.UnicodeString, Value: "calc.exe"}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, } require.True(t, sys.IsProcessRunning(pi.Process)) @@ -523,56 +522,56 @@ func BenchmarkRunRules(b *testing.B) { b.ResetTimer() - evts := []*kevent.Kevent{ + evts := []*event.Event{ { - Type: ktypes.ConnectTCPv4, + Type: event.ConnectTCPv4, Name: "Recv", Tid: 2484, PID: 859, - Category: ktypes.Net, + Category: event.Net, PS: &types.PS{ Name: "cmd.exe", }, - Kparams: kevent.Kparams{ - kparams.NetDport: {Name: kparams.NetDport, Type: kparams.Uint16, Value: uint16(443)}, - kparams.NetSport: {Name: kparams.NetSport, Type: kparams.Uint16, Value: uint16(43123)}, - kparams.NetSIP: {Name: kparams.NetSIP, Type: kparams.IPv4, Value: net.ParseIP("127.0.0.1")}, - kparams.NetDIP: {Name: kparams.NetDIP, Type: kparams.IPv4, Value: net.ParseIP("216.58.201.174")}, + Params: event.Params{ + params.NetDport: {Name: params.NetDport, Type: params.Uint16, Value: uint16(443)}, + params.NetSport: {Name: params.NetSport, Type: params.Uint16, Value: uint16(43123)}, + params.NetSIP: {Name: params.NetSIP, Type: params.IPv4, Value: net.ParseIP("127.0.0.1")}, + params.NetDIP: {Name: params.NetDIP, Type: params.IPv4, Value: net.ParseIP("216.58.201.174")}, }, - Metadata: make(map[kevent.MetadataKey]any), + Metadata: make(map[event.MetadataKey]any), }, { - Type: ktypes.CreateProcess, + Type: event.CreateProcess, Name: "CreateProcess", - Category: ktypes.Process, + Category: event.Process, Tid: 2484, PID: 859, PS: &types.PS{ Name: "powershell.exe", }, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: 2323}, - kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.PID, Value: uint32(8390)}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.UnicodeString, Value: "spotify.exe"}, - kparams.Cmdline: {Name: kparams.Cmdline, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`}, - kparams.Exe: {Name: kparams.Exe, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe`}, - kparams.UserSID: {Name: kparams.UserSID, Type: kparams.UnicodeString, Value: `admin\SYSTEM`}, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: 2323}, + params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(8390)}, + params.ProcessName: {Name: params.ProcessName, Type: params.UnicodeString, Value: "spotify.exe"}, + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`}, + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe`}, + params.UserSID: {Name: params.UserSID, Type: params.UnicodeString, Value: `admin\SYSTEM`}, }, - Metadata: make(map[kevent.MetadataKey]any), + Metadata: make(map[event.MetadataKey]any), }, { - Type: ktypes.CreateHandle, + Type: event.CreateHandle, Name: "CreateHandle", - Category: ktypes.Handle, + Category: event.Handle, Tid: 2484, PID: 859, PS: &types.PS{ Name: "powershell.exe", }, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: 2323}, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: 2323}, }, - Metadata: make(map[kevent.MetadataKey]any), + Metadata: make(map[event.MetadataKey]any), }, } diff --git a/pkg/rules/sequence.go b/pkg/rules/sequence.go index c441122f6..e1031fbad 100644 --- a/pkg/rules/sequence.go +++ b/pkg/rules/sequence.go @@ -23,11 +23,10 @@ import ( "expvar" fsm "github.com/qmuntal/stateless" "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/filter" "github.com/rabbitstack/fibratus/pkg/filter/ql" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/rabbitstack/fibratus/pkg/ps" "github.com/rabbitstack/fibratus/pkg/util/atomic" log "github.com/sirupsen/logrus" @@ -86,14 +85,14 @@ type sequenceState struct { maxSpan time.Duration // partials keeps the state of all matched events per expression - partials map[int][]*kevent.Kevent + partials map[int][]*event.Event // mu guards the partials map mu sync.RWMutex // matches stores only the event that matched // the upstream partials. These events will // be propagated in the rule action context - matches map[int]*kevent.Kevent + matches map[int]*event.Event // mmu guards the matches map mmu sync.RWMutex @@ -125,9 +124,9 @@ func newSequenceState(f filter.Filter, c *config.FilterConfig, psnap ps.Snapshot seq: f.GetSequence(), name: c.Name, maxSpan: f.GetSequence().MaxSpan, - partials: make(map[int][]*kevent.Kevent), + partials: make(map[int][]*event.Event), states: make(map[fsm.State]bool), - matches: make(map[int]*kevent.Kevent), + matches: make(map[int]*event.Event), exprs: make(map[int]string), spanDeadlines: make(map[fsm.State]*time.Timer), initialState: sequenceInitialState, @@ -142,10 +141,10 @@ func newSequenceState(f filter.Filter, c *config.FilterConfig, psnap ps.Snapshot return ss } -func (s *sequenceState) events() []*kevent.Kevent { +func (s *sequenceState) events() []*event.Event { s.mmu.RLock() defer s.mmu.RUnlock() - events := make([]*kevent.Kevent, 0, len(s.matches)) + events := make([]*event.Event, 0, len(s.matches)) for _, e := range s.matches { events = append(events, e) } @@ -261,7 +260,7 @@ func (s *sequenceState) configureFSM() { Permit(resetTransition, sequenceInitialState) } -func (s *sequenceState) matchTransition(seqID int, e *kevent.Kevent) error { +func (s *sequenceState) matchTransition(seqID int, e *event.Event) error { s.smu.Lock() defer s.smu.Unlock() shouldFire := !s.states[seqID] @@ -309,7 +308,7 @@ func (s *sequenceState) expr(state fsm.State) string { // addPartial appends the event that matched the expression at the // sequence index. If the event arrived out of order, then the isOOO // parameter is equal to false. -func (s *sequenceState) addPartial(seqID int, e *kevent.Kevent, isOOO bool) { +func (s *sequenceState) addPartial(seqID int, e *event.Event, isOOO bool) { s.mu.Lock() defer s.mu.Unlock() if len(s.partials[seqID]) > maxOutstandingPartials { @@ -331,7 +330,7 @@ func (s *sequenceState) addPartial(seqID int, e *kevent.Kevent, isOOO bool) { } } if isOOO { - e.AddMeta(kevent.RuleSequenceOOOKey, true) + e.AddMeta(event.RuleSequenceOOOKey, true) } log.Debugf("adding partial to sequence [%s] slot [%d] for expression %q, ooo: %t: %s", s.name, seqID, s.expr(seqID), isOOO, e) partialsPerSequence.Add(s.name, 1) @@ -365,8 +364,8 @@ func (s *sequenceState) gc() { } func (s *sequenceState) clear() { - s.partials = make(map[int][]*kevent.Kevent) - s.matches = make(map[int]*kevent.Kevent) + s.partials = make(map[int][]*event.Event) + s.matches = make(map[int]*event.Event) s.states = make(map[fsm.State]bool) s.spanDeadlines = make(map[fsm.State]*time.Timer) s.isPartialsBreached.Store(false) @@ -430,7 +429,7 @@ func (s *sequenceState) scheduleMaxSpanDeadline(seqID fsm.State, maxSpan time.Du s.spanDeadlines[seqID] = t } -func (s *sequenceState) runSequence(e *kevent.Kevent) bool { +func (s *sequenceState) runSequence(e *event.Event) bool { for i, expr := range s.seq.Expressions { // only try to evaluate the expression // if upstream expressions have matched @@ -481,7 +480,7 @@ func (s *sequenceState) runSequence(e *kevent.Kevent) bool { s.mu.RLock() for seqID := range s.partials { for _, evt := range s.partials[seqID] { - if !evt.ContainsMeta(kevent.RuleSequenceOOOKey) { + if !evt.ContainsMeta(event.RuleSequenceOOOKey) { continue } // try to initialize process state before evaluating the event @@ -496,7 +495,7 @@ func (s *sequenceState) runSequence(e *kevent.Kevent) bool { matchTransitionErrors.Add(1) log.Warnf("out of order match transition failure: %v", err) } - evt.RemoveMeta(kevent.RuleSequenceOOOKey) + evt.RemoveMeta(event.RuleSequenceOOOKey) } } } @@ -510,7 +509,7 @@ func (s *sequenceState) runSequence(e *kevent.Kevent) bool { // collect all events involved in the rule match isTerminal := s.isTerminalState() if isTerminal { - setMatch := func(seqID int, e *kevent.Kevent) { + setMatch := func(seqID int, e *event.Event) { s.mmu.Lock() defer s.mmu.Unlock() if s.matches[seqID] == nil { @@ -537,26 +536,26 @@ func (s *sequenceState) runSequence(e *kevent.Kevent) bool { return false } -func (s *sequenceState) expire(e *kevent.Kevent) bool { +func (s *sequenceState) expire(e *event.Event) bool { if !e.IsTerminateProcess() { return false } - canExpire := func(lhs, rhs *kevent.Kevent, isFinalSlot bool) bool { + canExpire := func(lhs, rhs *event.Event, isFinalSlot bool) bool { // if the TerminateProcess event arrives for the // process spawned by CreateProcess, and it pertains // to the final sequence slot, it is safe to expire // the whole sequence - pid := rhs.Kparams.MustGetPid() - if lhs.Type == ktypes.CreateProcess && isFinalSlot { - return lhs.Kparams.MustGetPid() == pid + pid := rhs.Params.MustGetPid() + if lhs.Type == event.CreateProcess && isFinalSlot { + return lhs.Params.MustGetPid() == pid } - if lhs.Type == ktypes.CreateThread { + if lhs.Type == event.CreateThread { // if the pids differ, the thread // is created in a remote process. // Sequence can be expired only if // the remote process terminates - if lhs.PID != lhs.Kparams.MustGetPid() { - return lhs.Kparams.MustGetPid() == pid + if lhs.PID != lhs.Params.MustGetPid() { + return lhs.Params.MustGetPid() == pid } } return lhs.PID == pid @@ -573,8 +572,8 @@ func (s *sequenceState) expire(e *kevent.Kevent) bool { } log.Debugf("removing event originated from %s (%d) "+ "in partials pertaining to sequence [%s] and slot [%d]", - e.Kparams.MustGetString(kparams.ProcessName), - e.Kparams.MustGetPid(), + e.Params.MustGetString(params.ProcessName), + e.Params.MustGetPid(), s.name, idx) // remove partial event from the corresponding slot diff --git a/pkg/rules/sequence_test.go b/pkg/rules/sequence_test.go index 884d41ff4..7cbedc5ae 100644 --- a/pkg/rules/sequence_test.go +++ b/pkg/rules/sequence_test.go @@ -20,11 +20,10 @@ package rules import ( "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/filter" "github.com/rabbitstack/fibratus/pkg/fs" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/rabbitstack/fibratus/pkg/ps" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" log "github.com/sirupsen/logrus" @@ -44,9 +43,9 @@ func TestSequenceState(t *testing.T) { f := filter.New(` sequence maxspan 100ms - |kevt.name = 'CreateProcess' and ps.name = 'cmd.exe'| by ps.exe - |kevt.name = 'CreateFile' and file.path icontains 'temp'| by file.path - |kevt.name = 'CreateProcess'| by ps.child.exe`, + |evt.name = 'CreateProcess' and ps.name = 'cmd.exe'| by ps.exe + |evt.name = 'CreateFile' and file.path icontains 'temp'| by file.path + |evt.name = 'CreateProcess'| by ps.child.exe`, &config.Config{Kstream: config.KstreamConfig{}, Filters: &config.Filters{}}) require.NoError(t, f.Compile()) @@ -55,10 +54,10 @@ func TestSequenceState(t *testing.T) { assert.Equal(t, 0, ss.currentState()) assert.True(t, ss.isInitialState()) - assert.Equal(t, "kevt.name = CreateProcess AND ps.name = cmd.exe", ss.expr(ss.initialState)) + assert.Equal(t, "evt.name = CreateProcess AND ps.name = cmd.exe", ss.expr(ss.initialState)) - e1 := &kevent.Kevent{ - Type: ktypes.CreateProcess, + e1 := &event.Event{ + Type: event.CreateProcess, Name: "CreateProcess", Tid: 2484, PID: 859, @@ -66,9 +65,9 @@ func TestSequenceState(t *testing.T) { Name: "cmd.exe", Exe: "C:\\Windows\\system32\\svchost.exe", }, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(4143)}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.AnsiString, Value: "powershell.exe"}, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(4143)}, + params.ProcessName: {Name: params.ProcessName, Type: params.AnsiString, Value: "powershell.exe"}, }, } require.True(t, ss.next(0)) @@ -80,10 +79,10 @@ func TestSequenceState(t *testing.T) { require.False(t, ss.next(2)) assert.False(t, ss.isInitialState()) - assert.Equal(t, "kevt.name = CreateFile AND file.path ICONTAINS temp", ss.expr(ss.currentState())) + assert.Equal(t, "evt.name = CreateFile AND file.path ICONTAINS temp", ss.expr(ss.currentState())) - e2 := &kevent.Kevent{ - Type: ktypes.CreateFile, + e2 := &event.Event{ + Type: event.CreateFile, Name: "CreateFile", Tid: 2484, PID: 4143, @@ -91,8 +90,8 @@ func TestSequenceState(t *testing.T) { Name: "cmd.exe", Exe: "C:\\Windows\\system32\\svchost.exe", }, - Kparams: kevent.Kparams{ - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Temp\\dropper"}, + Params: event.Params{ + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Temp\\dropper"}, }, } // can't go to the next transitions as the expr hasn't matched @@ -106,15 +105,15 @@ func TestSequenceState(t *testing.T) { assert.Len(t, ss.partials[1], 1) assert.Equal(t, 2, ss.currentState()) - assert.Equal(t, "kevt.name = CreateProcess", ss.expr(ss.currentState())) + assert.Equal(t, "evt.name = CreateProcess", ss.expr(ss.currentState())) - e3 := &kevent.Kevent{ - Type: ktypes.CreateProcess, + e3 := &event.Event{ + Type: event.CreateProcess, Name: "CreateProcess", Tid: 2484, PID: 4143, - Kparams: kevent.Kparams{ - kparams.Exe: {Name: kparams.Exe, Type: kparams.UnicodeString, Value: "C:\\Temp\\dropper.exe"}, + Params: event.Params{ + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: "C:\\Temp\\dropper.exe"}, }, } require.NoError(t, ss.matchTransition(2, e3)) @@ -130,10 +129,10 @@ func TestSequenceState(t *testing.T) { // reset transition leads back to initial state assert.Equal(t, 0, ss.currentState()) - assert.Equal(t, "kevt.name = CreateProcess AND ps.name = cmd.exe", ss.expr(ss.currentState())) + assert.Equal(t, "evt.name = CreateProcess AND ps.name = cmd.exe", ss.expr(ss.currentState())) // deadline exceeded require.NoError(t, ss.matchTransition(0, e1)) - assert.Equal(t, "kevt.name = CreateFile AND file.path ICONTAINS temp", ss.expr(ss.currentState())) + assert.Equal(t, "evt.name = CreateFile AND file.path ICONTAINS temp", ss.expr(ss.currentState())) time.Sleep(time.Millisecond * 120) // transition to initial state assert.True(t, ss.isInitialState()) @@ -156,8 +155,8 @@ func TestSequenceState(t *testing.T) { require.False(t, ss.inDeadline.Load()) // expire entire sequence - e4 := &kevent.Kevent{ - Type: ktypes.TerminateProcess, + e4 := &event.Event{ + Type: event.TerminateProcess, Name: "TerminateProcess", Tid: 2484, PID: 859, @@ -165,9 +164,9 @@ func TestSequenceState(t *testing.T) { Name: "cmd.exe", Exe: "C:\\Windows\\system32\\svchost.exe", }, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(4143)}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.AnsiString, Value: "powershell.exe"}, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(4143)}, + params.ProcessName: {Name: params.ProcessName, Type: params.AnsiString, Value: "powershell.exe"}, }, } require.True(t, ss.expire(e4)) @@ -176,7 +175,7 @@ func TestSequenceState(t *testing.T) { require.NoError(t, ss.matchTransition(0, e1)) require.False(t, ss.inExpired.Load()) - assert.Equal(t, "kevt.name = CreateFile AND file.path ICONTAINS temp", ss.expr(ss.currentState())) + assert.Equal(t, "evt.name = CreateFile AND file.path ICONTAINS temp", ss.expr(ss.currentState())) } func TestSimpleSequence(t *testing.T) { @@ -186,19 +185,19 @@ func TestSimpleSequence(t *testing.T) { f := filter.New(` sequence maxspan 100ms - |kevt.name = 'CreateProcess' and ps.name = 'cmd.exe'| by ps.exe - |kevt.name = 'CreateFile' and file.path icontains 'temp'| by file.path + |evt.name = 'CreateProcess' and ps.name = 'cmd.exe'| by ps.exe + |evt.name = 'CreateFile' and file.path icontains 'temp'| by file.path `, &config.Config{Kstream: config.KstreamConfig{EnableFileIOKevents: true}, Filters: &config.Filters{}}) require.NoError(t, f.Compile()) ss := newSequenceState(f, c, new(ps.SnapshotterMock)) var tests = []struct { - evts []*kevent.Kevent + evts []*event.Event matches []bool }{ - {[]*kevent.Kevent{{ - Type: ktypes.CreateProcess, + {[]*event.Event{{ + Type: event.CreateProcess, Name: "CreateProcess", Timestamp: time.Now(), Tid: 2484, @@ -207,26 +206,26 @@ func TestSimpleSequence(t *testing.T) { Name: "cmd.exe", Exe: "C:\\Windows\\system32\\svchost-temp.exe", }, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.Uint32, Value: uint32(4143)}, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.Uint32, Value: uint32(4143)}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, }, { - Type: ktypes.CreateFile, + Type: event.CreateFile, Name: "CreateFile", Timestamp: time.Now(), Tid: 2484, PID: 859, - Category: ktypes.File, + Category: event.File, PS: &pstypes.PS{ Name: "cmd.exe", }, - Kparams: kevent.Kparams{ - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\svchost-temp.exe"}, + Params: event.Params{ + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\svchost-temp.exe"}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}}}, []bool{false, true}}, - {[]*kevent.Kevent{{ - Type: ktypes.CreateProcess, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}}}, []bool{false, true}}, + {[]*event.Event{{ + Type: event.CreateProcess, Name: "CreateProcess", Timestamp: time.Now(), Tid: 2484, @@ -235,24 +234,24 @@ func TestSimpleSequence(t *testing.T) { Name: "cmd.exe", Exe: "C:\\Windows\\system32\\cmd.exe", }, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.Uint32, Value: uint32(4143)}, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.Uint32, Value: uint32(4143)}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, }, { - Type: ktypes.CreateFile, + Type: event.CreateFile, Name: "CreateFile", Timestamp: time.Now(), Tid: 2484, PID: 859, - Category: ktypes.File, + Category: event.File, PS: &pstypes.PS{ Name: "cmd.exe", }, - Kparams: kevent.Kparams{ - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\svchost-temp.exe"}, + Params: event.Params{ + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\svchost-temp.exe"}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}}}, []bool{false, false}}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}}}, []bool{false, false}}, } for i, tt := range tests { @@ -272,8 +271,8 @@ func TestSimpleSequenceMultiplePartials(t *testing.T) { sequence maxspan 200ms by ps.pid - |kevt.name = 'CreateProcess' and ps.name = 'cmd.exe'| - |kevt.name = 'CreateFile' and file.path icontains 'temp'| + |evt.name = 'CreateProcess' and ps.name = 'cmd.exe'| + |evt.name = 'CreateFile' and file.path icontains 'temp'| `, &config.Config{Kstream: config.KstreamConfig{EnableFileIOKevents: true}, Filters: &config.Filters{}}) require.NoError(t, f.Compile()) @@ -281,8 +280,8 @@ func TestSimpleSequenceMultiplePartials(t *testing.T) { // create random matches which don't satisfy the sequence link for i, pid := range []uint32{2343, 1024, 11122, 3450, 12319} { - e1 := &kevent.Kevent{ - Type: ktypes.CreateProcess, + e1 := &event.Event{ + Type: event.CreateProcess, Timestamp: time.Now().Add(time.Duration(i) * time.Millisecond), Name: "CreateProcess", Tid: 2484, @@ -291,26 +290,26 @@ func TestSimpleSequenceMultiplePartials(t *testing.T) { Name: "cmd.exe", Exe: "C:\\Windows\\system32\\cmd.exe", }, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.Uint32, Value: pid % 2}, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.Uint32, Value: pid % 2}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, } - e2 := &kevent.Kevent{ - Type: ktypes.CreateFile, + e2 := &event.Event{ + Type: event.CreateFile, Timestamp: time.Now().Add(time.Duration(i) * time.Millisecond * 2), Name: "CreateFile", Tid: 2484, PID: pid * 2, - Category: ktypes.File, + Category: event.File, PS: &pstypes.PS{ Name: "cmd.exe", Exe: "C:\\Windows\\system32\\cmd.exe", }, - Kparams: kevent.Kparams{ - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\svchost-temp.exe"}, + Params: event.Params{ + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\svchost-temp.exe"}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, } require.False(t, ss.runSequence(e1)) require.False(t, ss.runSequence(e2)) @@ -320,9 +319,9 @@ func TestSimpleSequenceMultiplePartials(t *testing.T) { assert.Len(t, ss.partials[0], 5) assert.Len(t, ss.partials[1], 0) - e1 := &kevent.Kevent{ + e1 := &event.Event{ Seq: 20, - Type: ktypes.CreateProcess, + Type: event.CreateProcess, Timestamp: time.Now().Add(time.Second), Name: "CreateProcess", Tid: 2484, @@ -334,27 +333,27 @@ func TestSimpleSequenceMultiplePartials(t *testing.T) { Name: "WmiPrvSE.exe", }, }, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.Uint32, Value: uint32(4143)}, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.Uint32, Value: uint32(4143)}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, } - e2 := &kevent.Kevent{ - Type: ktypes.CreateFile, + e2 := &event.Event{ + Type: event.CreateFile, Seq: 22, Timestamp: time.Now().Add(time.Second * time.Duration(2)), Name: "CreateFile", Tid: 2484, PID: 859, - Category: ktypes.File, + Category: event.File, PS: &pstypes.PS{ Name: "cmd.exe", Exe: "C:\\Windows\\system32\\svchost.exe", }, - Kparams: kevent.Kparams{ - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Temp\\file.tmp"}, + Params: event.Params{ + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Temp\\file.tmp"}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, } require.False(t, ss.runSequence(e1)) @@ -368,7 +367,7 @@ func TestSimpleSequenceMultiplePartials(t *testing.T) { assert.Equal(t, uint32(859), ss.matches[0].PID) assert.Equal(t, "WmiPrvSE.exe", ss.matches[0].PS.Parent.Name) assert.Equal(t, uint32(859), ss.matches[1].PID) - assert.Equal(t, "C:\\Temp\\file.tmp", ss.matches[1].GetParamAsString(kparams.FilePath)) + assert.Equal(t, "C:\\Temp\\file.tmp", ss.matches[1].GetParamAsString(params.FilePath)) } func TestSimpleSequenceDeadline(t *testing.T) { @@ -378,15 +377,15 @@ func TestSimpleSequenceDeadline(t *testing.T) { f := filter.New(` sequence maxspan 100ms - |kevt.name = 'CreateProcess' and ps.name = 'cmd.exe'| by ps.exe - |kevt.name = 'CreateFile' and file.path icontains 'temp'| by file.path + |evt.name = 'CreateProcess' and ps.name = 'cmd.exe'| by ps.exe + |evt.name = 'CreateFile' and file.path icontains 'temp'| by file.path `, &config.Config{Kstream: config.KstreamConfig{EnableFileIOKevents: true}, Filters: &config.Filters{}}) require.NoError(t, f.Compile()) ss := newSequenceState(f, c, new(ps.SnapshotterMock)) - e1 := &kevent.Kevent{ - Type: ktypes.CreateProcess, + e1 := &event.Event{ + Type: event.CreateProcess, Timestamp: time.Now(), Name: "CreateProcess", Tid: 2484, @@ -395,28 +394,28 @@ func TestSimpleSequenceDeadline(t *testing.T) { Name: "cmd.exe", Exe: "C:\\Windows\\system32\\svchost-temp.exe", }, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.Uint32, Value: uint32(4143)}, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.Uint32, Value: uint32(4143)}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, } require.False(t, ss.runSequence(e1)) - e2 := &kevent.Kevent{ - Type: ktypes.CreateFile, + e2 := &event.Event{ + Type: event.CreateFile, Timestamp: time.Now(), Name: "CreateFile", Tid: 2484, PID: 859, - Category: ktypes.File, + Category: event.File, PS: &pstypes.PS{ Name: "cmd.exe", Exe: "C:\\Windows\\system32\\svchost.exe", }, - Kparams: kevent.Kparams{ - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\svchost-temp.exe"}, + Params: event.Params{ + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\svchost-temp.exe"}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, } time.Sleep(time.Millisecond * 110) require.False(t, ss.runSequence(e2)) @@ -448,19 +447,19 @@ func TestComplexSequence(t *testing.T) { f := filter.New(` sequence maxspan 1h - |kevt.name = 'CreateProcess' and ps.child.name in ('firefox.exe', 'chrome.exe', 'edge.exe')| by ps.child.pid - |kevt.name = 'CreateFile' and file.operation = 'CREATE' and file.extension = '.exe'| by ps.pid - |kevt.name in ('Send', 'Connect')| by ps.pid + |evt.name = 'CreateProcess' and ps.child.name in ('firefox.exe', 'chrome.exe', 'edge.exe')| by ps.child.pid + |evt.name = 'CreateFile' and file.operation = 'CREATE' and file.extension = '.exe'| by ps.pid + |evt.name in ('Send', 'Connect')| by ps.pid `, &config.Config{Kstream: config.KstreamConfig{EnableFileIOKevents: true}, Filters: &config.Filters{}}) require.NoError(t, f.Compile()) ss := newSequenceState(f, c, new(ps.SnapshotterMock)) - e1 := &kevent.Kevent{ + e1 := &event.Event{ Seq: 1, - Type: ktypes.CreateProcess, + Type: event.CreateProcess, Timestamp: time.Now(), - Category: ktypes.Process, + Category: event.Process, Name: "CreateProcess", Tid: 2484, PID: 859, @@ -468,43 +467,43 @@ func TestComplexSequence(t *testing.T) { Name: "explorer.exe", Exe: "C:\\Windows\\system32\\explorer.exe", }, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(2243)}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.UnicodeString, Value: "firefox.exe"}, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(2243)}, + params.ProcessName: {Name: params.ProcessName, Type: params.UnicodeString, Value: "firefox.exe"}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, } require.False(t, ss.runSequence(e1)) - e2 := &kevent.Kevent{ + e2 := &event.Event{ Seq: 2, - Type: ktypes.CreateFile, + Type: event.CreateFile, Timestamp: time.Now().Add(time.Millisecond * 250), Name: "CreateFile", Tid: 2484, PID: 2243, - Category: ktypes.File, + Category: event.File, PS: &pstypes.PS{ Name: "firefox.exe", Exe: "C:\\Program Files\\Mozilla Firefox\\firefox.exe", Cmdline: "C:\\Program Files\\Mozilla Firefox\\firefox.exe\" -contentproc --channel=\"10464.7.539748228\\1366525930\" -childID 6 -isF", }, - Kparams: kevent.Kparams{ - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Temp\\dropper.exe"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.Enum, Value: uint32(2), Enum: fs.FileCreateDispositions}, + Params: event.Params{ + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Temp\\dropper.exe"}, + params.FileOperation: {Name: params.FileOperation, Type: params.Enum, Value: uint32(2), Enum: fs.FileCreateDispositions}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, } require.False(t, ss.runSequence(e2)) assert.Len(t, ss.partials[0], 1) assert.Len(t, ss.partials[1], 1) - e3 := &kevent.Kevent{ + e3 := &event.Event{ Seq: 4, - Type: ktypes.ConnectTCPv4, + Type: event.ConnectTCPv4, Timestamp: time.Now().Add(time.Second), - Category: ktypes.Net, + Category: event.Net, Name: "Connect", Tid: 244, PID: 2243, @@ -513,10 +512,10 @@ func TestComplexSequence(t *testing.T) { Exe: "C:\\Program Files\\Mozilla Firefox\\firefox.exe", Cmdline: "C:\\Program Files\\Mozilla Firefox\\firefox.exe\" -contentproc --channel=\"10464.7.539748228\\1366525930\" -childID 6 -isF", }, - Kparams: kevent.Kparams{ - kparams.NetDIP: {Name: kparams.NetDIP, Type: kparams.IPv4, Value: net.ParseIP("10.0.2.3")}, + Params: event.Params{ + params.NetDIP: {Name: params.NetDIP, Type: params.IPv4, Value: net.ParseIP("10.0.2.3")}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, } time.Sleep(time.Millisecond * 30) @@ -542,36 +541,36 @@ func TestSequenceOOO(t *testing.T) { f := filter.New(` sequence maxspan 2m - |kevt.name = 'OpenProcess' and kevt.arg[exe] imatches '?:\\Windows\\System32\\lsass.exe'| by ps.uuid - |kevt.name = 'CreateFile' and file.operation = 'CREATE' and file.extension = '.dmp'| by ps.uuid + |evt.name = 'OpenProcess' and evt.arg[exe] imatches '?:\\Windows\\System32\\lsass.exe'| by ps.uuid + |evt.name = 'CreateFile' and file.operation = 'CREATE' and file.extension = '.dmp'| by ps.uuid `, &config.Config{Kstream: config.KstreamConfig{EnableFileIOKevents: true}, Filters: &config.Filters{}}) require.NoError(t, f.Compile()) ss := newSequenceState(f, c, new(ps.SnapshotterMock)) - e1 := &kevent.Kevent{ - Type: ktypes.CreateFile, + e1 := &event.Event{ + Type: event.CreateFile, Timestamp: time.Now(), Name: "CreateFile", Tid: 2484, PID: 859, - Category: ktypes.File, + Category: event.File, PS: &pstypes.PS{ Name: "cmd.exe", Exe: "C:\\Windows\\system32\\rundll32.exe", }, - Kparams: kevent.Kparams{ - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\temp\\lsass.dmp"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.UnicodeString, Value: "CREATE"}, + Params: event.Params{ + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\temp\\lsass.dmp"}, + params.FileOperation: {Name: params.FileOperation, Type: params.UnicodeString, Value: "CREATE"}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, } require.False(t, ss.runSequence(e1)) require.Len(t, ss.partials[1], 1) - assert.True(t, ss.partials[1][0].ContainsMeta(kevent.RuleSequenceOOOKey)) + assert.True(t, ss.partials[1][0].ContainsMeta(event.RuleSequenceOOOKey)) - e2 := &kevent.Kevent{ - Type: ktypes.OpenProcess, + e2 := &event.Event{ + Type: event.OpenProcess, Timestamp: time.Now(), Name: "OpenProcess", Tid: 2484, @@ -580,17 +579,17 @@ func TestSequenceOOO(t *testing.T) { Name: "cmd.exe", Exe: "C:\\Windows\\system32\\rundll32.exe", }, - Kparams: kevent.Kparams{ - kparams.Exe: {Name: kparams.Exe, Type: kparams.UnicodeString, Value: "C:\\Windows\\System32\\lsass.exe"}, - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(2243)}, - kparams.DesiredAccess: {Name: kparams.DesiredAccess, Type: kparams.Flags, Value: uint32(0x1400), Flags: kevent.PsAccessRightFlags}, + Params: event.Params{ + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: "C:\\Windows\\System32\\lsass.exe"}, + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(2243)}, + params.DesiredAccess: {Name: params.DesiredAccess, Type: params.Flags, Value: uint32(0x1400), Flags: event.PsAccessRightFlags}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, } require.True(t, ss.runSequence(e2)) assert.Len(t, ss.partials[0], 1) - assert.False(t, ss.partials[1][0].ContainsMeta(kevent.RuleSequenceOOOKey)) + assert.False(t, ss.partials[1][0].ContainsMeta(event.RuleSequenceOOOKey)) } func TestSequenceGC(t *testing.T) { @@ -602,15 +601,15 @@ func TestSequenceGC(t *testing.T) { f := filter.New(` sequence by ps.uuid - |kevt.name = 'OpenProcess' and kevt.arg[exe] imatches '?:\\Windows\\System32\\lsass.exe'| - |kevt.name = 'CreateFile' and file.operation = 'CREATE' and file.extension = '.dmp'| + |evt.name = 'OpenProcess' and evt.arg[exe] imatches '?:\\Windows\\System32\\lsass.exe'| + |evt.name = 'CreateFile' and file.operation = 'CREATE' and file.extension = '.dmp'| `, &config.Config{Kstream: config.KstreamConfig{EnableFileIOKevents: true}, Filters: &config.Filters{}}) require.NoError(t, f.Compile()) ss := newSequenceState(f, c, new(ps.SnapshotterMock)) - e := &kevent.Kevent{ - Type: ktypes.OpenProcess, + e := &event.Event{ + Type: event.OpenProcess, Timestamp: time.Now(), Name: "OpenProcess", Tid: 2484, @@ -619,12 +618,12 @@ func TestSequenceGC(t *testing.T) { Name: "cmd.exe", Exe: "C:\\Windows\\system32\\rundll32.exe", }, - Kparams: kevent.Kparams{ - kparams.Exe: {Name: kparams.Exe, Type: kparams.UnicodeString, Value: "C:\\Windows\\System32\\lsass.exe"}, - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(2243)}, - kparams.DesiredAccess: {Name: kparams.DesiredAccess, Type: kparams.Flags, Value: uint32(0x1400), Flags: kevent.PsAccessRightFlags}, + Params: event.Params{ + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: "C:\\Windows\\System32\\lsass.exe"}, + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(2243)}, + params.DesiredAccess: {Name: params.DesiredAccess, Type: params.Flags, Value: uint32(0x1400), Flags: event.PsAccessRightFlags}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, } require.False(t, ss.runSequence(e)) @@ -643,19 +642,19 @@ func TestSequenceExpire(t *testing.T) { var tests = []struct { c *config.FilterConfig expr string - evts []*kevent.Kevent + evts []*event.Event wants bool }{ { &config.FilterConfig{Name: "LSASS memory dumping via legitimate or offensive tools"}, `sequence maxspan 2m - |kevt.name = 'OpenProcess' and kevt.arg[exe] imatches '?:\\Windows\\System32\\lsass.exe'| by ps.uuid - |kevt.name = 'CreateFile' and file.operation = 'CREATE' and file.extension = '.dmp'| by ps.uuid + |evt.name = 'OpenProcess' and evt.arg[exe] imatches '?:\\Windows\\System32\\lsass.exe'| by ps.uuid + |evt.name = 'CreateFile' and file.operation = 'CREATE' and file.extension = '.dmp'| by ps.uuid `, - []*kevent.Kevent{ + []*event.Event{ { - Type: ktypes.OpenProcess, + Type: event.OpenProcess, Timestamp: time.Now(), Name: "OpenProcess", Tid: 2484, @@ -664,15 +663,15 @@ func TestSequenceExpire(t *testing.T) { Name: "cmd.exe", Exe: "C:\\Windows\\system32\\rundll32.exe", }, - Kparams: kevent.Kparams{ - kparams.Exe: {Name: kparams.Exe, Type: kparams.UnicodeString, Value: "C:\\Windows\\System32\\lsass.exe"}, - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(2243)}, - kparams.DesiredAccess: {Name: kparams.DesiredAccess, Type: kparams.Flags, Value: uint32(0x1400), Flags: kevent.PsAccessRightFlags}, + Params: event.Params{ + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: "C:\\Windows\\System32\\lsass.exe"}, + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(2243)}, + params.DesiredAccess: {Name: params.DesiredAccess, Type: params.Flags, Value: uint32(0x1400), Flags: event.PsAccessRightFlags}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, }, { - Type: ktypes.TerminateProcess, + Type: event.TerminateProcess, Name: "TerminateProcess", Tid: 2484, PID: 859, @@ -680,9 +679,9 @@ func TestSequenceExpire(t *testing.T) { Name: "cmd.exe", Exe: "C:\\Windows\\system32\\svchost.exe", }, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(4143)}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.AnsiString, Value: "powershell.exe"}, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(4143)}, + params.ProcessName: {Name: params.ProcessName, Type: params.AnsiString, Value: "powershell.exe"}, }, }, }, @@ -692,15 +691,15 @@ func TestSequenceExpire(t *testing.T) { &config.FilterConfig{Name: "System Binary Proxy Execution via Rundll32"}, `sequence maxspan 2m - |kevt.name = 'CreateProcess' and ps.child.name = 'rundll32.exe'| by ps.child.pid - |kevt.name = 'CreateProcess' and ps.child.name = 'connhost.exe'| by ps.pid + |evt.name = 'CreateProcess' and ps.child.name = 'rundll32.exe'| by ps.child.pid + |evt.name = 'CreateProcess' and ps.child.name = 'connhost.exe'| by ps.pid `, - []*kevent.Kevent{ + []*event.Event{ { Seq: 1, - Type: ktypes.CreateProcess, + Type: event.CreateProcess, Timestamp: time.Now(), - Category: ktypes.Process, + Category: event.Process, Name: "CreateProcess", Tid: 2484, PID: 859, @@ -708,17 +707,17 @@ func TestSequenceExpire(t *testing.T) { Name: "explorer.exe", Exe: "C:\\Windows\\system32\\explorer.exe", }, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(2243)}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.UnicodeString, Value: "rundll32.exe"}, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(2243)}, + params.ProcessName: {Name: params.ProcessName, Type: params.UnicodeString, Value: "rundll32.exe"}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, }, { Seq: 2, - Type: ktypes.CreateProcess, + Type: event.CreateProcess, Timestamp: time.Now(), - Category: ktypes.Process, + Category: event.Process, Name: "CreateProcess", Tid: 2484, PID: 2243, @@ -726,14 +725,14 @@ func TestSequenceExpire(t *testing.T) { Name: "explorer.exe", Exe: "C:\\Windows\\system32\\explorer.exe", }, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(12243)}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.UnicodeString, Value: "connhost.exe"}, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(12243)}, + params.ProcessName: {Name: params.ProcessName, Type: params.UnicodeString, Value: "connhost.exe"}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, }, { - Type: ktypes.TerminateProcess, + Type: event.TerminateProcess, Name: "TerminateProcess", Tid: 2484, PID: 859, @@ -741,9 +740,9 @@ func TestSequenceExpire(t *testing.T) { Name: "cmd.exe", Exe: "C:\\Windows\\system32\\svchost.exe", }, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(12243)}, - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.AnsiString, Value: "powershell.exe"}, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(12243)}, + params.ProcessName: {Name: params.ProcessName, Type: params.AnsiString, Value: "powershell.exe"}, }, }, }, @@ -782,16 +781,16 @@ func TestSequenceBoundFields(t *testing.T) { f := filter.New(` sequence maxspan 200ms - |kevt.name = 'CreateProcess' and ps.name = 'cmd.exe'| as e1 - |kevt.name = 'CreateFile' and file.path icontains 'temp' and $e1.ps.sid = ps.sid| as e2 - |kevt.name = 'Connect' and ps.sid != $e2.ps.sid and ps.sid = $e1.ps.sid| + |evt.name = 'CreateProcess' and ps.name = 'cmd.exe'| as e1 + |evt.name = 'CreateFile' and file.path icontains 'temp' and $e1.ps.sid = ps.sid| as e2 + |evt.name = 'Connect' and ps.sid != $e2.ps.sid and ps.sid = $e1.ps.sid| `, &config.Config{Kstream: config.KstreamConfig{EnableFileIOKevents: true}, Filters: &config.Filters{}}) require.NoError(t, f.Compile()) ss := newSequenceState(f, c, new(ps.SnapshotterMock)) - e1 := &kevent.Kevent{ - Type: ktypes.CreateProcess, + e1 := &event.Event{ + Type: event.CreateProcess, Timestamp: time.Now(), Name: "CreateProcess", Tid: 2484, @@ -801,14 +800,14 @@ func TestSequenceBoundFields(t *testing.T) { Exe: "C:\\Windows\\system32\\svchost-temp.exe", SID: "zinet", }, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.Uint32, Value: uint32(4143)}, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.Uint32, Value: uint32(4143)}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, } - e2 := &kevent.Kevent{ - Type: ktypes.CreateProcess, + e2 := &event.Event{ + Type: event.CreateProcess, Timestamp: time.Now().Add(time.Millisecond * 20), Name: "CreateProcess", Tid: 2484, @@ -818,47 +817,47 @@ func TestSequenceBoundFields(t *testing.T) { Exe: "C:\\Windows\\system32\\svchost-temp.exe", SID: "nusret", }, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.Uint32, Value: uint32(4143)}, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.Uint32, Value: uint32(4143)}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, } - e3 := &kevent.Kevent{ - Type: ktypes.CreateFile, + e3 := &event.Event{ + Type: event.CreateFile, Timestamp: time.Now().Add(time.Second), Name: "CreateFile", Tid: 2484, PID: 859, - Category: ktypes.File, + Category: event.File, PS: &pstypes.PS{ Name: "cmd.exe", Exe: "C:\\Windows\\system32\\svchost.exe", SID: "nusret", }, - Kparams: kevent.Kparams{ - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\svchost-temp.exe"}, + Params: event.Params{ + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\svchost-temp.exe"}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, } - e4 := &kevent.Kevent{ - Type: ktypes.ConnectTCPv4, + e4 := &event.Event{ + Type: event.ConnectTCPv4, Timestamp: time.Now().Add(time.Second * 3), Name: "Connect", Tid: 2484, PID: 859, - Category: ktypes.File, + Category: event.File, PS: &pstypes.PS{ Name: "cmd.exe", Exe: "C:\\Windows\\system32\\svchost.exe", SID: "zinet", }, - Kparams: kevent.Kparams{ - kparams.NetDport: {Name: kparams.NetDport, Type: kparams.Uint16, Value: uint16(80)}, - kparams.NetDIP: {Name: kparams.NetDIP, Type: kparams.IPv4, Value: net.ParseIP("172.1.2.3")}, + Params: event.Params{ + params.NetDport: {Name: params.NetDport, Type: params.Uint16, Value: uint16(80)}, + params.NetDIP: {Name: params.NetDIP, Type: params.IPv4, Value: net.ParseIP("172.1.2.3")}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, } require.False(t, ss.runSequence(e1)) @@ -876,8 +875,8 @@ func TestSequenceBoundFieldsWithFunctions(t *testing.T) { f := filter.New(` sequence maxspan 5m - |kevt.name = 'CreateFile' and file.path imatches '?:\\Windows\\System32\\*.dll'| as e1 - |kevt.name = 'RegSetValue' and registry.path ~= 'HKEY_CURRENT_USER\\Volatile Environment\\Notification Packages' + |evt.name = 'CreateFile' and file.path imatches '?:\\Windows\\System32\\*.dll'| as e1 + |evt.name = 'RegSetValue' and registry.path ~= 'HKEY_CURRENT_USER\\Volatile Environment\\Notification Packages' and get_reg_value(registry.path) iin (base($e1.file.path, false))| `, &config.Config{Kstream: config.KstreamConfig{EnableFileIOKevents: true, EnableRegistryKevents: true}, Filters: &config.Filters{}}) @@ -885,36 +884,36 @@ func TestSequenceBoundFieldsWithFunctions(t *testing.T) { ss := newSequenceState(f, c, new(ps.SnapshotterMock)) - e1 := &kevent.Kevent{ - Type: ktypes.CreateFile, + e1 := &event.Event{ + Type: event.CreateFile, Name: "CreateFile", - Category: ktypes.File, + Category: event.File, Tid: 2484, PID: 859, PS: &pstypes.PS{ Name: "cmd.exe", Exe: "C:\\Windows\\system32\\cmd.exe", }, - Kparams: kevent.Kparams{ - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Windows\\System32\\passwdflt.dll"}, + Params: event.Params{ + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\System32\\passwdflt.dll"}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, } - e2 := &kevent.Kevent{ - Type: ktypes.RegSetValue, + e2 := &event.Event{ + Type: event.RegSetValue, Name: "RegSetValue", - Category: ktypes.Registry, + Category: event.Registry, Tid: 2484, PID: 859, PS: &pstypes.PS{ Name: "cmd.exe", Exe: "C:\\Windows\\system32\\cmd.exe", }, - Kparams: kevent.Kparams{ - kparams.RegPath: {Name: kparams.RegPath, Type: kparams.UnicodeString, Value: "HKEY_CURRENT_USER\\Volatile Environment\\Notification Packages"}, + Params: event.Params{ + params.RegPath: {Name: params.RegPath, Type: params.UnicodeString, Value: "HKEY_CURRENT_USER\\Volatile Environment\\Notification Packages"}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, } key, err := registry.OpenKey(registry.CURRENT_USER, "Volatile Environment", registry.SET_VALUE) @@ -938,15 +937,15 @@ func TestIsExpressionEvaluable(t *testing.T) { f := filter.New(` sequence maxspan 100ms - |kevt.name = 'CreateProcess' and ps.name = 'cmd.exe'| by ps.exe - |kevt.name = 'CreateFile' and file.path icontains 'temp'| by file.path + |evt.name = 'CreateProcess' and ps.name = 'cmd.exe'| by ps.exe + |evt.name = 'CreateFile' and file.path icontains 'temp'| by file.path `, &config.Config{Kstream: config.KstreamConfig{EnableFileIOKevents: true}, Filters: &config.Filters{}}) require.NoError(t, f.Compile()) ss := newSequenceState(f, c, new(ps.SnapshotterMock)) - e1 := &kevent.Kevent{ - Type: ktypes.CreateProcess, + e1 := &event.Event{ + Type: event.CreateProcess, Name: "CreateProcess", Tid: 2484, PID: 859, @@ -954,14 +953,14 @@ func TestIsExpressionEvaluable(t *testing.T) { Name: "cmd.exe", Exe: "C:\\Windows\\system32\\svchost.exe", }, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.Uint32, Value: uint32(4143)}, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.Uint32, Value: uint32(4143)}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, } - e2 := &kevent.Kevent{ - Type: ktypes.RenameFile, + e2 := &event.Event{ + Type: event.RenameFile, Name: "RenameFile", Tid: 2484, PID: 859, @@ -969,10 +968,10 @@ func TestIsExpressionEvaluable(t *testing.T) { Name: "cmd.exe", Exe: "C:\\Windows\\system32\\svchost.exe", }, - Kparams: kevent.Kparams{ - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Temp\\dropper"}, + Params: event.Params{ + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Temp\\dropper"}, }, - Metadata: map[kevent.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, + Metadata: map[event.MetadataKey]any{"foo": "bar", "fooz": "barzz"}, } assert.False(t, ss.filter.GetSequence().Expressions[0].IsEvaluable(e2)) diff --git a/pkg/ksource/doc.go b/pkg/source/doc.go similarity index 87% rename from pkg/ksource/doc.go rename to pkg/source/doc.go index c120e0f2b..88b68c408 100644 --- a/pkg/ksource/doc.go +++ b/pkg/source/doc.go @@ -16,5 +16,5 @@ * limitations under the License. */ -// Package ksource defines the contract all event sources have to satisfy. -package ksource +// Package source defines the contract all event sources have to satisfy. +package source diff --git a/pkg/ksource/source.go b/pkg/source/source.go similarity index 91% rename from pkg/ksource/source.go rename to pkg/source/source.go index 7f9433ddc..4ce4f4a7d 100644 --- a/pkg/ksource/source.go +++ b/pkg/source/source.go @@ -16,12 +16,12 @@ * limitations under the License. */ -package ksource +package source import ( "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/filter" - "github.com/rabbitstack/fibratus/pkg/kevent" ) // EventSource defines the contract all event sources have to satisfy. @@ -47,12 +47,12 @@ type EventSource interface { // Events return the channel where event source pushes all captured events. // At this point, the event has the full state associated with it. For example, // the full process state or the event call stack. - Events() <-chan *kevent.Kevent + Events() <-chan *event.Event // SetFilter attaches the filter to the event source. Only events that match // the filter are forwarded to the event source output channel. SetFilter(f filter.Filter) // RegisterEventListener installs event listener. Event listener represents any - // component that satisfies the kevent.Listener interface. Event listener can + // component that satisfies the event.Listener interface. Event listener can // decide if the event is pushed to the output queue. - RegisterEventListener(lis kevent.Listener) + RegisterEventListener(lis event.Listener) } diff --git a/pkg/symbolize/symbolizer.go b/pkg/symbolize/symbolizer.go index bb13b3faa..a62132254 100644 --- a/pkg/symbolize/symbolizer.go +++ b/pkg/symbolize/symbolizer.go @@ -23,9 +23,8 @@ import ( "fmt" "github.com/rabbitstack/fibratus/pkg/callstack" "github.com/rabbitstack/fibratus/pkg/config" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/pe" "github.com/rabbitstack/fibratus/pkg/ps" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" @@ -201,10 +200,10 @@ func (s *Symbolizer) Close() { } } -func (s *Symbolizer) ProcessEvent(e *kevent.Kevent) (bool, error) { +func (s *Symbolizer) ProcessEvent(e *event.Event) (bool, error) { if e.IsTerminateProcess() { // release symbol handler and process handle - pid := e.Kparams.MustGetPid() + pid := e.Params.MustGetPid() s.mu.Lock() defer s.mu.Unlock() if _, ok := s.symbols[pid]; !ok { @@ -223,12 +222,12 @@ func (s *Symbolizer) ProcessEvent(e *kevent.Kevent) (bool, error) { } if e.IsLoadImage() || e.IsUnloadImage() { - filename := e.GetParamAsString(kparams.ImagePath) - addr := e.Kparams.TryGetAddress(kparams.ImageBase) + filename := e.GetParamAsString(params.ImagePath) + addr := e.Params.TryGetAddress(params.ImageBase) // if the kernel driver is loaded or unloaded, // load/unload symbol handlers respectively if (strings.ToLower(filepath.Ext(filename)) == ".sys" || - e.Kparams.TryGetBool(kparams.FileIsDriver)) && s.config.SymbolizeKernelAddresses { + e.Params.TryGetBool(params.FileIsDriver)) && s.config.SymbolizeKernelAddresses { if e.IsLoadImage() { err := s.r.LoadModule(windows.CurrentProcess(), filename, addr) if err != nil { @@ -248,10 +247,10 @@ func (s *Symbolizer) ProcessEvent(e *kevent.Kevent) (bool, error) { } } - if !e.Kparams.Contains(kparams.Callstack) { + if !e.Params.Contains(params.Callstack) { return true, nil } - defer e.Kparams.Remove(kparams.Callstack) + defer e.Params.Remove(params.Callstack) err := s.processCallstack(e) if err != nil { @@ -267,9 +266,9 @@ func (s *Symbolizer) ProcessEvent(e *kevent.Kevent) (bool, error) { // the new module is loaded and not already present in // the map, we parse its export directory and insert // into the map. -func (s *Symbolizer) syncModules(e *kevent.Kevent) error { - filename := e.GetParamAsString(kparams.ImagePath) - addr := e.Kparams.TryGetAddress(kparams.ImageBase) +func (s *Symbolizer) syncModules(e *event.Event) error { + filename := e.GetParamAsString(params.ImagePath) + addr := e.Params.TryGetAddress(params.ImageBase) s.mu.Lock() defer s.mu.Unlock() @@ -308,8 +307,8 @@ func (s *Symbolizer) syncModules(e *kevent.Kevent) error { return nil } -func (s *Symbolizer) processCallstack(e *kevent.Kevent) error { - addrs := e.Kparams.MustGetSliceAddrs(kparams.Callstack) +func (s *Symbolizer) processCallstack(e *event.Event) error { + addrs := e.Params.MustGetSliceAddrs(params.Callstack) e.Callstack.Init(len(addrs)) // skip stack enrichment for the events generated by the System process @@ -330,12 +329,12 @@ func (s *Symbolizer) processCallstack(e *kevent.Kevent) error { // get the address that we want to symbolize switch e.Type { - case ktypes.CreateThread: - pid = e.Kparams.MustGetPid() - addr = e.Kparams.TryGetAddress(kparams.StartAddress) - case ktypes.SubmitThreadpoolWork, ktypes.SubmitThreadpoolCallback: + case event.CreateThread: + pid = e.Params.MustGetPid() + addr = e.Params.TryGetAddress(params.StartAddress) + case event.SubmitThreadpoolWork, event.SubmitThreadpoolCallback: pid = e.PID - addr = e.Kparams.TryGetAddress(kparams.ThreadpoolCallback) + addr = e.Params.TryGetAddress(params.ThreadpoolCallback) } // symbolize thread start or thread pool callback address @@ -350,12 +349,12 @@ func (s *Symbolizer) processCallstack(e *kevent.Kevent) error { if symbol != "" && symbol != "?" { switch e.Type { - case ktypes.CreateThread: - e.Kparams.Append(kparams.StartAddressSymbol, kparams.UnicodeString, symbol) - case ktypes.SubmitThreadpoolWork, ktypes.SubmitThreadpoolCallback: - e.Kparams.Append(kparams.ThreadpoolCallbackSymbol, kparams.UnicodeString, symbol) + case event.CreateThread: + e.Params.Append(params.StartAddressSymbol, params.UnicodeString, symbol) + case event.SubmitThreadpoolWork, event.SubmitThreadpoolCallback: + e.Params.Append(params.ThreadpoolCallbackSymbol, params.UnicodeString, symbol) - ctx := e.Kparams.TryGetAddress(kparams.ThreadpoolContext) + ctx := e.Params.TryGetAddress(params.ThreadpoolContext) // if the callback resolves to one of the functions // that receive the CONTEXT structure as a parameter @@ -364,16 +363,16 @@ func (s *Symbolizer) processCallstack(e *kevent.Kevent) error { if ctx != 0 && threadcontext.IsParamOfFunc(symbol) { rip := threadcontext.Rip(pid, ctx) if rip != 0 { - e.Kparams.Append(kparams.ThreadpoolContextRip, kparams.Address, rip.Uint64()) + e.Params.Append(params.ThreadpoolContextRip, params.Address, rip.Uint64()) m := e.PS.FindModuleByVa(rip) if m != nil { - e.Kparams.Append(kparams.ThreadpoolContextRipModule, kparams.UnicodeString, m.Name) + e.Params.Append(params.ThreadpoolContextRipModule, params.UnicodeString, m.Name) } sym := s.symbolizeAddress(pid, rip, m) if sym != "" && sym != "?" { - e.Kparams.Append(kparams.ThreadpoolContextRipSymbol, kparams.UnicodeString, sym) + e.Params.Append(params.ThreadpoolContextRipSymbol, params.UnicodeString, sym) } } } @@ -382,10 +381,10 @@ func (s *Symbolizer) processCallstack(e *kevent.Kevent) error { if mod != nil { switch e.Type { - case ktypes.CreateThread: - e.Kparams.Append(kparams.StartAddressModule, kparams.UnicodeString, mod.Name) - case ktypes.SubmitThreadpoolWork, ktypes.SubmitThreadpoolCallback: - e.Kparams.Append(kparams.ThreadpoolCallbackModule, kparams.UnicodeString, mod.Name) + case event.CreateThread: + e.Params.Append(params.StartAddressModule, params.UnicodeString, mod.Name) + case event.SubmitThreadpoolWork, event.SubmitThreadpoolCallback: + e.Params.Append(params.ThreadpoolCallbackModule, params.UnicodeString, mod.Name) } } } @@ -427,7 +426,7 @@ func (s *Symbolizer) processCallstack(e *kevent.Kevent) error { // addresses where the first element is the // most recent kernel return address that is // pushed last into the event callstack. -func (s *Symbolizer) pushFrames(addrs []va.Address, e *kevent.Kevent) { +func (s *Symbolizer) pushFrames(addrs []va.Address, e *event.Event) { for i := len(addrs) - 1; i >= 0; i-- { e.Callstack.PushFrame(s.produceFrame(addrs[i], e)) } @@ -440,7 +439,7 @@ func (s *Symbolizer) pushFrames(addrs []va.Address, e *kevent.Kevent) { // PE export directory entries. If either the // symbol or module are not resolved, then we // fall back to Debug API. -func (s *Symbolizer) produceFrame(addr va.Address, e *kevent.Kevent) callstack.Frame { +func (s *Symbolizer) produceFrame(addr va.Address, e *event.Event) callstack.Frame { frame := callstack.Frame{PID: e.PID, Addr: addr} if addr.InSystemRange() { if s.config.SymbolizeKernelAddresses { diff --git a/pkg/symbolize/symbolizer_test.go b/pkg/symbolize/symbolizer_test.go index 5c670aac6..28fcfb8aa 100644 --- a/pkg/symbolize/symbolizer_test.go +++ b/pkg/symbolize/symbolizer_test.go @@ -20,10 +20,9 @@ package symbolize import ( "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/fs" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/rabbitstack/fibratus/pkg/pe" "github.com/rabbitstack/fibratus/pkg/ps" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" @@ -142,23 +141,23 @@ func TestProcessCallstackPeExports(t *testing.T) { }, } - e := &kevent.Kevent{ - Type: ktypes.CreateFile, + e := &event.Event{ + Type: event.CreateFile, Tid: 2484, PID: uint32(os.Getpid()), CPU: 1, Seq: 2, Name: "CreateFile", Timestamp: time.Now(), - Category: ktypes.File, + Category: event.File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", - Kparams: kevent.Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\mimi.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.Enum, Value: uint32(2), Enum: fs.FileCreateDispositions}, - kparams.Callstack: {Name: kparams.Callstack, Type: kparams.Slice, Value: []va.Address{0x7ffb5c1d0396, 0x7ffb5d8e61f4, 0x7ffb3138592e, 0x7ffb313853b2, 0x2638e59e0a5}}, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\mimi.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.Enum, Value: uint32(2), Enum: fs.FileCreateDispositions}, + params.Callstack: {Name: params.Callstack, Type: params.Slice, Value: []va.Address{0x7ffb5c1d0396, 0x7ffb5d8e61f4, 0x7ffb3138592e, 0x7ffb313853b2, 0x2638e59e0a5}}, }, PS: proc, } @@ -202,18 +201,18 @@ func TestProcessCallstackPeExports(t *testing.T) { // and when the image is unloaded and there are // no processes with the image section mapped // inside their VAS, we can remove the module - e2 := &kevent.Kevent{ - Type: ktypes.LoadImage, + e2 := &event.Event{ + Type: event.LoadImage, Tid: 2484, PID: uint32(12328), CPU: 1, Seq: 2, Name: "LoadImage", Timestamp: time.Now(), - Category: ktypes.Image, - Kparams: kevent.Kparams{ - kparams.ImageBase: {Name: kparams.ImageBase, Type: kparams.Address, Value: uint64(0x12345f)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Windows\\System32\\bcrypt32.dll"}, + Category: event.Image, + Params: event.Params{ + params.ImageBase: {Name: params.ImageBase, Type: params.Address, Value: uint64(0x12345f)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\System32\\bcrypt32.dll"}, }, PS: proc, } @@ -221,18 +220,18 @@ func TestProcessCallstackPeExports(t *testing.T) { require.NoError(t, err) assert.Len(t, s.mods, 4) - e3 := &kevent.Kevent{ - Type: ktypes.UnloadImage, + e3 := &event.Event{ + Type: event.UnloadImage, Tid: 2484, PID: uint32(12328), CPU: 1, Seq: 2, Name: "UnloadImage", Timestamp: time.Now(), - Category: ktypes.Image, - Kparams: kevent.Kparams{ - kparams.ImageBase: {Name: kparams.ImageBase, Type: kparams.Address, Value: uint64(0x12345f)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: filepath.Join(os.Getenv("SystemRoot"), "System32", "bcrypt32.dll")}, + Category: event.Image, + Params: event.Params{ + params.ImageBase: {Name: params.ImageBase, Type: params.Address, Value: uint64(0x12345f)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: filepath.Join(os.Getenv("SystemRoot"), "System32", "bcrypt32.dll")}, }, PS: proc, } @@ -280,18 +279,18 @@ func TestProcessCallstack(t *testing.T) { }, Envs: map[string]string{"ProgramData": "C:\\ProgramData", "COMPUTRENAME": "archrabbit"}, } - e := &kevent.Kevent{ - Type: ktypes.CreateProcess, + e := &event.Event{ + Type: event.CreateProcess, Tid: 2484, PID: uint32(os.Getpid()), CPU: 1, Seq: 2, Name: "CreatedProcess", Timestamp: time.Now(), - Category: ktypes.Process, + Category: event.Process, Host: "archrabbit", - Kparams: kevent.Kparams{ - kparams.Callstack: {Name: kparams.Callstack, Type: kparams.Slice, Value: []va.Address{0x7ffb5c1d0396, 0x7ffb5d8e61f4, 0x7ffb3138592e, 0x7ffb313853b2, 0x2638e59e0a5}}, + Params: event.Params{ + params.Callstack: {Name: params.Callstack, Type: params.Slice, Value: []va.Address{0x7ffb5c1d0396, 0x7ffb5d8e61f4, 0x7ffb3138592e, 0x7ffb313853b2, 0x2638e59e0a5}}, }, PS: proc, } @@ -301,18 +300,18 @@ func TestProcessCallstack(t *testing.T) { assert.Equal(t, 1, s.procsSize()) assert.Equal(t, "0x7ffb5c1d0396 C:\\WINDOWS\\System32\\ntdll.dll!NtCreateProcessEx+0x3a2|0x7ffb5d8e61f4 C:\\WINDOWS\\System32\\ntdll.dll!NtCreateProcessEx+0x3a2|0x7ffb3138592e C:\\WINDOWS\\System32\\ntdll.dll!NtCreateProcess+0x3a2|0x7ffb313853b2 C:\\WINDOWS\\System32\\KERNELBASE.dll!CreateProcessW+0x66|0x2638e59e0a5 C:\\WINDOWS\\System32\\KERNEL32.DLL!CreateProcessW+0x54", e.Callstack.String()) - e1 := &kevent.Kevent{ - Type: ktypes.TerminateProcess, + e1 := &event.Event{ + Type: event.TerminateProcess, Tid: 2484, PID: 12345, CPU: 1, Seq: 3, Name: "TerminateProcess", Timestamp: time.Now(), - Category: ktypes.Process, + Category: event.Process, Host: "archrabbit", - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(os.Getpid())}, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(os.Getpid())}, }, PS: proc, } @@ -379,20 +378,20 @@ func TestSymbolizeEventParamAddress(t *testing.T) { {Name: "C:\\Windows\\System32\\user32.dll", Size: 212354, Checksum: 33123343, BaseAddress: va.Address(0x7ffb5d8e11c4), DefaultBaseAddress: va.Address(0x7ffb5d8e11c4)}, }, } - e := &kevent.Kevent{ - Type: ktypes.CreateThread, + e := &event.Event{ + Type: event.CreateThread, Tid: 2484, PID: uint32(os.Getpid()), CPU: 1, Seq: 2, Name: "CreateThread", Timestamp: time.Now(), - Category: ktypes.Thread, + Category: event.Thread, Host: "archrabbit", - Kparams: kevent.Kparams{ - kparams.Callstack: {Name: kparams.Callstack, Type: kparams.Slice, Value: []va.Address{0x7ffb5c1d0396, 0x7ffb5d8e61f4, 0x7ffb3138592e, 0x7ffb313853b2, 0x2638e59e0a5}}, - kparams.StartAddress: {Name: kparams.StartAddress, Type: kparams.Address, Value: uint64(0x7ffb3138592e)}, - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(os.Getpid())}, + Params: event.Params{ + params.Callstack: {Name: params.Callstack, Type: params.Slice, Value: []va.Address{0x7ffb5c1d0396, 0x7ffb5d8e61f4, 0x7ffb3138592e, 0x7ffb313853b2, 0x2638e59e0a5}}, + params.StartAddress: {Name: params.StartAddress, Type: params.Address, Value: uint64(0x7ffb3138592e)}, + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(os.Getpid())}, }, PS: proc, } @@ -400,23 +399,23 @@ func TestSymbolizeEventParamAddress(t *testing.T) { _, err := s.ProcessEvent(e) require.NoError(t, err) - assert.Equal(t, "CreateProcessW", e.GetParamAsString(kparams.StartAddressSymbol)) - assert.Equal(t, "C:\\Windows\\System32\\ntdll.dll", e.GetParamAsString(kparams.StartAddressModule)) + assert.Equal(t, "CreateProcessW", e.GetParamAsString(params.StartAddressSymbol)) + assert.Equal(t, "C:\\Windows\\System32\\ntdll.dll", e.GetParamAsString(params.StartAddressModule)) - e1 := &kevent.Kevent{ - Type: ktypes.SubmitThreadpoolCallback, + e1 := &event.Event{ + Type: event.SubmitThreadpoolCallback, Tid: 2484, PID: uint32(os.Getpid()), CPU: 1, Seq: 2, Name: "SubmitThreadpoolCallback", Timestamp: time.Now(), - Category: ktypes.Threadpool, + Category: event.Threadpool, Host: "archrabbit", - Kparams: kevent.Kparams{ - kparams.Callstack: {Name: kparams.Callstack, Type: kparams.Slice, Value: []va.Address{0x7ffb5c1d0396}}, - kparams.ThreadpoolCallback: {Name: kparams.ThreadpoolCallback, Type: kparams.Address, Value: uint64(0x7ffb3138592e)}, - kparams.ThreadpoolContext: {Name: kparams.ThreadpoolContext, Type: kparams.Address, Value: uint64(0)}, + Params: event.Params{ + params.Callstack: {Name: params.Callstack, Type: params.Slice, Value: []va.Address{0x7ffb5c1d0396}}, + params.ThreadpoolCallback: {Name: params.ThreadpoolCallback, Type: params.Address, Value: uint64(0x7ffb3138592e)}, + params.ThreadpoolContext: {Name: params.ThreadpoolContext, Type: params.Address, Value: uint64(0)}, }, PS: proc, } @@ -424,8 +423,8 @@ func TestSymbolizeEventParamAddress(t *testing.T) { _, err = s.ProcessEvent(e1) require.NoError(t, err) - assert.Equal(t, "CreateProcessW", e1.GetParamAsString(kparams.ThreadpoolCallbackSymbol)) - assert.Equal(t, "C:\\Windows\\System32\\ntdll.dll", e1.GetParamAsString(kparams.ThreadpoolCallbackModule)) + assert.Equal(t, "CreateProcessW", e1.GetParamAsString(params.ThreadpoolCallbackSymbol)) + assert.Equal(t, "C:\\Windows\\System32\\ntdll.dll", e1.GetParamAsString(params.ThreadpoolCallbackModule)) } func init() { @@ -452,18 +451,18 @@ func TestProcessCallstackProcsTTL(t *testing.T) { n := 10 for n > 0 { - e := &kevent.Kevent{ - Type: ktypes.CreateProcess, + e := &event.Event{ + Type: event.CreateProcess, Tid: 2484, PID: uint32(os.Getpid()), CPU: 1, Seq: 2, Name: "CreatedProcess", Timestamp: time.Now().Add(time.Millisecond * time.Duration(n)), - Category: ktypes.Process, + Category: event.Process, Host: "archrabbit", - Kparams: kevent.Kparams{ - kparams.Callstack: {Name: kparams.Callstack, Type: kparams.Slice, Value: []va.Address{0x7ffb5c1d0396, 0x7ffb5d8e61f4, 0x7ffb3138592e, 0x7ffb313853b2, 0x2638e59e0a5}}, + Params: event.Params{ + params.Callstack: {Name: params.Callstack, Type: params.Slice, Value: []va.Address{0x7ffb5c1d0396, 0x7ffb5d8e61f4, 0x7ffb3138592e, 0x7ffb313853b2, 0x2638e59e0a5}}, }, } _, _ = s.ProcessEvent(e) diff --git a/pkg/sys/etw/etw.go b/pkg/sys/etw/etw.go index f90f24051..3874d7e94 100644 --- a/pkg/sys/etw/etw.go +++ b/pkg/sys/etw/etw.go @@ -22,7 +22,7 @@ package etw import ( - kerrors "github.com/rabbitstack/fibratus/pkg/errors" + "github.com/rabbitstack/fibratus/pkg/errors" "golang.org/x/sys/windows" "os" "unsafe" @@ -96,17 +96,17 @@ func StartTrace(name string, props EventTraceProperties) (TraceHandle, error) { } switch err.(windows.Errno) { case windows.ERROR_ACCESS_DENIED: - return TraceHandle(0), kerrors.ErrTraceAccessDenied + return TraceHandle(0), errors.ErrTraceAccessDenied case windows.ERROR_DISK_FULL: - return TraceHandle(0), kerrors.ErrTraceDiskFull + return TraceHandle(0), errors.ErrTraceDiskFull case windows.ERROR_ALREADY_EXISTS: - return TraceHandle(0), kerrors.ErrTraceAlreadyRunning + return TraceHandle(0), errors.ErrTraceAlreadyRunning case windows.ERROR_INVALID_PARAMETER: - return TraceHandle(0), kerrors.ErrTraceInvalidParameter + return TraceHandle(0), errors.ErrTraceInvalidParameter case windows.ERROR_BAD_LENGTH: - return TraceHandle(0), kerrors.ErrTraceBadLength + return TraceHandle(0), errors.ErrTraceBadLength case windows.ERROR_NO_SYSTEM_RESOURCES: - return TraceHandle(0), kerrors.ErrTraceNoSysResources + return TraceHandle(0), errors.ErrTraceNoSysResources default: return TraceHandle(0), os.NewSyscallError("StartTrace", err) } @@ -168,11 +168,11 @@ func ProcessTrace(handle TraceHandle) error { } switch err.(windows.Errno) { case windows.ERROR_WMI_INSTANCE_NOT_FOUND: - return kerrors.ErrKsessionNotRunning + return errors.ErrSessionNotRunning case windows.ERROR_NOACCESS: - return kerrors.ErrEventCallbackException + return errors.ErrEventCallbackException case windows.ERROR_CANCELLED: - return kerrors.ErrTraceCancelled + return errors.ErrTraceCancelled default: return os.NewSyscallError("ProcessTrace", err) } diff --git a/pkg/util/eventlog/eventlog.go b/pkg/util/eventlog/eventlog.go index 21082a6ca..7ee583ddd 100644 --- a/pkg/util/eventlog/eventlog.go +++ b/pkg/util/eventlog/eventlog.go @@ -20,7 +20,7 @@ package eventlog import ( "fmt" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/event" "golang.org/x/sys/windows/registry" ) @@ -39,7 +39,7 @@ const ( var ErrKeyExists = fmt.Errorf("%s\\%s already exists", keyName, Source) // categoryCount indicates the number of current event categories -var categoryCount = uint32(len(ktypes.Categories())) +var categoryCount = uint32(len(event.Categories())) // Level is the type definition for the eventlog log level type Level uint16 diff --git a/pkg/yara/config/config.go b/pkg/yara/config/config.go index 9d29ce4ae..084adfdc0 100644 --- a/pkg/yara/config/config.go +++ b/pkg/yara/config/config.go @@ -22,9 +22,8 @@ import ( "bytes" "fmt" "github.com/mitchellh/mapstructure" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/util/wildcard" ytypes "github.com/rabbitstack/fibratus/pkg/yara/types" "github.com/spf13/pflag" @@ -164,8 +163,8 @@ func (c Config) ShouldSkipFile(file string) bool { // AlertTitle returns the brief alert title depending on // whether the process scan took place or a file/registry // key was scanned. -func (c Config) AlertTitle(e *kevent.Kevent) string { - if (e.Category == ktypes.File && e.GetParamAsString(kparams.FilePath) != "") || e.Category == ktypes.Registry { +func (c Config) AlertTitle(e *event.Event) string { + if (e.Category == event.File && e.GetParamAsString(params.FilePath) != "") || e.Category == event.Registry { return FileThreatAlertTitle } return MemoryThreatAlertTitle @@ -174,7 +173,7 @@ func (c Config) AlertTitle(e *kevent.Kevent) string { // AlertText returns the short alert text if the Go template is // not specified. On the contrary, the provided Go template is // parsed and executing yielding the alert text. -func (c Config) AlertText(e *kevent.Kevent, match ytypes.MatchRule) (string, error) { +func (c Config) AlertText(e *event.Event, match ytypes.MatchRule) (string, error) { if c.AlertTemplate == "" { threat := match.ThreatName() if threat == "" { @@ -186,7 +185,7 @@ func (c Config) AlertText(e *kevent.Kevent, match ytypes.MatchRule) (string, err var writer bytes.Buffer var data = struct { Match ytypes.MatchRule - Event *kevent.Kevent + Event *event.Event }{ match, e, diff --git a/pkg/yara/config/config_test.go b/pkg/yara/config/config_test.go index c5d55d70b..aee4a9c33 100644 --- a/pkg/yara/config/config_test.go +++ b/pkg/yara/config/config_test.go @@ -20,9 +20,8 @@ package config import ( "errors" - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" ytypes "github.com/rabbitstack/fibratus/pkg/yara/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -106,25 +105,25 @@ func TestShouldSkipFile(t *testing.T) { func TestAlertTitle(t *testing.T) { var tests = []struct { - e *kevent.Kevent + e *event.Event t string }{ { - &kevent.Kevent{Type: ktypes.MapViewFile, Category: ktypes.File}, + &event.Event{Type: event.MapViewFile, Category: event.File}, MemoryThreatAlertTitle, }, { - &kevent.Kevent{Type: ktypes.MapViewFile, Category: ktypes.File, - Kparams: kevent.Kparams{kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Windows\\System32\\wusa.exe"}}, + &event.Event{Type: event.MapViewFile, Category: event.File, + Params: event.Params{params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\System32\\wusa.exe"}}, }, FileThreatAlertTitle, }, { - &kevent.Kevent{Type: ktypes.RegSetValue, Category: ktypes.Registry}, + &event.Event{Type: event.RegSetValue, Category: event.Registry}, FileThreatAlertTitle, }, { - &kevent.Kevent{Type: ktypes.LoadImage, Category: ktypes.Image}, + &event.Event{Type: event.LoadImage, Category: event.Image}, MemoryThreatAlertTitle, }, } @@ -141,7 +140,7 @@ func TestAlertText(t *testing.T) { var tests = []struct { name string c Config - e *kevent.Kevent + e *event.Event m ytypes.MatchRule text string err error @@ -149,7 +148,7 @@ func TestAlertText(t *testing.T) { { "empty template and no threat_name meta", Config{}, - &kevent.Kevent{Type: ktypes.LoadImage, Category: ktypes.Image}, + &event.Event{Type: event.LoadImage, Category: event.Image}, ytypes.MatchRule{Rule: "Badlands Trojan"}, "Threat detected Badlands Trojan", nil, @@ -157,7 +156,7 @@ func TestAlertText(t *testing.T) { { "empty template and threat_name meta", Config{}, - &kevent.Kevent{Type: ktypes.LoadImage, Category: ktypes.Image}, + &event.Event{Type: event.LoadImage, Category: event.Image}, ytypes.MatchRule{Rule: "Badlands Trojan", Metas: []ytypes.Meta{{Identifier: "threat_name", Value: "Gravity Trojan"}}}, "Threat detected Gravity Trojan", nil, @@ -170,7 +169,7 @@ func TestAlertText(t *testing.T) { Event name: {{ .Event.Name -}} `, }, - &kevent.Kevent{Type: ktypes.LoadImage, Name: "LoadImage", Category: ktypes.Image}, + &event.Event{Type: event.LoadImage, Name: "LoadImage", Category: event.Image}, ytypes.MatchRule{Rule: "Badlands Trojan", Metas: []ytypes.Meta{{Identifier: "threat_name", Value: "Gravity Trojan"}}}, ` Rule name: Badlands Trojan @@ -185,7 +184,7 @@ func TestAlertText(t *testing.T) { Event name: {{ .Evet.Name -}} `, }, - &kevent.Kevent{Type: ktypes.LoadImage, Name: "LoadImage", Category: ktypes.Image}, + &event.Event{Type: event.LoadImage, Name: "LoadImage", Category: event.Image}, ytypes.MatchRule{Rule: "Badlands Trojan", Metas: []ytypes.Meta{{Identifier: "threat_name", Value: "Gravity Trojan"}}}, "", errors.New("yara alert template syntax error"), diff --git a/pkg/yara/scanner.go b/pkg/yara/scanner.go index 2d59437c3..fb3bf21bd 100644 --- a/pkg/yara/scanner.go +++ b/pkg/yara/scanner.go @@ -27,9 +27,8 @@ import ( "fmt" "github.com/google/uuid" "github.com/rabbitstack/fibratus/pkg/alertsender" + "github.com/rabbitstack/fibratus/pkg/event/params" libntfs "github.com/rabbitstack/fibratus/pkg/fs/ntfs" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" "github.com/rabbitstack/fibratus/pkg/sys" "github.com/rabbitstack/fibratus/pkg/util/signature" "github.com/rabbitstack/fibratus/pkg/util/va" @@ -40,7 +39,7 @@ import ( "strings" "github.com/hillu/go-yara/v4" - "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/ps" "github.com/rabbitstack/fibratus/pkg/util/multierror" "github.com/rabbitstack/fibratus/pkg/yara/config" @@ -172,39 +171,39 @@ func parseCompilerErrors(errors []yara.CompilerMessage) error { func (s scanner) CanEnqueue() bool { return false } -func (s *scanner) ProcessEvent(evt *kevent.Kevent) (bool, error) { +func (s *scanner) ProcessEvent(evt *event.Event) (bool, error) { if evt.IsTerminateProcess() { // cleanup - pid := evt.Kparams.MustGetPid() + pid := evt.Params.MustGetPid() delete(s.rwxs, pid) delete(s.mmaps, pid) } return s.Scan(evt) } -func (s scanner) Scan(e *kevent.Kevent) (bool, error) { +func (s scanner) Scan(e *event.Event) (bool, error) { var matches yara.MatchRules var isScanned bool var err error switch e.Type { - case ktypes.CreateProcess: + case event.CreateProcess: // scan the created child process - pid := e.Kparams.MustGetPid() - log.Debugf("scanning child process. pid: %d, exe: %s", pid, e.GetParamAsString(kparams.Exe)) + pid := e.Params.MustGetPid() + log.Debugf("scanning child process. pid: %d, exe: %s", pid, e.GetParamAsString(params.Exe)) matches, err = s.scan(pid) procScans.Add(1) isScanned = true - case ktypes.LoadImage: + case event.LoadImage: // scan the process loading unsigned/untrusted module // or loading the module from unbacked memory region pid := e.PID - addr := e.Kparams.MustGetUint64(kparams.ImageBase) - typ := e.Kparams.MustGetUint32(kparams.ImageSignatureType) + addr := e.Params.MustGetUint64(params.ImageBase) + typ := e.Params.MustGetUint32(params.ImageSignatureType) if typ != signature.None { return false, nil } - filename := e.GetParamAsString(kparams.ImagePath) + filename := e.GetParamAsString(params.ImagePath) if s.config.ShouldSkipFile(filename) { return false, nil } @@ -225,7 +224,7 @@ func (s scanner) Scan(e *kevent.Kevent) (bool, error) { moduleScans.Add(1) isScanned = true } - case ktypes.CreateFile: + case event.CreateFile: if s.config.SkipFiles { return false, nil } @@ -233,15 +232,15 @@ func (s scanner) Scan(e *kevent.Kevent) (bool, error) { return false, nil } - filename := e.GetParamAsString(kparams.FilePath) + filename := e.GetParamAsString(params.FilePath) if s.config.ShouldSkipFile(filename) || (e.PS != nil && s.config.ShouldSkipProcess(e.PS.Exe)) { return false, nil } // scan dropped PE files - isDLL := strings.ToLower(filepath.Ext(filename)) == ".dll" || e.Kparams.TryGetBool(kparams.FileIsDLL) - isDriver := strings.ToLower(filepath.Ext(filename)) == ".sys" || e.Kparams.TryGetBool(kparams.FileIsDriver) - isExe := strings.ToLower(filepath.Ext(filename)) == ".exe" || e.Kparams.TryGetBool(kparams.FileIsExecutable) + isDLL := strings.ToLower(filepath.Ext(filename)) == ".dll" || e.Params.TryGetBool(params.FileIsDLL) + isDriver := strings.ToLower(filepath.Ext(filename)) == ".sys" || e.Params.TryGetBool(params.FileIsDriver) + isExe := strings.ToLower(filepath.Ext(filename)) == ".exe" || e.Params.TryGetBool(params.FileIsExecutable) if isExe || isDLL || isDriver { log.Debugf("scanning PE file %s. pid: %d", filename, e.PID) @@ -271,16 +270,16 @@ func (s scanner) Scan(e *kevent.Kevent) (bool, error) { streamScans.Add(1) isScanned = true } - case ktypes.VirtualAlloc: + case event.VirtualAlloc: if s.config.SkipAllocs { return false, nil } // scan process allocating RWX memory region - pid := e.Kparams.MustGetPid() - addr := e.Kparams.TryGetAddress(kparams.MemBaseAddress) - if e.PID != 4 && e.Kparams.TryGetUint32(kparams.MemProtect) == windows.PAGE_EXECUTE_READWRITE && !s.isRwxMatched(pid) { - log.Debugf("scanning RWX allocation. pid: %d, exe: %s, addr: %s", pid, e.GetParamAsString(kparams.Exe), - e.GetParamAsString(kparams.MemBaseAddress)) + pid := e.Params.MustGetPid() + addr := e.Params.TryGetAddress(params.MemBaseAddress) + if e.PID != 4 && e.Params.TryGetUint32(params.MemProtect) == windows.PAGE_EXECUTE_READWRITE && !s.isRwxMatched(pid) { + log.Debugf("scanning RWX allocation. pid: %d, exe: %s, addr: %s", pid, e.GetParamAsString(params.Exe), + e.GetParamAsString(params.MemBaseAddress)) matches, err = s.scan(pid) allocScans.Add(1) isScanned = true @@ -288,18 +287,18 @@ func (s scanner) Scan(e *kevent.Kevent) (bool, error) { s.rwxs[pid] = addr } } - case ktypes.MapViewFile: + case event.MapViewFile: if s.config.SkipMmaps { return false, nil } // scan process mapping a suspicious RX/RWX section view - pid := e.Kparams.MustGetPid() - prot := e.Kparams.MustGetUint32(kparams.MemProtect) - size := e.Kparams.MustGetUint64(kparams.FileViewSize) + pid := e.Params.MustGetPid() + prot := e.Params.MustGetUint32(params.MemProtect) + size := e.Params.MustGetUint64(params.FileViewSize) if e.PID != 4 && size >= 4096 && ((prot&sys.SectionRX) != 0 && (prot&sys.SectionRWX) != 0) && !s.isMmapMatched(pid) { - filename := e.GetParamAsString(kparams.FilePath) + filename := e.GetParamAsString(params.FilePath) // skip mappings of signed images - addr := e.Kparams.MustGetUint64(kparams.FileViewBase) + addr := e.Params.MustGetUint64(params.FileViewBase) sign := signature.GetSignatures().GetSignature(addr) if sign != nil && sign.IsSigned() && sign.IsTrusted() { return false, nil @@ -309,13 +308,13 @@ func (s scanner) Scan(e *kevent.Kevent) (bool, error) { if s.config.ShouldSkipFile(filename) { return false, nil } - log.Debugf("scanning %s section view mapping. filename: %s pid: %d, addr: %s", e.GetParamAsString(kparams.MemProtect), - filename, pid, e.GetParamAsString(kparams.FileViewBase)) + log.Debugf("scanning %s section view mapping. filename: %s pid: %d, addr: %s", e.GetParamAsString(params.MemProtect), + filename, pid, e.GetParamAsString(params.FileViewBase)) matches, err = s.scan(filename) } else { // otherwise, scan the process - log.Debugf("scanning %s section view mapping. pid: %d, addr: %s", e.GetParamAsString(kparams.MemProtect), pid, - e.GetParamAsString(kparams.FileViewBase)) + log.Debugf("scanning %s section view mapping. pid: %d, addr: %s", e.GetParamAsString(params.MemProtect), pid, + e.GetParamAsString(params.FileViewBase)) matches, err = s.scan(pid) } if len(matches) > 0 { @@ -324,7 +323,7 @@ func (s scanner) Scan(e *kevent.Kevent) (bool, error) { mmapScans.Add(1) isScanned = true } - case ktypes.RegSetValue: + case event.RegSetValue: if s.config.SkipRegistry { return false, nil } @@ -332,16 +331,16 @@ func (s scanner) Scan(e *kevent.Kevent) (bool, error) { return false, nil } // scan registry binary values - if typ := e.Kparams.TryGetUint32(kparams.RegValueType); typ != registry.BINARY { + if typ := e.Params.TryGetUint32(params.RegValueType); typ != registry.BINARY { return false, nil } - v, err := e.Kparams.Get(kparams.RegValue) + v, err := e.Params.Get(params.RegValue) if err != nil { // value not attached to the event return false, nil } if b, ok := v.Value.([]byte); ok && len(b) > 0 { - log.Debugf("scanning registry binary value %s. pid: %d", e.GetParamAsString(kparams.RegPath), e.PID) + log.Debugf("scanning registry binary value %s. pid: %d", e.GetParamAsString(params.RegPath), e.PID) matches, err = s.scan(b) registryScans.Add(1) isScanned = true @@ -389,7 +388,7 @@ func (s scanner) scan(target any) (yara.MatchRules, error) { return matches, nil } -func (s scanner) emit(matches yara.MatchRules, e *kevent.Kevent) error { +func (s scanner) emit(matches yara.MatchRules, e *event.Event) error { senders := alertsender.FindAll() if len(senders) == 0 { return fmt.Errorf("no alertsenders registered. Alert won't be sent") @@ -418,7 +417,7 @@ func (s scanner) emit(matches yara.MatchRules, e *kevent.Kevent) error { if err != nil { return err } - e.AddMeta(kevent.YaraMatchesKey, string(b)) + e.AddMeta(event.YaraMatchesKey, string(b)) // render alert title and text title := s.config.AlertTitle(e) @@ -444,7 +443,7 @@ func (s scanner) emit(matches yara.MatchRules, e *kevent.Kevent) error { id = uuid.New().String() } alert.ID = id - alert.Events = []*kevent.Kevent{e} + alert.Events = []*event.Event{e} alert.Labels = m.Labels() alert.Description = m.Description() diff --git a/pkg/yara/scanner_test.go b/pkg/yara/scanner_test.go index 86079cd8f..80f0d3a36 100644 --- a/pkg/yara/scanner_test.go +++ b/pkg/yara/scanner_test.go @@ -22,8 +22,7 @@ package yara import ( - "github.com/rabbitstack/fibratus/pkg/kevent" - "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/ps" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" "github.com/rabbitstack/fibratus/pkg/sys" @@ -39,7 +38,7 @@ import ( "time" "github.com/rabbitstack/fibratus/pkg/alertsender" - "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/event/params" "golang.org/x/sys/windows" ) @@ -74,14 +73,14 @@ func TestScan(t *testing.T) { var tests = []struct { name string - setup func() (*kevent.Kevent, error) + setup func() (*event.Event, error) newScanner func() (Scanner, error) expectedAlert alertsender.Alert matches bool }{ { "scan spawned process", - func() (*kevent.Kevent, error) { + func() (*event.Event, error) { var si windows.StartupInfo si.Flags = windows.STARTF_USESHOWWINDOW var pi windows.ProcessInformation @@ -123,16 +122,16 @@ func TestScan(t *testing.T) { } psnap.On("Find", pi.ProcessId).Return(true, proc) - e := &kevent.Kevent{ - Type: ktypes.CreateProcess, + e := &event.Event{ + Type: event.CreateProcess, Name: "CreateProcess", Tid: 2484, PID: 859, - Kparams: kevent.Kparams{ - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.UnicodeString, Value: "notepad.exe"}, - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: pi.ProcessId}, + Params: event.Params{ + params.ProcessName: {Name: params.ProcessName, Type: params.UnicodeString, Value: "notepad.exe"}, + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: pi.ProcessId}, }, - Metadata: make(map[kevent.MetadataKey]any), + Metadata: make(map[event.MetadataKey]any), PS: proc, } return e, nil @@ -161,7 +160,7 @@ func TestScan(t *testing.T) { }, { "scan spawned process excluded by config", - func() (*kevent.Kevent, error) { + func() (*event.Event, error) { var si windows.StartupInfo si.Flags = windows.STARTF_USESHOWWINDOW si.ShowWindow = windows.SW_HIDE @@ -204,16 +203,16 @@ func TestScan(t *testing.T) { } psnap.On("Find", pi.ProcessId).Return(true, proc) - e := &kevent.Kevent{ - Type: ktypes.CreateProcess, + e := &event.Event{ + Type: event.CreateProcess, Name: "CreateProcess", Tid: 2484, PID: 859, - Kparams: kevent.Kparams{ - kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.UnicodeString, Value: "notepad.exe"}, - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: pi.ProcessId}, + Params: event.Params{ + params.ProcessName: {Name: params.ProcessName, Type: params.UnicodeString, Value: "notepad.exe"}, + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: pi.ProcessId}, }, - Metadata: make(map[kevent.MetadataKey]any), + Metadata: make(map[event.MetadataKey]any), PS: proc, } return e, nil @@ -238,7 +237,7 @@ func TestScan(t *testing.T) { }, { "scan unsigned module loading", - func() (*kevent.Kevent, error) { + func() (*event.Event, error) { var si windows.StartupInfo si.Flags = windows.STARTF_USESHOWWINDOW var pi windows.ProcessInformation @@ -281,18 +280,18 @@ func TestScan(t *testing.T) { } psnap.On("Find", pid).Return(true, proc) - e := &kevent.Kevent{ - Type: ktypes.LoadImage, + e := &event.Event{ + Type: event.LoadImage, Name: "LoadImage", Tid: 2484, PID: pid, - Kparams: kevent.Kparams{ - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "tests.exe"}, - kparams.ImageBase: {Name: kparams.ImageBase, Type: kparams.Uint64, Value: uint64(0x74888fd99)}, - kparams.ImageSignatureType: {Name: kparams.ImageSignatureType, Type: kparams.Uint32, Value: signature.None}, - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: pid}, + Params: event.Params{ + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "tests.exe"}, + params.ImageBase: {Name: params.ImageBase, Type: params.Uint64, Value: uint64(0x74888fd99)}, + params.ImageSignatureType: {Name: params.ImageSignatureType, Type: params.Uint32, Value: signature.None}, + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: pid}, }, - Metadata: make(map[kevent.MetadataKey]any), + Metadata: make(map[event.MetadataKey]any), PS: proc, } return e, nil @@ -321,7 +320,7 @@ func TestScan(t *testing.T) { }, { "scan pe file created in the file system", - func() (*kevent.Kevent, error) { + func() (*event.Event, error) { proc := &pstypes.PS{ Name: "tests.exe", @@ -335,17 +334,17 @@ func TestScan(t *testing.T) { } psnap.On("Find", 565).Return(true, proc) - e := &kevent.Kevent{ - Type: ktypes.CreateFile, + e := &event.Event{ + Type: event.CreateFile, Name: "CreateFile", - Category: ktypes.File, + Category: event.File, Tid: 2484, PID: 565, - Kparams: kevent.Kparams{ - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: filepath.Join(os.Getenv("windir"), "notepad.exe")}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.Uint32, Value: uint32(windows.FILE_CREATE)}, + Params: event.Params{ + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: filepath.Join(os.Getenv("windir"), "notepad.exe")}, + params.FileOperation: {Name: params.FileOperation, Type: params.Uint32, Value: uint32(windows.FILE_CREATE)}, }, - Metadata: make(map[kevent.MetadataKey]any), + Metadata: make(map[event.MetadataKey]any), PS: proc, } return e, nil @@ -374,7 +373,7 @@ func TestScan(t *testing.T) { }, { "scan pe file excluded by config", - func() (*kevent.Kevent, error) { + func() (*event.Event, error) { proc := &pstypes.PS{ Name: "tests.exe", @@ -388,17 +387,17 @@ func TestScan(t *testing.T) { } psnap.On("Find", 565).Return(true, proc) - e := &kevent.Kevent{ - Type: ktypes.CreateFile, + e := &event.Event{ + Type: event.CreateFile, Name: "CreateFile", - Category: ktypes.File, + Category: event.File, Tid: 2484, PID: 565, - Kparams: kevent.Kparams{ - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: filepath.Join(os.Getenv("windir"), "System32", "cmd.exe")}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.Uint32, Value: uint32(windows.FILE_CREATE)}, + Params: event.Params{ + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: filepath.Join(os.Getenv("windir"), "System32", "cmd.exe")}, + params.FileOperation: {Name: params.FileOperation, Type: params.Uint32, Value: uint32(windows.FILE_CREATE)}, }, - Metadata: make(map[kevent.MetadataKey]any), + Metadata: make(map[event.MetadataKey]any), PS: proc, } return e, nil @@ -425,7 +424,7 @@ func TestScan(t *testing.T) { }, { "scan non-pe file created in the file system", - func() (*kevent.Kevent, error) { + func() (*event.Event, error) { proc := &pstypes.PS{ Name: "tests.exe", @@ -439,17 +438,17 @@ func TestScan(t *testing.T) { } psnap.On("Find", 565).Return(true, proc) - e := &kevent.Kevent{ - Type: ktypes.CreateFile, + e := &event.Event{ + Type: event.CreateFile, Name: "CreateFile", - Category: ktypes.File, + Category: event.File, Tid: 2484, PID: 565, - Kparams: kevent.Kparams{ - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: filepath.Join(os.Getenv("windir"), "splwow64.xml")}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.Uint32, Value: uint32(windows.FILE_CREATE)}, + Params: event.Params{ + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: filepath.Join(os.Getenv("windir"), "splwow64.xml")}, + params.FileOperation: {Name: params.FileOperation, Type: params.Uint32, Value: uint32(windows.FILE_CREATE)}, }, - Metadata: make(map[kevent.MetadataKey]any), + Metadata: make(map[event.MetadataKey]any), PS: proc, } return e, nil @@ -473,7 +472,7 @@ func TestScan(t *testing.T) { }, { "scan pe file excluded by generating process name", - func() (*kevent.Kevent, error) { + func() (*event.Event, error) { proc := &pstypes.PS{ Name: "tests.exe", @@ -487,17 +486,17 @@ func TestScan(t *testing.T) { } psnap.On("Find", 565).Return(true, proc) - e := &kevent.Kevent{ - Type: ktypes.CreateFile, + e := &event.Event{ + Type: event.CreateFile, Name: "CreateFile", - Category: ktypes.File, + Category: event.File, Tid: 2484, PID: 565, - Kparams: kevent.Kparams{ - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: filepath.Join(os.Getenv("windir"), "System32", "cmd.exe")}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.Uint32, Value: uint32(windows.FILE_CREATE)}, + Params: event.Params{ + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: filepath.Join(os.Getenv("windir"), "System32", "cmd.exe")}, + params.FileOperation: {Name: params.FileOperation, Type: params.Uint32, Value: uint32(windows.FILE_CREATE)}, }, - Metadata: make(map[kevent.MetadataKey]any), + Metadata: make(map[event.MetadataKey]any), PS: proc, } return e, nil @@ -524,7 +523,7 @@ func TestScan(t *testing.T) { }, { "scan ads created in the file system", - func() (*kevent.Kevent, error) { + func() (*event.Event, error) { ads := filepath.Join(os.TempDir(), "suspicious-ads.txt:mal") f, err := os.Create(ads) if err != nil { @@ -549,17 +548,17 @@ func TestScan(t *testing.T) { } psnap.On("Find", 565).Return(true, proc) - e := &kevent.Kevent{ - Type: ktypes.CreateFile, + e := &event.Event{ + Type: event.CreateFile, Name: "CreateFile", - Category: ktypes.File, + Category: event.File, Tid: 2484, PID: 565, - Kparams: kevent.Kparams{ - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: ads}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.Uint32, Value: uint32(windows.FILE_CREATE)}, + Params: event.Params{ + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: ads}, + params.FileOperation: {Name: params.FileOperation, Type: params.Uint32, Value: uint32(windows.FILE_CREATE)}, }, - Metadata: make(map[kevent.MetadataKey]any), + Metadata: make(map[event.MetadataKey]any), PS: proc, } return e, nil @@ -588,7 +587,7 @@ func TestScan(t *testing.T) { }, { "scan rwx memory region allocation", - func() (*kevent.Kevent, error) { + func() (*event.Event, error) { var si windows.StartupInfo si.Flags = windows.STARTF_USESHOWWINDOW var pi windows.ProcessInformation @@ -632,18 +631,18 @@ func TestScan(t *testing.T) { } psnap.On("Find", pid).Return(true, proc) - e := &kevent.Kevent{ - Type: ktypes.VirtualAlloc, + e := &event.Event{ + Type: event.VirtualAlloc, Name: "VirtualAlloc", - Category: ktypes.Mem, + Category: event.Mem, Tid: 2484, PID: 565, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: pid}, - kparams.MemBaseAddress: {Name: kparams.MemBaseAddress, Type: kparams.Address, Value: uint64(0x7ffe0000)}, - kparams.MemProtect: {Name: kparams.MemProtect, Type: kparams.Flags, Value: uint32(windows.PAGE_EXECUTE_READWRITE), Flags: kevent.MemProtectionFlags}, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: pid}, + params.MemBaseAddress: {Name: params.MemBaseAddress, Type: params.Address, Value: uint64(0x7ffe0000)}, + params.MemProtect: {Name: params.MemProtect, Type: params.Flags, Value: uint32(windows.PAGE_EXECUTE_READWRITE), Flags: event.MemProtectionFlags}, }, - Metadata: make(map[kevent.MetadataKey]any), + Metadata: make(map[event.MetadataKey]any), PS: proc, } return e, nil @@ -672,7 +671,7 @@ func TestScan(t *testing.T) { }, { "scan rx pagefile mmap", - func() (*kevent.Kevent, error) { + func() (*event.Event, error) { var si windows.StartupInfo si.Flags = windows.STARTF_USESHOWWINDOW var pi windows.ProcessInformation @@ -716,19 +715,19 @@ func TestScan(t *testing.T) { } psnap.On("Find", pid).Return(true, proc) - e := &kevent.Kevent{ - Type: ktypes.MapViewFile, + e := &event.Event{ + Type: event.MapViewFile, Name: "MapViewFile", - Category: ktypes.File, + Category: event.File, Tid: 2484, PID: 565, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: pid}, - kparams.FileViewBase: {Name: kparams.FileViewBase, Type: kparams.Address, Value: uint64(0x7ffe0000)}, - kparams.FileViewSize: {Name: kparams.FileViewSize, Type: kparams.Uint64, Value: uint64(12333)}, - kparams.MemProtect: {Name: kparams.MemProtect, Type: kparams.Flags, Value: uint32(sys.SectionRX), Flags: kevent.ViewProtectionFlags}, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: pid}, + params.FileViewBase: {Name: params.FileViewBase, Type: params.Address, Value: uint64(0x7ffe0000)}, + params.FileViewSize: {Name: params.FileViewSize, Type: params.Uint64, Value: uint64(12333)}, + params.MemProtect: {Name: params.MemProtect, Type: params.Flags, Value: uint32(sys.SectionRX), Flags: event.ViewProtectionFlags}, }, - Metadata: make(map[kevent.MetadataKey]any), + Metadata: make(map[event.MetadataKey]any), PS: proc, } return e, nil @@ -757,7 +756,7 @@ func TestScan(t *testing.T) { }, { "scan rx pagefile mmap address for signed module", - func() (*kevent.Kevent, error) { + func() (*event.Event, error) { proc := &pstypes.PS{ Name: "tests.exe", PID: 1123, @@ -772,19 +771,19 @@ func TestScan(t *testing.T) { signature.GetSignatures().PutSignature(uint64(0x7f3e1000), &signature.Signature{Level: signature.AuthenticodeLevel, Type: signature.Catalog}) - e := &kevent.Kevent{ - Type: ktypes.MapViewFile, + e := &event.Event{ + Type: event.MapViewFile, Name: "MapViewFile", - Category: ktypes.File, + Category: event.File, Tid: 2484, PID: 565, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(1123)}, - kparams.FileViewBase: {Name: kparams.FileViewBase, Type: kparams.Address, Value: uint64(0x7f3e1000)}, - kparams.FileViewSize: {Name: kparams.FileViewSize, Type: kparams.Uint64, Value: uint64(12333)}, - kparams.MemProtect: {Name: kparams.MemProtect, Type: kparams.Flags, Value: uint32(sys.SectionRX), Flags: kevent.ViewProtectionFlags}, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1123)}, + params.FileViewBase: {Name: params.FileViewBase, Type: params.Address, Value: uint64(0x7f3e1000)}, + params.FileViewSize: {Name: params.FileViewSize, Type: params.Uint64, Value: uint64(12333)}, + params.MemProtect: {Name: params.MemProtect, Type: params.Flags, Value: uint32(sys.SectionRX), Flags: event.ViewProtectionFlags}, }, - Metadata: make(map[kevent.MetadataKey]any), + Metadata: make(map[event.MetadataKey]any), PS: proc, } return e, nil @@ -808,7 +807,7 @@ func TestScan(t *testing.T) { }, { "scan rx pagefile readonly mmap", - func() (*kevent.Kevent, error) { + func() (*event.Event, error) { proc := &pstypes.PS{ Name: "tests.exe", PID: 321321, @@ -821,19 +820,19 @@ func TestScan(t *testing.T) { } psnap.On("Find", uint32(321321)).Return(true, proc) - e := &kevent.Kevent{ - Type: ktypes.MapViewFile, + e := &event.Event{ + Type: event.MapViewFile, Name: "MapViewFile", - Category: ktypes.File, + Category: event.File, Tid: 2484, PID: 321321, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(321321)}, - kparams.FileViewBase: {Name: kparams.FileViewBase, Type: kparams.Address, Value: uint64(0x7ffe0000)}, - kparams.FileViewSize: {Name: kparams.FileViewSize, Type: kparams.Uint64, Value: uint64(12333)}, - kparams.MemProtect: {Name: kparams.MemProtect, Type: kparams.Flags, Value: uint32(0x10000), Flags: kevent.ViewProtectionFlags}, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(321321)}, + params.FileViewBase: {Name: params.FileViewBase, Type: params.Address, Value: uint64(0x7ffe0000)}, + params.FileViewSize: {Name: params.FileViewSize, Type: params.Uint64, Value: uint64(12333)}, + params.MemProtect: {Name: params.MemProtect, Type: params.Flags, Value: uint32(0x10000), Flags: event.ViewProtectionFlags}, }, - Metadata: make(map[kevent.MetadataKey]any), + Metadata: make(map[event.MetadataKey]any), PS: proc, } return e, nil @@ -857,7 +856,7 @@ func TestScan(t *testing.T) { }, { "scan rwx image file mmap", - func() (*kevent.Kevent, error) { + func() (*event.Event, error) { proc := &pstypes.PS{ Name: "tests.exe", PID: 1123, @@ -870,20 +869,20 @@ func TestScan(t *testing.T) { } psnap.On("Find", 1123).Return(true, proc) - e := &kevent.Kevent{ - Type: ktypes.MapViewFile, + e := &event.Event{ + Type: event.MapViewFile, Name: "MapViewFile", - Category: ktypes.File, + Category: event.File, Tid: 2484, PID: 565, - Kparams: kevent.Kparams{ - kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(1123)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: filepath.Join(os.Getenv("windir"), "regedit.exe")}, - kparams.FileViewBase: {Name: kparams.FileViewBase, Type: kparams.Address, Value: uint64(0x7ffe0000)}, - kparams.FileViewSize: {Name: kparams.FileViewSize, Type: kparams.Uint64, Value: uint64(12333)}, - kparams.MemProtect: {Name: kparams.MemProtect, Type: kparams.Flags, Value: uint32(sys.SectionRWX), Flags: kevent.ViewProtectionFlags}, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1123)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: filepath.Join(os.Getenv("windir"), "regedit.exe")}, + params.FileViewBase: {Name: params.FileViewBase, Type: params.Address, Value: uint64(0x7ffe0000)}, + params.FileViewSize: {Name: params.FileViewSize, Type: params.Uint64, Value: uint64(12333)}, + params.MemProtect: {Name: params.MemProtect, Type: params.Flags, Value: uint32(sys.SectionRWX), Flags: event.ViewProtectionFlags}, }, - Metadata: make(map[kevent.MetadataKey]any), + Metadata: make(map[event.MetadataKey]any), PS: proc, } return e, nil @@ -912,7 +911,7 @@ func TestScan(t *testing.T) { }, { "scan registry binary value", - func() (*kevent.Kevent, error) { + func() (*event.Event, error) { proc := &pstypes.PS{ Name: "tests.exe", PID: 1123, @@ -926,18 +925,18 @@ func TestScan(t *testing.T) { psnap.On("Find", 1123).Return(true, proc) data := []byte{0x6F, 0x66, 0x74, 0x2E, 0x4E, 0x6F, 0x74, 0x65, 0x70, 0x61, 0x64, 0x00, 0x13, 0x00, 0x01, 0x1A} - e := &kevent.Kevent{ - Type: ktypes.RegSetValue, + e := &event.Event{ + Type: event.RegSetValue, Name: "RegSetValue", - Category: ktypes.Registry, + Category: event.Registry, Tid: 2484, PID: 565, - Kparams: kevent.Kparams{ - kparams.RegValueType: {Name: kparams.RegValueType, Type: kparams.Uint32, Value: uint32(registry.BINARY)}, - kparams.RegValue: {Name: kparams.RegValue, Type: kparams.Binary, Value: data}, - kparams.RegPath: {Name: kparams.RegPath, Type: kparams.UnicodeString, Value: `HKEY_LOCAL_MACHINE\CurrentControlSet\Control\DeviceGuard\Mal`}, + Params: event.Params{ + params.RegValueType: {Name: params.RegValueType, Type: params.Uint32, Value: uint32(registry.BINARY)}, + params.RegValue: {Name: params.RegValue, Type: params.Binary, Value: data}, + params.RegPath: {Name: params.RegPath, Type: params.UnicodeString, Value: `HKEY_LOCAL_MACHINE\CurrentControlSet\Control\DeviceGuard\Mal`}, }, - Metadata: make(map[kevent.MetadataKey]any), + Metadata: make(map[event.MetadataKey]any), PS: proc, } return e, nil @@ -990,12 +989,12 @@ func TestScan(t *testing.T) { assert.True(t, len(yaraAlert.Labels) > 0) assert.Len(t, yaraAlert.Events, 1) assert.NotEmpty(t, e.Metadata) - assert.Contains(t, e.Metadata, kevent.YaraMatchesKey) + assert.Contains(t, e.Metadata, event.YaraMatchesKey) } if e.IsCreateProcess() || e.IsLoadImage() || e.IsVirtualAlloc() || e.IsMapViewFile() { // cleanup - proc, err := windows.OpenProcess(windows.PROCESS_TERMINATE, false, e.Kparams.MustGetPid()) + proc, err := windows.OpenProcess(windows.PROCESS_TERMINATE, false, e.Params.MustGetPid()) if err == nil { windows.TerminateProcess(proc, uint32(257)) windows.Close(proc) diff --git a/pkg/yara/types.go b/pkg/yara/types.go index 2710eb00e..c26d0e08f 100644 --- a/pkg/yara/types.go +++ b/pkg/yara/types.go @@ -18,17 +18,17 @@ package yara -import "github.com/rabbitstack/fibratus/pkg/kevent" +import "github.com/rabbitstack/fibratus/pkg/event" // Scanner watches for certain events such as process creation or image loading and // triggers the scanning either on the process memory or on-disk file. If matches occur, // an alert is emitted via registered alert senders. type Scanner interface { - kevent.Listener + event.Listener // Scan runs a scan when the specified signal is observed. The signal // can be the creation of a new process, image loading, writing the PE // file or ADS to the file system, or a suspicious memory allocation. - Scan(*kevent.Kevent) (bool, error) + Scan(*event.Event) (bool, error) // Close disposes any resources allocated by the scanner. Close() } From 92c480bd7a0c743075bb1eaddbffc25551d553bd Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Mon, 26 May 2025 21:52:21 +0200 Subject: [PATCH 02/42] refactor: Trim the `k` prefix from config types Most notable change is the refactoring of KstreamConfig to EventSourceConfig type along with other changes that trim the `k` prefix from types, identifiers or methods. --- cmd/fibratus/app/config/config.go | 4 +- cmd/fibratus/app/stats/stats.go | 4 +- configs/fibratus.json | 6 +- configs/fibratus.yml | 64 +++---- filaments/teamviewer_remote_file_copy.py | 4 +- internal/bootstrap/bootstrap.go | 6 +- internal/etw/consumer.go | 6 +- internal/etw/processors/chain_windows.go | 12 +- internal/etw/processors/fs_windows.go | 4 +- internal/etw/source.go | 28 ++-- internal/etw/source_test.go | 156 +++++++++--------- internal/etw/stackext.go | 14 +- internal/etw/stackext_test.go | 16 +- internal/etw/trace.go | 30 ++-- internal/etw/trace_test.go | 12 +- pkg/aggregator/aggregator_test.go | 8 +- pkg/cap/reader.go | 4 +- pkg/cap/reader_windows.go | 8 +- pkg/cap/section/section_windows_test.go | 6 +- pkg/cap/writer.go | 2 +- pkg/cap/writer_unsupported.go | 4 +- pkg/cap/writer_windows.go | 10 +- pkg/cap/writer_windows_test.go | 18 +- pkg/config/_fixtures/fibratus.json | 6 +- pkg/config/_fixtures/fibratus.yml | 18 +- pkg/config/_fixtures/http-output.yml | 2 +- pkg/config/_fixtures/output.yml | 2 +- pkg/config/_fixtures/transformers.yml | 2 +- pkg/config/config_windows.go | 58 +++---- pkg/config/{kstream.go => eventsource.go} | 114 ++++++------- .../{kstream_test.go => eventsource_test.go} | 34 ++-- pkg/config/print_test.go | 2 +- pkg/config/schema_windows.go | 10 +- pkg/event/formatter.go | 12 +- pkg/event/formatter_windows.go | 4 +- pkg/event/marshaller_test.go | 8 +- pkg/event/marshaller_windows.go | 2 +- pkg/event/queue.go | 6 +- pkg/event/types_windows.go | 8 +- pkg/filament/_fixtures/test_filter.py | 8 +- ...n_next_kevent.py => test_on_next_event.py} | 12 +- pkg/filament/_fixtures/top_hives_io.py | 2 +- pkg/filament/_fixtures/top_keys_io_table.py | 2 +- .../cpython/_fixtures/top_hives_io.py | 4 +- pkg/filament/dict_test.go | 8 +- pkg/filament/filament.go | 78 ++++----- pkg/filament/filament_test.go | 10 +- pkg/filter/filter_test.go | 18 +- pkg/filter/filter_windows.go | 20 +-- pkg/filter/ql/functions/yara_unsupported.go | 4 +- pkg/handle/snapshotter.go | 4 +- pkg/handle/types/types.go | 4 +- pkg/outputs/amqp/amqp_test.go | 22 +-- pkg/outputs/http/http_test.go | 12 +- pkg/pe/marshaller.go | 4 +- pkg/pe/marshaller_test.go | 4 +- pkg/ps/types/marshaller_windows.go | 4 +- pkg/ps/types/marshaller_windows_test.go | 10 +- pkg/ps/types/types_windows.go | 4 +- pkg/rules/engine_test.go | 17 +- pkg/rules/sequence_test.go | 22 +-- pkg/yara/scanner_unsupported.go | 4 +- 62 files changed, 495 insertions(+), 496 deletions(-) rename pkg/config/{kstream.go => eventsource.go} (56%) rename pkg/config/{kstream_test.go => eventsource_test.go} (53%) rename pkg/filament/_fixtures/{test_on_next_kevent.py => test_on_next_event.py} (77%) diff --git a/cmd/fibratus/app/config/config.go b/cmd/fibratus/app/config/config.go index 6ef1feded..88810d92b 100644 --- a/cmd/fibratus/app/config/config.go +++ b/cmd/fibratus/app/config/config.go @@ -22,7 +22,7 @@ import ( "fmt" "github.com/rabbitstack/fibratus/internal/bootstrap" "github.com/rabbitstack/fibratus/pkg/config" - kerrors "github.com/rabbitstack/fibratus/pkg/errors" + errs "github.com/rabbitstack/fibratus/pkg/errors" "github.com/rabbitstack/fibratus/pkg/util/rest" "github.com/spf13/cobra" "os" @@ -49,7 +49,7 @@ func printConfig(cmd *cobra.Command, args []string) error { } body, err := rest.Get(rest.WithTransport(cfg.API.Transport), rest.WithURI("config")) if err != nil { - return kerrors.ErrHTTPServerUnavailable(cfg.API.Transport, err) + return errs.ErrHTTPServerUnavailable(cfg.API.Transport, err) } _, err = fmt.Fprintln(os.Stdout, string(body)) if err != nil { diff --git a/cmd/fibratus/app/stats/stats.go b/cmd/fibratus/app/stats/stats.go index 1335217ed..929775409 100644 --- a/cmd/fibratus/app/stats/stats.go +++ b/cmd/fibratus/app/stats/stats.go @@ -26,7 +26,7 @@ import ( "github.com/jedib0t/go-pretty/v6/table" "github.com/rabbitstack/fibratus/pkg/config" - kerrors "github.com/rabbitstack/fibratus/pkg/errors" + errs "github.com/rabbitstack/fibratus/pkg/errors" "github.com/rabbitstack/fibratus/pkg/util/rest" "github.com/spf13/cobra" ) @@ -123,7 +123,7 @@ func stats(cmd *cobra.Command, args []string) error { c := cfg.API body, err := rest.Get(rest.WithTransport(c.Transport), rest.WithURI("debug/vars")) if err != nil { - return kerrors.ErrHTTPServerUnavailable(c.Transport, err) + return errs.ErrHTTPServerUnavailable(c.Transport, err) } var stats Stats if err := json.Unmarshal(body, &stats); err != nil { diff --git a/configs/fibratus.json b/configs/fibratus.json index cac66b122..b41e7bdb3 100644 --- a/configs/fibratus.json +++ b/configs/fibratus.json @@ -30,15 +30,15 @@ "init-snapshot": true }, - "kevent": { + "event": { }, - "kcap": { + "cap": { }, - "kstream": {}, + "eventsource": {}, "logging": {}, "output": { diff --git a/configs/fibratus.yml b/configs/fibratus.yml index aca889811..28a2cb34e 100644 --- a/configs/fibratus.yml +++ b/configs/fibratus.yml @@ -2,10 +2,10 @@ # =============================== Aggregator ========================================== -# Aggregator is responsible for creating kernel event batches, applying transformers to each event +# Aggregator is responsible for creating event batches, applying transformers to each event # present in the batch, and forwarding those batches to the output sinks. aggregator: - # Determines the flush period that triggers the flushing of the kernel event batches to output sinks + # Determines the flush period that triggers the flushing of the event batches to output sinks flush-period: 500ms # Represents the max time to wait before announcing failed flushing of enqueued events when fibratus @@ -111,7 +111,7 @@ forward: false # =============================== Filament ============================================= -# Filaments are lightweight Python scriplets that are executed on top of the kernel event stream. You can easily +# Filaments are lightweight Python scriplets that are executed on top of the event stream. You can easily # extend Fibratus with custom features that is encapsulated in filaments. This section controls the behaviour of # the filament engine. filament: @@ -156,10 +156,10 @@ handle: # Indicates if process handles are collected during startup or when a new process is spawn. enumerate-handles: false -# =============================== Kevent =============================================== +# =============================== Event =============================================== -# The following settings control the state of the kernel event. -kevent: +# The following settings control the state of the event. +event: # Indicates if threads are serialized as part of the process state serialize-threads: false @@ -175,19 +175,19 @@ kevent: # Indicates if environment variables are serialized as part of the process state serialize-envs: false -# =============================== Kcap ================================================= +# =============================== Capture ================================================= -# Contains the settings that dictate the behaviour of the kernel event captures. +# Contains the settings that dictate the behaviour of the captures. -kcap: - # Specifies the name of the output kcap file. If not empty, capture files are always stored +cap: + # Specifies the name of the output cap file. If not empty, capture files are always stored # to this file by overwriting any existing capture file file: "" -# =============================== Kstream ============================================== +# =============================== Event source ============================================== -# Tweaks for controlling the behaviour of the kernel stream consumer. -kstream: +# Tweaks for controlling the behaviour of the event source. +eventsource: # Determines the maximum number of buffers allocated for the event tracing session's buffer pool #max-buffers: @@ -202,32 +202,32 @@ kstream: # less memory but it increases the rate at which buffers must be flushed) #buffer-size: - # Determines whether thread kernel events are collected by Kernel Logger provider + # Determines whether thread events are collected by Kernel Logger provider #enable-thread: true - # Determines whether registry kernel events are collected by Kernel Logger provider + # Determines whether registry events are collected by Kernel Logger provider #enable-registry: true - # Determines whether network kernel events are collected by Kernel Logger provider + # Determines whether network events are collected by Kernel Logger provider #enable-net: true - # Determines whether file kernel events are collected by Kernel Logger provider + # Determines whether file events are collected by Kernel Logger provider #enable-fileio: true # Determines whether VA map/unmap events are collected by Kernel Logger provider #enable-vamap: true - # Determines whether image kernel events are collected by Kernel Logger provider + # Determines whether image events are collected by Kernel Logger provider #enable-image: true - # Determines whether object manager kernel events (handle creation/destruction) are + # Determines whether object manager events (handle creation/destruction) are # collected by Kernel Logger provider #enable-handle: false - # Determines whether memory manager kernel events are collected by Kernel Logger provider + # Determines whether memory manager events are collected by Kernel Logger provider #enable-mem: true - # Determines whether kernel Audit API calls events are collected + # Determines whether Audit API calls events are collected #enable-audit-api: true # Determines whether DNS client events are collected @@ -282,7 +282,7 @@ logging: # =============================== Output ================================================ -# Outputs transport the event flowing through kernel event stream to its final destination. Only one output +# Outputs transport the event flowing through event stream to its final destination. Only one output # can be active at the time. The following section contains available outputs and their preferences. output: # Console output writes the event to standard output stream. @@ -296,7 +296,7 @@ output: # Template that's feed into event formatter. The default event formatter template is: # - # {{ .Seq }} {{ .Timestamp }} - {{ .CPU }} {{ .Process }} ({{ .Pid }}) - {{ .Type }} ({{ .Kparams }}) + # {{ .Seq }} {{ .Timestamp }} - {{ .CPU }} {{ .Process }} ({{ .Pid }}) - {{ .Type }} ({{ .Params }}) # #template: @@ -330,7 +330,7 @@ output: # Specifies the timeout for periodic health checks #healthcheck-timeout: 5s - # Identifies the user name for the basic HTTP authentication + # Identifies the username for the basic HTTP authentication #username: # Identifies the password for the basic HTTP authentication @@ -349,7 +349,7 @@ output: # Specifies the name of the index template #template-name: fibratus - # Represents the target index for kernel events. It allows time specifiers to create indices per time frame. + # Represents the target index for events. It allows time specifiers to create indices per time frame. # For example, fibratus-%Y-%m generates the index name with current year and month time specifiers #index-name: fibratus @@ -380,7 +380,7 @@ output: # Specifies the AMQP connection timeout #timeout: 5s - # Specifies target exchange name that receives inbound kernel events + # Specifies target exchange name that receives inbound events #exchange: fibratus # Represents the AMQP exchange type. Available exchange type include common types are "direct", "fanout", @@ -519,7 +519,7 @@ pe: # =============================== Transformers ========================================= -# Transformers are responsible for augmenting, parsing or enriching kernel events. +# Transformers are responsible for augmenting, parsing or enriching events. transformers: # Remove transformer deletes provided event parameters. remove: @@ -527,7 +527,7 @@ transformers: enabled: false # Represents the list of parameters that are removed from the event - #kparams: + #params: # - irp # Rename transformer renames parameter from old to new name. @@ -537,7 +537,7 @@ transformers: # Contains the list of old/new mappings. Old represents the original # parameter name, while new is the new parameter name - #kparams: + #params: # - old: # new: @@ -549,7 +549,7 @@ transformers: # Contains the list of parameter replacements. For each target event parameter, the old represent the substring # that gets replaced by the new string. #replacements: - # - kparam: + # - param: # old: # new: @@ -571,12 +571,12 @@ transformers: # Contains the list of parameters associated with the prefix that is trimmed from the parameter's value #prefixes: - # - kparam: + # - param: # trim: # Contains the list of parameters associated with the suffix that is trimmed from the parameter's value #suffixes: - # - kparam: + # - param: # trim: # =============================== YARA ================================================= diff --git a/filaments/teamviewer_remote_file_copy.py b/filaments/teamviewer_remote_file_copy.py index 5e8ae067b..1850e553d 100644 --- a/filaments/teamviewer_remote_file_copy.py +++ b/filaments/teamviewer_remote_file_copy.py @@ -52,13 +52,13 @@ def on_init(): - kfilter("evt.name = 'CreateFile' and ps.name = 'TeamViewer.exe' and file.operation = 'create' " + set_filter("evt.name = 'CreateFile' and ps.name = 'TeamViewer.exe' and file.operation = 'create' " "and file.extension in (%s)" % (', '.join([f'\'{ext}\'' for ext in extensions]))) @dotdictify -def on_next_kevent(event): +def on_next_event(event): emit_alert( f'Remote File Copy via TeamViewer', f'TeamViewer downloaded an executable or script file {event.params.file_name} via transfer session', diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go index 395c8d4b5..7cd552db1 100644 --- a/internal/bootstrap/bootstrap.go +++ b/internal/bootstrap/bootstrap.go @@ -120,7 +120,7 @@ func NewApp(cfg *config.Config, options ...Option) (*App, error) { sigs = signals.Install() } if opts.isCaptureReplay { - reader, err := cap.NewReader(cfg.KcapFile, cfg) + reader, err := cap.NewReader(cfg.CapFile, cfg) if err != nil { return nil, err } @@ -230,7 +230,7 @@ func (f *App) Run(args []string) error { }() } else { // register stack symbolizer - if cfg.Kstream.StackEnrichment { + if cfg.EventSource.StackEnrichment { f.symbolizer = symbolize.NewSymbolizer(symbolize.NewDebugHelpResolver(cfg), f.psnap, cfg, false) f.evs.RegisterEventListener(f.symbolizer) } @@ -288,7 +288,7 @@ func (f *App) WriteCapture(args []string) error { if err != nil { return err } - f.writer, err = cap.NewWriter(f.config.KcapFile, f.psnap, f.hsnap) + f.writer, err = cap.NewWriter(f.config.CapFile, f.psnap, f.hsnap) if err != nil { return err } diff --git a/internal/etw/consumer.go b/internal/etw/consumer.go index f3d4cff65..78fed5590 100644 --- a/internal/etw/consumer.go +++ b/internal/etw/consumer.go @@ -53,7 +53,7 @@ func NewConsumer( evts chan *event.Event, ) *Consumer { return &Consumer{ - q: event.NewQueueWithChannel(evts, config.Kstream.StackEnrichment, config.ForwardMode || config.IsCaptureSet()), + q: event.NewQueueWithChannel(evts, config.EventSource.StackEnrichment, config.ForwardMode || config.IsCaptureSet()), sequencer: sequencer, processors: processors.NewChain(psnap, hsnap, config), psnap: psnap, @@ -77,7 +77,7 @@ func (c *Consumer) ProcessEvent(ev *etw.EventRecord) error { if event.IsCurrentProcDropped(ev.Header.ProcessID) { return nil } - if c.config.Kstream.ExcludeKevent(ev.Header.ProviderID, ev.HookID()) { + if c.config.EventSource.ExcludeEvent(ev.Header.ProviderID, ev.HookID()) { eventsExcluded.Add(1) return nil } @@ -129,7 +129,7 @@ func (c *Consumer) ProcessEvent(ev *etw.EventRecord) error { // the filter is evaluated on the event to // decide whether it should get dropped if (evt.IsDropped(c.config.IsCaptureSet()) || - c.config.Kstream.ExcludeImage(evt.PS)) && !evt.IsStackWalk() { + c.config.EventSource.ExcludeImage(evt.PS)) && !evt.IsStackWalk() { eventsExcluded.Add(1) return nil } diff --git a/internal/etw/processors/chain_windows.go b/internal/etw/processors/chain_windows.go index 0036a0d26..d3651b0e7 100644 --- a/internal/etw/processors/chain_windows.go +++ b/internal/etw/processors/chain_windows.go @@ -50,22 +50,22 @@ func NewChain( chain.addProcessor(newPsProcessor(psnap, vaRegionProber)) - if config.Kstream.EnableFileIOKevents { + if config.EventSource.EnableFileIOEvents { chain.addProcessor(newFsProcessor(hsnap, psnap, devMapper, devPathResolver, config)) } - if config.Kstream.EnableRegistryKevents { + if config.EventSource.EnableRegistryEvents { chain.addProcessor(newRegistryProcessor(hsnap)) } - if config.Kstream.EnableImageKevents { + if config.EventSource.EnableImageEvents { chain.addProcessor(newImageProcessor(psnap)) } - if config.Kstream.EnableNetKevents { + if config.EventSource.EnableNetEvents { chain.addProcessor(newNetProcessor()) } - if config.Kstream.EnableHandleKevents { + if config.EventSource.EnableHandleEvents { chain.addProcessor(newHandleProcessor(hsnap, psnap, devMapper, devPathResolver)) } - if config.Kstream.EnableMemKevents { + if config.EventSource.EnableMemEvents { chain.addProcessor(newMemProcessor(psnap, vaRegionProber)) } diff --git a/internal/etw/processors/fs_windows.go b/internal/etw/processors/fs_windows.go index b1090ee60..f8737868c 100644 --- a/internal/etw/processors/fs_windows.go +++ b/internal/etw/processors/fs_windows.go @@ -208,7 +208,7 @@ func (f *fsProcessor) processEvent(e *event.Event) (*event.Event, error) { f.files[fileObject] = fileinfo } - if f.config.Kstream.EnableHandleKevents { + if f.config.EventSource.EnableHandleEvents { f.devPathResolver.AddPath(ev.GetParamAsString(params.FilePath)) } @@ -228,7 +228,7 @@ func (f *fsProcessor) processEvent(e *event.Event) (*event.Event, error) { // the previous stack walk for CreateFile is popped from // the queue and the callstack parameter attached to the // event. - if f.config.Kstream.StackEnrichment { + if f.config.EventSource.StackEnrichment { f.mu.Lock() defer f.mu.Unlock() diff --git a/internal/etw/source.go b/internal/etw/source.go index 3404a337a..5bcc62456 100644 --- a/internal/etw/source.go +++ b/internal/etw/source.go @@ -125,16 +125,16 @@ func (e *EventSource) Open(config *config.Config) error { // disabled in the config, then thread events // are not captured if e.r != nil { - config.Kstream.EnableThreadKevents = config.Kstream.EnableThreadKevents && e.r.HasThreadEvents - config.Kstream.EnableImageKevents = config.Kstream.EnableImageKevents && e.r.HasImageEvents - config.Kstream.EnableNetKevents = config.Kstream.EnableNetKevents && e.r.HasNetworkEvents - config.Kstream.EnableRegistryKevents = config.Kstream.EnableRegistryKevents && (e.r.HasRegistryEvents || (config.Yara.Enabled && !config.Yara.SkipRegistry)) - config.Kstream.EnableFileIOKevents = config.Kstream.EnableFileIOKevents && (e.r.HasFileEvents || (config.Yara.Enabled && !config.Yara.SkipFiles)) - config.Kstream.EnableVAMapKevents = config.Kstream.EnableVAMapKevents && (e.r.HasVAMapEvents || (config.Yara.Enabled && !config.Yara.SkipMmaps)) - config.Kstream.EnableMemKevents = config.Kstream.EnableMemKevents && (e.r.HasMemEvents || (config.Yara.Enabled && !config.Yara.SkipAllocs)) - config.Kstream.EnableDNSEvents = config.Kstream.EnableDNSEvents && e.r.HasDNSEvents - config.Kstream.EnableAuditAPIEvents = config.Kstream.EnableAuditAPIEvents && e.r.HasAuditAPIEvents - config.Kstream.EnableThreadpoolEvents = config.Kstream.EnableThreadpoolEvents && e.r.HasThreadpoolEvents + config.EventSource.EnableThreadEvents = config.EventSource.EnableThreadEvents && e.r.HasThreadEvents + config.EventSource.EnableImageEvents = config.EventSource.EnableImageEvents && e.r.HasImageEvents + config.EventSource.EnableNetEvents = config.EventSource.EnableNetEvents && e.r.HasNetworkEvents + config.EventSource.EnableRegistryEvents = config.EventSource.EnableRegistryEvents && (e.r.HasRegistryEvents || (config.Yara.Enabled && !config.Yara.SkipRegistry)) + config.EventSource.EnableFileIOEvents = config.EventSource.EnableFileIOEvents && (e.r.HasFileEvents || (config.Yara.Enabled && !config.Yara.SkipFiles)) + config.EventSource.EnableVAMapEvents = config.EventSource.EnableVAMapEvents && (e.r.HasVAMapEvents || (config.Yara.Enabled && !config.Yara.SkipMmaps)) + config.EventSource.EnableMemEvents = config.EventSource.EnableMemEvents && (e.r.HasMemEvents || (config.Yara.Enabled && !config.Yara.SkipAllocs)) + config.EventSource.EnableDNSEvents = config.EventSource.EnableDNSEvents && e.r.HasDNSEvents + config.EventSource.EnableAuditAPIEvents = config.EventSource.EnableAuditAPIEvents && e.r.HasAuditAPIEvents + config.EventSource.EnableThreadpoolEvents = config.EventSource.EnableThreadpoolEvents && e.r.HasThreadpoolEvents for _, typ := range event.All() { if typ == event.CreateProcess || typ == event.TerminateProcess || typ == event.LoadImage || typ == event.UnloadImage { @@ -157,20 +157,20 @@ func (e *EventSource) Open(config *config.Config) error { } if !e.r.ContainsEvent(typ) { - config.Kstream.SetDropMask(typ) + config.EventSource.SetDropMask(typ) } } } e.addTrace(etw.KernelLoggerSession, etw.KernelTraceControlGUID) - if config.Kstream.EnableDNSEvents { + if config.EventSource.EnableDNSEvents { e.addTrace(etw.DNSClientSession, etw.DNSClientGUID) } - if config.Kstream.EnableAuditAPIEvents { + if config.EventSource.EnableAuditAPIEvents { e.addTrace(etw.KernelAuditAPICallsSession, etw.KernelAuditAPICallsGUID) } - if config.Kstream.EnableThreadpoolEvents { + if config.EventSource.EnableThreadpoolEvents { e.addTrace(etw.ThreadpoolSession, etw.ThreadpoolGUID) } diff --git a/internal/etw/source_test.go b/internal/etw/source_test.go index a3a73b9ea..ee4222e37 100644 --- a/internal/etw/source_test.go +++ b/internal/etw/source_test.go @@ -88,13 +88,13 @@ func TestEventSourceStartTraces(t *testing.T) { }{ {"start kernel logger session", &config.Config{ - Kstream: config.KstreamConfig{ - EnableThreadKevents: true, - EnableNetKevents: true, - EnableFileIOKevents: true, - EnableVAMapKevents: true, - BufferSize: 1024, - FlushTimer: time.Millisecond * 2300, + EventSource: config.EventSourceConfig{ + EnableThreadEvents: true, + EnableNetEvents: true, + EnableFileIOEvents: true, + EnableVAMapEvents: true, + BufferSize: 1024, + FlushTimer: time.Millisecond * 2300, }, Filters: &config.Filters{}, }, @@ -103,16 +103,16 @@ func TestEventSourceStartTraces(t *testing.T) { }, {"start kernel logger and audit api sessions", &config.Config{ - Kstream: config.KstreamConfig{ - EnableThreadKevents: true, - EnableNetKevents: true, - EnableFileIOKevents: true, - EnableVAMapKevents: true, - EnableHandleKevents: true, - EnableRegistryKevents: true, - BufferSize: 1024, - FlushTimer: time.Millisecond * 2300, - EnableAuditAPIEvents: true, + EventSource: config.EventSourceConfig{ + EnableThreadEvents: true, + EnableNetEvents: true, + EnableFileIOEvents: true, + EnableVAMapEvents: true, + EnableHandleEvents: true, + EnableRegistryEvents: true, + BufferSize: 1024, + FlushTimer: time.Millisecond * 2300, + EnableAuditAPIEvents: true, }, Filters: &config.Filters{}, }, @@ -183,12 +183,12 @@ func TestEventSourceEnableFlagsDynamically(t *testing.T) { }, } cfg := &config.Config{ - Kstream: config.KstreamConfig{ - EnableThreadKevents: true, - EnableRegistryKevents: true, - EnableImageKevents: true, - EnableFileIOKevents: true, - EnableAuditAPIEvents: true, + EventSource: config.EventSourceConfig{ + EnableThreadEvents: true, + EnableRegistryEvents: true, + EnableImageEvents: true, + EnableFileIOEvents: true, + EnableAuditAPIEvents: true, }, Filters: &config.Filters{}, } @@ -197,7 +197,7 @@ func TestEventSourceEnableFlagsDynamically(t *testing.T) { require.NoError(t, evs.Open(cfg)) defer evs.Close() - flags := evs.(*EventSource).traces[0].enableFlagsDynamically(cfg.Kstream) + flags := evs.(*EventSource).traces[0].enableFlagsDynamically(cfg.EventSource) require.Len(t, evs.(*EventSource).traces, 2) @@ -216,10 +216,10 @@ func TestEventSourceEnableFlagsDynamically(t *testing.T) { // but VAMap is disabled in the config require.True(t, flags&etw.VaMap == 0) - require.False(t, cfg.Kstream.TestDropMask(event.UnloadImage)) - require.True(t, cfg.Kstream.TestDropMask(event.WriteFile)) - require.True(t, cfg.Kstream.TestDropMask(event.UnmapViewFile)) - require.False(t, cfg.Kstream.TestDropMask(event.OpenProcess)) + require.False(t, cfg.EventSource.TestDropMask(event.UnloadImage)) + require.True(t, cfg.EventSource.TestDropMask(event.WriteFile)) + require.True(t, cfg.EventSource.TestDropMask(event.UnmapViewFile)) + require.False(t, cfg.EventSource.TestDropMask(event.OpenProcess)) } func TestEventSourceEnableFlagsDynamicallyWithYaraEnabled(t *testing.T) { @@ -259,14 +259,14 @@ func TestEventSourceEnableFlagsDynamicallyWithYaraEnabled(t *testing.T) { }, } cfg := &config.Config{ - Kstream: config.KstreamConfig{ - EnableThreadKevents: true, - EnableRegistryKevents: true, - EnableImageKevents: true, - EnableFileIOKevents: true, - EnableAuditAPIEvents: true, - EnableVAMapKevents: false, - EnableMemKevents: true, + EventSource: config.EventSourceConfig{ + EnableThreadEvents: true, + EnableRegistryEvents: true, + EnableImageEvents: true, + EnableFileIOEvents: true, + EnableAuditAPIEvents: true, + EnableVAMapEvents: false, + EnableMemEvents: true, }, Filters: &config.Filters{}, Yara: yara.Config{ @@ -281,7 +281,7 @@ func TestEventSourceEnableFlagsDynamicallyWithYaraEnabled(t *testing.T) { require.NoError(t, evs.Open(cfg)) defer evs.Close() - flags := evs.(*EventSource).traces[0].enableFlagsDynamically(cfg.Kstream) + flags := evs.(*EventSource).traces[0].enableFlagsDynamically(cfg.EventSource) // rules compile result doesn't have file events // but Yara file scanning is enabled @@ -292,9 +292,9 @@ func TestEventSourceEnableFlagsDynamicallyWithYaraEnabled(t *testing.T) { // alloc scanning is enabled require.True(t, flags&etw.VirtualAlloc != 0) - require.False(t, cfg.Kstream.TestDropMask(event.CreateFile)) - require.True(t, cfg.Kstream.TestDropMask(event.MapViewFile)) - require.False(t, cfg.Kstream.TestDropMask(event.VirtualAlloc)) + require.False(t, cfg.EventSource.TestDropMask(event.CreateFile)) + require.True(t, cfg.EventSource.TestDropMask(event.MapViewFile)) + require.False(t, cfg.EventSource.TestDropMask(event.VirtualAlloc)) } func TestEventSourceRundownEvents(t *testing.T) { @@ -315,17 +315,17 @@ func TestEventSourceRundownEvents(t *testing.T) { hsnap.On("FindByObject", mock.Anything).Return(htypes.Handle{}, false) hsnap.On("FindHandles", mock.Anything).Return([]htypes.Handle{}, nil) - kstreamConfig := config.KstreamConfig{ - EnableThreadKevents: true, - EnableImageKevents: true, - EnableFileIOKevents: true, - EnableNetKevents: true, - EnableRegistryKevents: true, + evsConfig := config.EventSourceConfig{ + EnableThreadEvents: true, + EnableImageEvents: true, + EnableFileIOEvents: true, + EnableNetEvents: true, + EnableRegistryEvents: true, } cfg := &config.Config{ - Kstream: kstreamConfig, - KcapFile: "fake.cap", // simulate capture to receive state/rundown events - Filters: &config.Filters{}, + EventSource: evsConfig, + CapFile: "fake.cap", // simulate capture to receive state/rundown events + Filters: &config.Filters{}, } evs := NewEventSource(psnap, hsnap, cfg, nil) @@ -727,21 +727,21 @@ func TestEventSourceAllEvents(t *testing.T) { hsnap.On("Write", mock.Anything).Return(nil) hsnap.On("Remove", mock.Anything).Return(nil) - kstreamConfig := config.KstreamConfig{ - EnableThreadKevents: true, - EnableImageKevents: true, - EnableFileIOKevents: true, - EnableVAMapKevents: true, - EnableNetKevents: true, - EnableRegistryKevents: true, - EnableMemKevents: true, - EnableHandleKevents: true, - EnableDNSEvents: true, - EnableAuditAPIEvents: true, - StackEnrichment: false, + evsConfig := config.EventSourceConfig{ + EnableThreadEvents: true, + EnableImageEvents: true, + EnableFileIOEvents: true, + EnableVAMapEvents: true, + EnableNetEvents: true, + EnableRegistryEvents: true, + EnableMemEvents: true, + EnableHandleEvents: true, + EnableDNSEvents: true, + EnableAuditAPIEvents: true, + StackEnrichment: false, } - cfg := &config.Config{Kstream: kstreamConfig, Filters: &config.Filters{}} + cfg := &config.Config{EventSource: evsConfig, Filters: &config.Filters{}} evs := NewEventSource(psnap, hsnap, cfg, nil) l := &MockListener{} @@ -818,7 +818,7 @@ func (s *NoopPsSnapshotter) AddModule(evt *event.Event) error func (s *NoopPsSnapshotter) FindModule(addr va.Address) (bool, *pstypes.Module) { return false, nil } func (s *NoopPsSnapshotter) RemoveThread(pid uint32, tid uint32) error { return nil } func (s *NoopPsSnapshotter) RemoveModule(pid uint32, addr va.Address) error { return nil } -func (s *NoopPsSnapshotter) WriteFromKcap(evt *event.Event) error { return nil } +func (s *NoopPsSnapshotter) WriteFromCapture(evt *event.Event) error { return nil } func (s *NoopPsSnapshotter) AddMmap(evt *event.Event) error { return nil } func (s *NoopPsSnapshotter) RemoveMmap(pid uint32, addr va.Address) error { return nil } @@ -1210,24 +1210,24 @@ func testCallstackEnrichment(t *testing.T, hsnap handle.Snapshotter, psnap ps.Sn }, } - kstreamConfig := config.KstreamConfig{ - EnableThreadKevents: true, - EnableImageKevents: true, - EnableFileIOKevents: true, - EnableRegistryKevents: true, - EnableMemKevents: true, - EnableAuditAPIEvents: true, - StackEnrichment: true, - BufferSize: 1024, - MinBuffers: uint32(runtime.NumCPU() * 2), - MaxBuffers: uint32((runtime.NumCPU() * 2) + 20), - ExcludedImages: []string{"System"}, - ExcludedKevents: []string{"WriteFile", "ReadFile", "RegOpenKey", "RegCloseKey", "CloseFile"}, - FlushTimer: 1, + evsConfig := config.EventSourceConfig{ + EnableThreadEvents: true, + EnableImageEvents: true, + EnableFileIOEvents: true, + EnableRegistryEvents: true, + EnableMemEvents: true, + EnableAuditAPIEvents: true, + StackEnrichment: true, + BufferSize: 1024, + MinBuffers: uint32(runtime.NumCPU() * 2), + MaxBuffers: uint32((runtime.NumCPU() * 2) + 20), + ExcludedImages: []string{"System"}, + ExcludedEvents: []string{"WriteFile", "ReadFile", "RegOpenKey", "RegCloseKey", "CloseFile"}, + FlushTimer: 1, } cfg := &config.Config{ - Kstream: kstreamConfig, + EventSource: evsConfig, Filters: &config.Filters{}, SymbolizeKernelAddresses: true, } diff --git a/internal/etw/stackext.go b/internal/etw/stackext.go index e87cc53e8..b2b2f9379 100644 --- a/internal/etw/stackext.go +++ b/internal/etw/stackext.go @@ -29,11 +29,11 @@ import ( // for particular event or event categories. type StackExtensions struct { ids []etw.ClassicEventID - config config.KstreamConfig + config config.EventSourceConfig } // NewStackExtensions creates an empty stack extensions. -func NewStackExtensions(config config.KstreamConfig) *StackExtensions { +func NewStackExtensions(config config.EventSourceConfig) *StackExtensions { return &StackExtensions{ids: make([]etw.ClassicEventID, 0), config: config} } @@ -61,11 +61,11 @@ func (s *StackExtensions) EventIds() []etw.ClassicEventID { return s.ids } // process address space. func (s *StackExtensions) EnableProcessCallstack() { s.AddStackTracing(event.CreateProcess) - if s.config.EnableThreadKevents { + if s.config.EnableThreadEvents { s.AddStackTracing(event.CreateThread) s.AddStackTracing(event.TerminateThread) } - if s.config.EnableImageKevents { + if s.config.EnableImageEvents { s.AddStackTracingWith(event.ProcessEventGUID, event.LoadImage.HookID()) } } @@ -74,7 +74,7 @@ func (s *StackExtensions) EnableProcessCallstack() { // with event types eligible for publishing call stack // return addresses for file system activity. func (s *StackExtensions) EnableFileCallstack() { - if s.config.EnableFileIOKevents { + if s.config.EnableFileIOEvents { s.AddStackTracing(event.CreateFile) s.AddStackTracing(event.DeleteFile) s.AddStackTracing(event.RenameFile) @@ -85,7 +85,7 @@ func (s *StackExtensions) EnableFileCallstack() { // with event types eligible for publishing call stack // return addresses for registry operations. func (s *StackExtensions) EnableRegistryCallstack() { - if s.config.EnableRegistryKevents { + if s.config.EnableRegistryEvents { s.AddStackTracing(event.RegCreateKey) s.AddStackTracing(event.RegDeleteKey) s.AddStackTracing(event.RegSetValue) @@ -96,7 +96,7 @@ func (s *StackExtensions) EnableRegistryCallstack() { // EnableMemoryCallstack enables stack tracing for the memory // events such as memory allocations. func (s *StackExtensions) EnableMemoryCallstack() { - if s.config.EnableMemKevents { + if s.config.EnableMemEvents { s.AddStackTracing(event.VirtualAlloc) } } diff --git a/internal/etw/stackext_test.go b/internal/etw/stackext_test.go index a2ea5470c..480761b73 100644 --- a/internal/etw/stackext_test.go +++ b/internal/etw/stackext_test.go @@ -29,16 +29,16 @@ import ( func TestStackExtensions(t *testing.T) { cfg := &config.Config{ - Kstream: config.KstreamConfig{ - EnableThreadKevents: true, - EnableNetKevents: true, - EnableFileIOKevents: true, - EnableMemKevents: true, - BufferSize: 1024, - FlushTimer: time.Millisecond * 2300, + EventSource: config.EventSourceConfig{ + EnableThreadEvents: true, + EnableNetEvents: true, + EnableFileIOEvents: true, + EnableMemEvents: true, + BufferSize: 1024, + FlushTimer: time.Millisecond * 2300, }, } - exts := NewStackExtensions(cfg.Kstream) + exts := NewStackExtensions(cfg.EventSource) assert.Len(t, exts.EventIds(), 0) exts.EnableProcessCallstack() diff --git a/internal/etw/trace.go b/internal/etw/trace.go index 7a85963c4..6a1d78ac3 100644 --- a/internal/etw/trace.go +++ b/internal/etw/trace.go @@ -21,7 +21,7 @@ package etw import ( "fmt" "github.com/rabbitstack/fibratus/pkg/config" - kerrors "github.com/rabbitstack/fibratus/pkg/errors" + errs "github.com/rabbitstack/fibratus/pkg/errors" "github.com/rabbitstack/fibratus/pkg/sys/etw" log "github.com/sirupsen/logrus" "golang.org/x/sys/windows" @@ -33,7 +33,7 @@ import ( // initEventTraceProps builds the trace properties descriptor which // influences the behaviour of event publishing to the trace session // buffers. -func initEventTraceProps(c config.KstreamConfig) etw.EventTraceProperties { +func initEventTraceProps(c config.EventSourceConfig) etw.EventTraceProperties { bufferSize := c.BufferSize if bufferSize > maxBufferSize { bufferSize = maxBufferSize @@ -122,7 +122,7 @@ type Trace struct { // NewTrace creates a new trace with specified name, provider GUID, and keywords. func NewTrace(name string, guid windows.GUID, keywords uint64, config *config.Config) *Trace { - t := &Trace{Name: name, GUID: guid, Keywords: keywords, stackExtensions: NewStackExtensions(config.Kstream), config: config} + t := &Trace{Name: name, GUID: guid, Keywords: keywords, stackExtensions: NewStackExtensions(config.EventSource), config: config} t.enableCallstacks() return t } @@ -151,7 +151,7 @@ func (t *Trace) Start() error { if len(t.Name) > maxLoggerNameSize { return fmt.Errorf("trace name [%s] is too long", t.Name) } - cfg := t.config.Kstream + cfg := t.config.EventSource props := initEventTraceProps(cfg) flags := t.enableFlagsDynamically(cfg) if t.IsKernelTrace() { @@ -171,7 +171,7 @@ func (t *Trace) Start() error { return err } if !t.startHandle.IsValid() { - return kerrors.ErrInvalidTrace + return errs.ErrInvalidTrace } if t.IsKernelTrace() { @@ -197,7 +197,7 @@ func (t *Trace) Start() error { sysTraceFlags[0] = flags // enable object manager tracking - if cfg.EnableHandleKevents { + if cfg.EnableHandleEvents { sysTraceFlags[4] = etw.Handle } // enable stack enrichment @@ -329,7 +329,7 @@ func (t *Trace) IsThreadpoolTrace() bool { return t.GUID == etw.ThreadpoolGUID } // machine. Note these flags are relevant to system logger traces // and initializing the EnableFlags field of the etw.EventTraceProperties // structure for non-system logger providers will result in an error. -func (t *Trace) enableFlagsDynamically(config config.KstreamConfig) etw.EventTraceFlags { +func (t *Trace) enableFlagsDynamically(config config.EventSourceConfig) etw.EventTraceFlags { var flags etw.EventTraceFlags if !t.IsKernelTrace() { @@ -338,28 +338,28 @@ func (t *Trace) enableFlagsDynamically(config config.KstreamConfig) etw.EventTra flags |= etw.Process - if config.EnableThreadKevents { + if config.EnableThreadEvents { flags |= etw.Thread } - if config.EnableImageKevents { + if config.EnableImageEvents { flags |= etw.ImageLoad } - if config.EnableNetKevents { + if config.EnableNetEvents { flags |= etw.NetTCPIP } - if config.EnableRegistryKevents { + if config.EnableRegistryEvents { flags |= etw.Registry } - if config.EnableFileIOKevents { + if config.EnableFileIOEvents { flags |= etw.DiskFileIO | etw.FileIO | etw.FileIOInit } - if config.EnableVAMapKevents { + if config.EnableVAMapEvents { flags |= etw.VaMap } - if config.EnableMemKevents { + if config.EnableMemEvents { flags |= etw.VirtualAlloc } - if config.EnableRegistryKevents { + if config.EnableRegistryEvents { flags |= etw.Registry } diff --git a/internal/etw/trace_test.go b/internal/etw/trace_test.go index 6e212212b..23075a2ad 100644 --- a/internal/etw/trace_test.go +++ b/internal/etw/trace_test.go @@ -28,12 +28,12 @@ import ( func TestStartTrace(t *testing.T) { cfg := &config.Config{ - Kstream: config.KstreamConfig{ - EnableThreadKevents: true, - EnableNetKevents: true, - EnableFileIOKevents: true, - BufferSize: 1024, - FlushTimer: time.Millisecond * 2300, + EventSource: config.EventSourceConfig{ + EnableThreadEvents: true, + EnableNetEvents: true, + EnableFileIOEvents: true, + BufferSize: 1024, + FlushTimer: time.Millisecond * 2300, }, } diff --git a/pkg/aggregator/aggregator_test.go b/pkg/aggregator/aggregator_test.go index 37ad1bbb6..b48b1a50b 100644 --- a/pkg/aggregator/aggregator_test.go +++ b/pkg/aggregator/aggregator_test.go @@ -31,10 +31,10 @@ import ( ) func TestNewBufferedAggregator(t *testing.T) { - keventsc := make(chan *event.Event, 20) + eventsc := make(chan *event.Event, 20) errsc := make(chan error, 1) agg, err := NewBuffered( - keventsc, + eventsc, errsc, Config{FlushPeriod: time.Millisecond * 200}, outputs.Config{Type: outputs.Console, Output: console.Config{Format: "pretty"}}, @@ -56,7 +56,7 @@ func TestNewBufferedAggregator(t *testing.T) { params.NetDIP: {Name: params.NetDIP, Type: params.IPv4, Value: net.ParseIP("216.58.201.174")}, }, } - keventsc <- evt + eventsc <- evt } <-time.After(time.Millisecond * 275) assert.Equal(t, int64(4), batchEvents.Value()) @@ -74,7 +74,7 @@ func TestNewBufferedAggregator(t *testing.T) { params.NetDIP: {Name: params.NetDIP, Type: params.IPv4, Value: net.ParseIP("216.58.201.174")}, }, } - keventsc <- evt + eventsc <- evt } <-time.After(time.Millisecond * 260) assert.Equal(t, int64(6), batchEvents.Value()) diff --git a/pkg/cap/reader.go b/pkg/cap/reader.go index 9f52ec153..80673207e 100644 --- a/pkg/cap/reader.go +++ b/pkg/cap/reader.go @@ -40,9 +40,9 @@ var ( // ErrReadSection is thrown when section read errors occur ErrReadSection = func(s section.Type, err error) error { return fmt.Errorf("couldn't read %s section: %v", s, err) } - capReadKevents = expvar.NewInt("cap.read.events") + capReadEvents = expvar.NewInt("cap.read.events") capReadBytes = expvar.NewInt("cap.read.bytes") - capKeventUnmarshalErrors = expvar.NewInt("cap.event.unmarshal.errors") + capEventUnmarshalErrors = expvar.NewInt("cap.event.unmarshal.errors") capHandleUnmarshalErrors = expvar.NewInt("cap.reader.handle.unmarshal.errors") capDroppedByFilter = expvar.NewInt("cap.reader.dropped.by.filter") ) diff --git a/pkg/cap/reader_windows.go b/pkg/cap/reader_windows.go index be00cb3f7..cee14ca9c 100644 --- a/pkg/cap/reader_windows.go +++ b/pkg/cap/reader_windows.go @@ -136,7 +136,7 @@ func (r *reader) Read(ctx context.Context) (chan *event.Event, chan error) { evt, err := event.NewFromCapture(buf, sec.Version()) if err != nil { errsc <- fmt.Errorf("fail to unmarshal event: %v", err) - capKeventUnmarshalErrors.Add(1) + capEventUnmarshalErrors.Add(1) continue } capReadBytes.Add(int64(len(buf))) @@ -173,7 +173,7 @@ func (r *reader) read(evt *event.Event, eventsc chan *event.Event) { return } eventsc <- evt - capReadKevents.Add(1) + capReadEvents.Add(1) } func (r *reader) updateSnapshotters(evt *event.Event) error { @@ -247,11 +247,11 @@ func (r *reader) recoverHandleSnapshotter() (handle.Snapshotter, error) { } var err error - handles[i], err = htypes.NewFromKcap(b) + handles[i], err = htypes.NewFromCapture(b) if err != nil { capHandleUnmarshalErrors.Add(1) } } - r.hsnapshotter = handle.NewFromKcap(handles) + r.hsnapshotter = handle.NewFromCapture(handles) return r.hsnapshotter, nil } diff --git a/pkg/cap/section/section_windows_test.go b/pkg/cap/section/section_windows_test.go index 41fa28def..cec994f67 100644 --- a/pkg/cap/section/section_windows_test.go +++ b/pkg/cap/section/section_windows_test.go @@ -19,15 +19,15 @@ package section import ( - kcapver "github.com/rabbitstack/fibratus/pkg/cap/version" + capver "github.com/rabbitstack/fibratus/pkg/cap/version" "github.com/stretchr/testify/assert" "testing" ) func TestSection(t *testing.T) { - s := New(Process, kcapver.ProcessSecV1, uint32(2456), uint32(30000)) + s := New(Process, capver.ProcessSecV1, uint32(2456), uint32(30000)) assert.Equal(t, Process, s.Type()) - assert.Equal(t, kcapver.ProcessSecV1, s.Version()) + assert.Equal(t, capver.ProcessSecV1, s.Version()) assert.Equal(t, uint32(2456), s.Len()) assert.Equal(t, uint32(30000), s.Size()) } diff --git a/pkg/cap/writer.go b/pkg/cap/writer.go index 9d1ac0844..19fbc2600 100644 --- a/pkg/cap/writer.go +++ b/pkg/cap/writer.go @@ -39,7 +39,7 @@ var ( handleWriteErrors = expvar.NewInt("cap.handle.write.errors") evtWriteErrors = expvar.NewInt("cap.evt.write.errors") flusherErrors = expvar.NewMap("cap.flusher.errors") - overflowKevents = expvar.NewInt("cap.overflow.kevents") + overflowEvents = expvar.NewInt("cap.overflow.events") eventSourceErrors = expvar.NewInt("cap.eventsource.errors") ) diff --git a/pkg/cap/writer_unsupported.go b/pkg/cap/writer_unsupported.go index c6c85cfba..824092c9f 100644 --- a/pkg/cap/writer_unsupported.go +++ b/pkg/cap/writer_unsupported.go @@ -22,12 +22,12 @@ package cap import ( - kerrors "github.com/rabbitstack/fibratus/pkg/errors" + errs "github.com/rabbitstack/fibratus/pkg/errors" "github.com/rabbitstack/fibratus/pkg/handle" "github.com/rabbitstack/fibratus/pkg/ps" ) // NewWriter returns unsupported writer. func NewWriter(filename string, psnap ps.Snapshotter, hsnap handle.Snapshotter) (Writer, error) { - return nil, kerrors.ErrFeatureUnsupported("cap") + return nil, errs.ErrFeatureUnsupported("cap") } diff --git a/pkg/cap/writer_windows.go b/pkg/cap/writer_windows.go index 08845273c..bd381a263 100644 --- a/pkg/cap/writer_windows.go +++ b/pkg/cap/writer_windows.go @@ -41,7 +41,7 @@ import ( ) type stats struct { - kcapFile string + capFile string evtsWritten uint64 bytesWritten uint64 handlesWritten uint64 @@ -67,7 +67,7 @@ func (s *stats) printStats() { t.SetTitle("Capture Statistics") t.SetStyle(table.StyleLight) - t.AppendRow(table.Row{"File", filepath.Base(s.kcapFile)}) + t.AppendRow(table.Row{"File", filepath.Base(s.capFile)}) t.AppendSeparator() t.AppendRow(table.Row{"Events written", atomic.LoadUint64(&s.evtsWritten)}) @@ -75,7 +75,7 @@ func (s *stats) printStats() { t.AppendRow(table.Row{"Processes written", atomic.LoadUint64(&s.procsWritten)}) t.AppendRow(table.Row{"Handles written", atomic.LoadUint64(&s.handlesWritten)}) - f, err := os.Stat(s.kcapFile) + f, err := os.Stat(s.capFile) if err != nil { t.Render() return @@ -142,7 +142,7 @@ func NewWriter(filename string, psnap ps.Snapshotter, hsnap handle.Snapshotter) psnap: psnap, hsnap: hsnap, stop: make(chan struct{}), - stats: &stats{kcapFile: filename}, + stats: &stats{capFile: filename}, } if err := w.writeSnapshots(); err != nil { @@ -220,7 +220,7 @@ func (w *writer) write(b []byte) error { defer w.mu.Unlock() l := len(b) if l > maxKevtSize { - overflowKevents.Add(1) + overflowEvents.Add(1) return fmt.Errorf("event size overflow by %d bytes", l-maxKevtSize) } if err := w.ws(section.Event, capver.EvtSecV2, 0, uint32(l)); err != nil { diff --git a/pkg/cap/writer_windows_test.go b/pkg/cap/writer_windows_test.go index 6a8d16618..4f81d5671 100644 --- a/pkg/cap/writer_windows_test.go +++ b/pkg/cap/writer_windows_test.go @@ -165,15 +165,15 @@ func TestWrite(t *testing.T) { func TestLiveCapture(t *testing.T) { t.SkipNow() cfg := &config.Config{ - Kstream: config.KstreamConfig{ - EnableFileIOKevents: true, - EnableImageKevents: true, - EnableRegistryKevents: true, - EnableNetKevents: true, - EnableThreadKevents: true, - EnableHandleKevents: true, + EventSource: config.EventSourceConfig{ + EnableFileIOEvents: true, + EnableImageEvents: true, + EnableRegistryEvents: true, + EnableNetEvents: true, + EnableThreadEvents: true, + EnableHandleEvents: true, }, - KcapFile: "../../test.cap", + CapFile: "../../test.cap", Filters: &config.Filters{}, InitHandleSnapshot: true, } @@ -193,7 +193,7 @@ func TestLiveCapture(t *testing.T) { } // bootstrap cap writer with inbound event channel - writer, err := NewWriter(cfg.KcapFile, psnap, hsnap) + writer, err := NewWriter(cfg.CapFile, psnap, hsnap) if err != nil { t.Fatal(err) } diff --git a/pkg/config/_fixtures/fibratus.json b/pkg/config/_fixtures/fibratus.json index cba8b29de..d2824b566 100644 --- a/pkg/config/_fixtures/fibratus.json +++ b/pkg/config/_fixtures/fibratus.json @@ -30,15 +30,15 @@ "init-snapshot": true }, - "Event": { + "event": { }, - "kcap": { + "cap": { }, - "kstream": {}, + "eventsource": {}, "logging": {}, "output": { "console": { diff --git a/pkg/config/_fixtures/fibratus.yml b/pkg/config/_fixtures/fibratus.yml index 9c8c9b9ac..86460d2a1 100644 --- a/pkg/config/_fixtures/fibratus.yml +++ b/pkg/config/_fixtures/fibratus.yml @@ -2,10 +2,10 @@ # =============================== Aggregator ========================================== -# Aggregator is responsible for creating kernel event batches, applying transformers to each event +# Aggregator is responsible for creating event batches, applying transformers to each event # present in the batch, and forwarding those batches to the output sinks. aggregator: - # Determines the flush period that triggers the flushing of the kernel event batches to output sinks. + # Determines the flush period that triggers the flushing of the event batches to output sinks. flush-period: 230ms # Represents the max time to wait before announcing failed flushing of enqueued events when fibratus @@ -41,7 +41,7 @@ alertsenders: # Represents the port of the SMTP server. port: 587 - # Specifies the user name when authenticating to the SMTP server. + # Specifies the username when authenticating to the SMTP server. user: bunny # Specifies the password when authenticating to the SMTP server. @@ -116,12 +116,12 @@ handle: # =============================== Kcap ================================================= -kcap: +cap: file: "" # =============================== Event =============================================== -Event: +event: serialize-threads: false serialize-images: false serialize-handles: false @@ -129,7 +129,7 @@ Event: # =============================== Kstream ============================================== -kstream: +eventsource: max-buffers: 2 min-buffers: 1 flush-interval: 1s @@ -209,7 +209,7 @@ transformers: replace: enabled: false replacements: - - Param: key_name + - param: key_name old: HKEY_CURRENT_USER new: HCU tags: @@ -220,10 +220,10 @@ transformers: trim: enabled: false prefixes: - - Param: key_name + - param: key_name trim: CurrentControlSet suffixes: - - Param: file_name + - param: file_name trim: .exe # =============================== YARA ================================================= diff --git a/pkg/config/_fixtures/http-output.yml b/pkg/config/_fixtures/http-output.yml index 898e0db00..50bd3ca88 100644 --- a/pkg/config/_fixtures/http-output.yml +++ b/pkg/config/_fixtures/http-output.yml @@ -1,4 +1,4 @@ -kstream: +eventsource: max-buffers: 10 min-buffers: 8 flush-interval: 1s diff --git a/pkg/config/_fixtures/output.yml b/pkg/config/_fixtures/output.yml index d2db8139e..cf45b7bd9 100644 --- a/pkg/config/_fixtures/output.yml +++ b/pkg/config/_fixtures/output.yml @@ -1,4 +1,4 @@ -kstream: +eventsource: max-buffers: 10 min-buffers: 8 flush-interval: 1s diff --git a/pkg/config/_fixtures/transformers.yml b/pkg/config/_fixtures/transformers.yml index 37a39665b..556fbb53e 100644 --- a/pkg/config/_fixtures/transformers.yml +++ b/pkg/config/_fixtures/transformers.yml @@ -1,4 +1,4 @@ -kstream: +eventsource: max-buffers: 10 min-buffers: 8 flush-interval: 1s diff --git a/pkg/config/config_windows.go b/pkg/config/config_windows.go index 0921bdcc6..3d51cb0c0 100644 --- a/pkg/config/config_windows.go +++ b/pkg/config/config_windows.go @@ -62,7 +62,7 @@ import ( ) const ( - kcapFile = "cap.file" + capFile = "cap.file" configFile = "config-file" debugPrivilege = "debug-privilege" initHandleSnapshot = "handle.init-snapshot" @@ -80,8 +80,8 @@ const ( // Config stores configuration options for fine-tuning the behaviour of Fibratus. type Config struct { - // Kstream stores different configuration options for fine-tuning kstream consumer/controller settings. - Kstream KstreamConfig `json:"kstream" yaml:"kstream"` + // EventSource stores different configuration options for fine-tuning the event source. + EventSource EventSourceConfig `json:"eventsource" yaml:"eventsource"` // Filament contains filament settings Filament FilamentConfig `json:"filament" yaml:"filament"` // PE contains the settings that influences the behaviour of the PE (Portable Executable) reader. @@ -105,8 +105,8 @@ type Config struct { // ForwardMode designates if event forwarding mode is engaged ForwardMode bool `json:"forward" yaml:"forward"` - // KcapFile represents the name of the capture file. - KcapFile string + // CapFile represents the name of the capture file. + CapFile string // API stores global HTTP API preferences API APIConfig `json:"api" yaml:"api"` @@ -201,16 +201,16 @@ func NewWithOpts(options ...Option) *Config { flagSet := new(pflag.FlagSet) c := &Config{ - Kstream: KstreamConfig{}, - Filament: FilamentConfig{}, - API: APIConfig{}, - PE: pe.Config{}, - Log: log.Config{}, - Aggregator: aggregator.Config{}, - Filters: &Filters{}, - viper: v, - flags: flagSet, - opts: opts, + EventSource: EventSourceConfig{}, + Filament: FilamentConfig{}, + API: APIConfig{}, + PE: pe.Config{}, + Log: log.Config{}, + Aggregator: aggregator.Config{}, + Filters: &Filters{}, + viper: v, + flags: flagSet, + opts: opts, } if opts.run || opts.replay { @@ -261,7 +261,7 @@ func (c *Config) MustViperize(cmd *cobra.Command) { panic(err) } if c.opts.capture || c.opts.replay { - if err := cmd.MarkPersistentFlagRequired(kcapFile); err != nil { + if err := cmd.MarkPersistentFlagRequired(capFile); err != nil { panic(err) } } @@ -269,7 +269,7 @@ func (c *Config) MustViperize(cmd *cobra.Command) { // Init setups the configuration state from Viper. func (c *Config) Init() error { - c.Kstream.initFromViper(c.viper) + c.EventSource.initFromViper(c.viper) c.Filament.initFromViper(c.viper) c.API.initFromViper(c.viper) c.PE.InitFromViper(c.viper) @@ -284,7 +284,7 @@ func (c *Config) Init() error { c.SymbolizeKernelAddresses = c.viper.GetBool(symbolizeKernelAddresses) c.DebugPrivilege = c.viper.GetBool(debugPrivilege) c.ForwardMode = c.viper.GetBool(forwardMode) - c.KcapFile = c.viper.GetString(kcapFile) + c.CapFile = c.viper.GetString(capFile) event.SerializeThreads = c.viper.GetBool(serializeThreads) event.SerializeImages = c.viper.GetBool(serializeImages) @@ -308,7 +308,7 @@ func (c *Config) Init() error { // IsCaptureSet determines if the events are stored // in the capture file. -func (c *Config) IsCaptureSet() bool { return c.KcapFile != "" } +func (c *Config) IsCaptureSet() bool { return c.CapFile != "" } // TryLoadFile attempts to load the configuration file from specified path on the file system. func (c *Config) TryLoadFile(file string) error { @@ -383,10 +383,10 @@ func (c *Config) addFlags() { c.flags.Bool(matchAll, true, "Indicates if the match all strategy is enabled for the rule engine. If the match all strategy is enabled, a single event can trigger multiple rules") } if c.opts.capture { - c.flags.StringP(kcapFile, "o", "", "The path of the output cap file") + c.flags.StringP(capFile, "o", "", "The path of the output cap file") } if c.opts.replay { - c.flags.StringP(kcapFile, "k", "", "The path of the input cap file") + c.flags.StringP(capFile, "k", "", "The path of the input cap file") } if c.opts.run || c.opts.replay || c.opts.list || c.opts.validate { c.flags.String(filamentPath, filepath.Join(os.Getenv("PROGRAMFILES"), "fibratus", "filaments"), "Denotes the directory where filaments are located") @@ -402,14 +402,14 @@ func (c *Config) addFlags() { c.flags.String(symbolPaths, "srv*c:\\\\SymCache*https://msdl.microsoft.com/download/symbols", "Designates the path or a series of paths separated by a semicolon that is used to search for symbols files") c.flags.Bool(symbolizeKernelAddresses, false, "Determines if kernel stack addresses are symbolized") - c.flags.Bool(enableThreadKevents, true, "Determines whether thread kernel events are collected by Kernel Logger provider") - c.flags.Bool(enableRegistryKevents, true, "Determines whether registry kernel events are collected by Kernel Logger provider") - c.flags.Bool(enableNetKevents, true, "Determines whether network (TCP/UDP) kernel events are collected by Kernel Logger provider") - c.flags.Bool(enableFileIOKevents, true, "Determines whether disk I/O kernel events are collected by Kernel Logger provider") - c.flags.Bool(enableVAMapKevents, true, "Determines whether VA map/unmap events are collected by Kernel Logger provider") - c.flags.Bool(enableImageKevents, true, "Determines whether file I/O kernel events are collected by Kernel Logger provider") - c.flags.Bool(enableHandleKevents, false, "Determines whether object manager kernel events (handle creation/destruction) are collected by Kernel Logger provider") - c.flags.Bool(enableMemKevents, true, "Determines whether memory manager kernel events are collected by Kernel Logger provider") + c.flags.Bool(enableThreadEvents, true, "Determines whether thread events are collected by Kernel Logger provider") + c.flags.Bool(enableRegistryEvents, true, "Determines whether registry events are collected by Kernel Logger provider") + c.flags.Bool(enableNetEvents, true, "Determines whether network (TCP/UDP) events are collected by Kernel Logger provider") + c.flags.Bool(enableFileIOEvents, true, "Determines whether disk I/O events are collected by Kernel Logger provider") + c.flags.Bool(enableVAMapEvents, true, "Determines whether VA map/unmap events are collected by Kernel Logger provider") + c.flags.Bool(enableImageEvents, true, "Determines whether file I/O events are collected by Kernel Logger provider") + c.flags.Bool(enableHandleEvents, false, "Determines whether object manager events (handle creation/destruction) are collected by Kernel Logger provider") + c.flags.Bool(enableMemEvents, true, "Determines whether memory manager events are collected by Kernel Logger provider") c.flags.Bool(enableAuditAPIEvents, true, "Determines whether kernel audit API calls events are published") c.flags.Bool(enableDNSEvents, true, "Determines whether DNS client events are enabled") c.flags.Bool(enableThreadpoolEvents, true, "Determines whether thread pool events are published") diff --git a/pkg/config/kstream.go b/pkg/config/eventsource.go similarity index 56% rename from pkg/config/kstream.go rename to pkg/config/eventsource.go index e0b09a78f..20fddfde2 100644 --- a/pkg/config/kstream.go +++ b/pkg/config/eventsource.go @@ -32,25 +32,25 @@ import ( ) const ( - enableThreadKevents = "kstream.enable-thread" - enableRegistryKevents = "kstream.enable-registry" - enableNetKevents = "kstream.enable-net" - enableFileIOKevents = "kstream.enable-fileio" - enableVAMapKevents = "kstream.enable-vamap" - enableImageKevents = "kstream.enable-image" - enableHandleKevents = "kstream.enable-handle" - enableMemKevents = "kstream.enable-mem" - enableAuditAPIEvents = "kstream.enable-audit-api" - enableDNSEvents = "kstream.enable-dns" - enableThreadpoolEvents = "kstream.enable-threadpool" - stackEnrichment = "kstream.stack-enrichment" - bufferSize = "kstream.buffer-size" - minBuffers = "kstream.min-buffers" - maxBuffers = "kstream.max-buffers" - flushInterval = "kstream.flush-interval" - - excludedEvents = "kstream.blacklist.events" - excludedImages = "kstream.blacklist.images" + enableThreadEvents = "eventsource.enable-thread" + enableRegistryEvents = "eventsource.enable-registry" + enableNetEvents = "eventsource.enable-net" + enableFileIOEvents = "eventsource.enable-fileio" + enableVAMapEvents = "eventsource.enable-vamap" + enableImageEvents = "eventsource.enable-image" + enableHandleEvents = "eventsource.enable-handle" + enableMemEvents = "eventsource.enable-mem" + enableAuditAPIEvents = "eventsource.enable-audit-api" + enableDNSEvents = "eventsource.enable-dns" + enableThreadpoolEvents = "eventsource.enable-threadpool" + stackEnrichment = "eventsource.stack-enrichment" + bufferSize = "eventsource.buffer-size" + minBuffers = "eventsource.min-buffers" + maxBuffers = "eventsource.max-buffers" + flushInterval = "eventsource.flush-interval" + + excludedEvents = "eventsource.blacklist.events" + excludedImages = "eventsource.blacklist.images" maxBufferSize = uint32(512) ) @@ -61,24 +61,24 @@ var ( defaultFlushInterval = time.Second ) -// KstreamConfig stores different configuration options for fine-tuning kstream consumer/controller settings. -type KstreamConfig struct { - // EnableThreadKevents indicates if thread events are collected by the ETW provider. - EnableThreadKevents bool `json:"enable-thread" yaml:"enable-thread"` - // EnableRegistryKevents indicates if registry events are collected by the ETW provider. - EnableRegistryKevents bool `json:"enable-registry" yaml:"enable-registry"` - // EnableNetKevents determines whether network (TCP/UDP) events are collected by the ETW provider. - EnableNetKevents bool `json:"enable-net" yaml:"enable-net"` - // EnableFileIOKevents indicates if file I/O events are collected by the ETW provider. - EnableFileIOKevents bool `json:"enable-fileio" yaml:"enable-fileio"` - // EnableVAMapKevents indicates if VA map/unmap events are collected by the ETW provider. - EnableVAMapKevents bool `json:"enable-vamap" yaml:"enable-vamap"` - // EnableImageKevents indicates if image events are collected by the ETW provider. - EnableImageKevents bool `json:"enable-image" yaml:"enable-image"` - // EnableHandleKevents indicates whether handle creation/disposal events are enabled. - EnableHandleKevents bool `json:"enable-handle" yaml:"enable-handle"` - // EnableMemKevents indicates whether memory manager events are enabled. - EnableMemKevents bool `json:"enable-memory" yaml:"enable-memory"` +// EventSourceConfig stores different configuration options for fine-tuning the event source. +type EventSourceConfig struct { + // EnableThreadEvents indicates if thread events are collected by the ETW provider. + EnableThreadEvents bool `json:"enable-thread" yaml:"enable-thread"` + // EnableRegistryEvents indicates if registry events are collected by the ETW provider. + EnableRegistryEvents bool `json:"enable-registry" yaml:"enable-registry"` + // EnableNetEvents determines whether network (TCP/UDP) events are collected by the ETW provider. + EnableNetEvents bool `json:"enable-net" yaml:"enable-net"` + // EnableFileIOEvents indicates if file I/O events are collected by the ETW provider. + EnableFileIOEvents bool `json:"enable-fileio" yaml:"enable-fileio"` + // EnableVAMapEvents indicates if VA map/unmap events are collected by the ETW provider. + EnableVAMapEvents bool `json:"enable-vamap" yaml:"enable-vamap"` + // EnableImageEvents indicates if image events are collected by the ETW provider. + EnableImageEvents bool `json:"enable-image" yaml:"enable-image"` + // EnableHandleEvents indicates whether handle creation/disposal events are enabled. + EnableHandleEvents bool `json:"enable-handle" yaml:"enable-handle"` + // EnableMemEvents indicates whether memory manager events are enabled. + EnableMemEvents bool `json:"enable-memory" yaml:"enable-memory"` // EnableAuditAPIEvents indicates if kernel audit API calls events are enabled EnableAuditAPIEvents bool `json:"enable-audit-api" yaml:"enable-audit-api"` // EnableDNSEvents indicates if DNS client events are enabled @@ -97,8 +97,8 @@ type KstreamConfig struct { MaxBuffers uint32 `json:"max-buffers" yaml:"max-buffers"` // FlushTimer specifies how often the trace buffers are forcibly flushed. FlushTimer time.Duration `json:"flush-interval" yaml:"flush-interval"` - // ExcludedKevents are kernel event names that will be dropped from the kernel event stream. - ExcludedKevents []string `json:"blacklist.events" yaml:"blacklist.events"` + // ExcludedEvents are kernel event names that will be dropped from the kernel event stream. + ExcludedEvents []string `json:"blacklist.events" yaml:"blacklist.events"` // ExcludedImages are process image names that will be rejected if they generate a kernel event. ExcludedImages []string `json:"blacklist.images" yaml:"blacklist.images"` @@ -107,15 +107,15 @@ type KstreamConfig struct { excludedImages map[string]bool } -func (c *KstreamConfig) initFromViper(v *viper.Viper) { - c.EnableThreadKevents = v.GetBool(enableThreadKevents) - c.EnableRegistryKevents = v.GetBool(enableRegistryKevents) - c.EnableNetKevents = v.GetBool(enableNetKevents) - c.EnableFileIOKevents = v.GetBool(enableFileIOKevents) - c.EnableVAMapKevents = v.GetBool(enableVAMapKevents) - c.EnableImageKevents = v.GetBool(enableImageKevents) - c.EnableHandleKevents = v.GetBool(enableHandleKevents) - c.EnableMemKevents = v.GetBool(enableMemKevents) +func (c *EventSourceConfig) initFromViper(v *viper.Viper) { + c.EnableThreadEvents = v.GetBool(enableThreadEvents) + c.EnableRegistryEvents = v.GetBool(enableRegistryEvents) + c.EnableNetEvents = v.GetBool(enableNetEvents) + c.EnableFileIOEvents = v.GetBool(enableFileIOEvents) + c.EnableVAMapEvents = v.GetBool(enableVAMapEvents) + c.EnableImageEvents = v.GetBool(enableImageEvents) + c.EnableHandleEvents = v.GetBool(enableHandleEvents) + c.EnableMemEvents = v.GetBool(enableMemEvents) c.EnableAuditAPIEvents = v.GetBool(enableAuditAPIEvents) c.EnableDNSEvents = v.GetBool(enableDNSEvents) c.EnableThreadpoolEvents = v.GetBool(enableThreadpoolEvents) @@ -124,12 +124,12 @@ func (c *KstreamConfig) initFromViper(v *viper.Viper) { c.MinBuffers = uint32(v.GetInt(minBuffers)) c.MaxBuffers = uint32(v.GetInt(maxBuffers)) c.FlushTimer = v.GetDuration(flushInterval) - c.ExcludedKevents = v.GetStringSlice(excludedEvents) + c.ExcludedEvents = v.GetStringSlice(excludedEvents) c.ExcludedImages = v.GetStringSlice(excludedImages) c.excludedImages = make(map[string]bool) - for _, name := range c.ExcludedKevents { + for _, name := range c.ExcludedEvents { if typ := event.NameToType(name); typ != event.UnknownType { c.dropMasks.Set(typ) } @@ -140,9 +140,9 @@ func (c *KstreamConfig) initFromViper(v *viper.Viper) { } // Init is an exported method to allow initializing exclusion maps from external modules. -func (c *KstreamConfig) Init() { +func (c *EventSourceConfig) Init() { c.excludedImages = make(map[string]bool) - for _, name := range c.ExcludedKevents { + for _, name := range c.ExcludedEvents { for _, typ := range event.NameToTypes(name) { if typ != event.UnknownType { c.dropMasks.Set(typ) @@ -157,26 +157,26 @@ func (c *KstreamConfig) Init() { // SetDropMask inserts the event mask in the bitset to // instruct the given event type should be dropped from // the event stream. -func (c *KstreamConfig) SetDropMask(Type event.Type) { +func (c *EventSourceConfig) SetDropMask(Type event.Type) { c.dropMasks.Set(Type) } // TestDropMask checks if the specified event type has // the drop mask in the bitset. -func (c *KstreamConfig) TestDropMask(Type event.Type) bool { +func (c *EventSourceConfig) TestDropMask(Type event.Type) bool { return c.dropMasks.Test(Type.GUID(), Type.HookID()) } -// ExcludeKevent determines whether the supplied provider GUID +// ExcludeEvent determines whether the supplied provider GUID // and the hook identifier are in the bitset of excluded events. -func (c *KstreamConfig) ExcludeKevent(guid windows.GUID, hookID uint16) bool { +func (c *EventSourceConfig) ExcludeEvent(guid windows.GUID, hookID uint16) bool { return c.dropMasks.Test(guid, hookID) } // ExcludeImage determines whether the process generating event is present in the // list of excluded images. If the hit occurs, the event associated with the process // is dropped. -func (c *KstreamConfig) ExcludeImage(ps *pstypes.PS) bool { +func (c *EventSourceConfig) ExcludeImage(ps *pstypes.PS) bool { if len(c.excludedImages) == 0 { return false } diff --git a/pkg/config/kstream_test.go b/pkg/config/eventsource_test.go similarity index 53% rename from pkg/config/kstream_test.go rename to pkg/config/eventsource_test.go index 7a3d6f258..cd43354fb 100644 --- a/pkg/config/kstream_test.go +++ b/pkg/config/eventsource_test.go @@ -31,17 +31,17 @@ import ( "github.com/stretchr/testify/require" ) -func TestKstreamConfig(t *testing.T) { +func TestEventSourceConfig(t *testing.T) { c := NewWithOpts(WithRun()) err := c.flags.Parse([]string{ - "--kstream.enable-thread=false", - "--kstream.enable-registry=false", - "--kstream.enable-fileio=false", - "--kstream.enable-net=false", - "--kstream.enable-image=false", - "--kstream.blacklist.events=CloseFile,CloseHandle", - "--kstream.blacklist.images=System,svchost.exe", + "--eventsource.enable-thread=false", + "--eventsource.enable-registry=false", + "--eventsource.enable-fileio=false", + "--eventsource.enable-net=false", + "--eventsource.enable-image=false", + "--eventsource.blacklist.events=CloseFile,CloseHandle", + "--eventsource.blacklist.images=System,svchost.exe", }) require.NoError(t, err) require.NoError(t, c.viper.BindPFlags(c.flags)) @@ -49,15 +49,15 @@ func TestKstreamConfig(t *testing.T) { require.NoError(t, c.Init()) - assert.False(t, c.Kstream.EnableThreadKevents) - assert.False(t, c.Kstream.EnableNetKevents) - assert.False(t, c.Kstream.EnableRegistryKevents) - assert.False(t, c.Kstream.EnableImageKevents) - assert.False(t, c.Kstream.EnableFileIOKevents) + assert.False(t, c.EventSource.EnableThreadEvents) + assert.False(t, c.EventSource.EnableNetEvents) + assert.False(t, c.EventSource.EnableRegistryEvents) + assert.False(t, c.EventSource.EnableImageEvents) + assert.False(t, c.EventSource.EnableFileIOEvents) - assert.True(t, c.Kstream.ExcludeKevent(event.CloseHandle.GUID(), event.CloseHandle.HookID())) - assert.False(t, c.Kstream.ExcludeKevent(event.CreateProcess.GUID(), event.CreateProcess.HookID())) + assert.True(t, c.EventSource.ExcludeEvent(event.CloseHandle.GUID(), event.CloseHandle.HookID())) + assert.False(t, c.EventSource.ExcludeEvent(event.CreateProcess.GUID(), event.CreateProcess.HookID())) - assert.True(t, c.Kstream.ExcludeImage(&pstypes.PS{Name: "svchost.exe"})) - assert.False(t, c.Kstream.ExcludeImage(&pstypes.PS{Name: "explorer.exe"})) + assert.True(t, c.EventSource.ExcludeImage(&pstypes.PS{Name: "svchost.exe"})) + assert.False(t, c.EventSource.ExcludeImage(&pstypes.PS{Name: "explorer.exe"})) } diff --git a/pkg/config/print_test.go b/pkg/config/print_test.go index 4e5f9b4ec..e5cf70478 100644 --- a/pkg/config/print_test.go +++ b/pkg/config/print_test.go @@ -25,7 +25,7 @@ import ( func TestConfigPrint(t *testing.T) { c := NewWithOpts(WithRun()) - err := c.flags.Parse([]string{"--kstream.enable-thread=false", "--config-file=_fixtures/fibratus.yml"}) + err := c.flags.Parse([]string{"--eventsource.enable-thread=false", "--config-file=_fixtures/fibratus.yml"}) require.NoError(t, c.viper.BindPFlags(c.flags)) require.NoError(t, err) require.NoError(t, c.TryLoadFile(c.GetConfigFile())) diff --git a/pkg/config/schema_windows.go b/pkg/config/schema_windows.go index 631e76665..69f0b2b0a 100644 --- a/pkg/config/schema_windows.go +++ b/pkg/config/schema_windows.go @@ -177,7 +177,7 @@ var schema = ` }, "additionalProperties": false }, - "kstream": { + "eventsource": { "type": "object", "properties": { "enable-thread": {"type": "boolean"}, @@ -195,7 +195,7 @@ var schema = ` "min-buffers": {"type": "integer", "minimum": 1, "maximum": {{ .MinBuffers }}}, "max-buffers": {"type": "integer", "minimum": 2, "maximum": {{ .MaxBuffers }}}, "buffer-size": {"type": "integer", "maximum": {{ .MaxBufferSize }}}, - "flush-interval": {"type": "string", "minLength": 2, "pattern": "[0-9]+s"}, + "flush-interval": {"type": "string", "minLength": 2, "pattern": "[0-9]+s"}, "blacklist": { "type": "object", "properties": { @@ -380,7 +380,7 @@ var schema = ` { "type": "object", "properties": { - "Param": {"type": "string", "minLength": 1}, + "param": {"type": "string", "minLength": 1}, "old": {"type": "string", "minLength": 1}, "new": {"type": "string"} }, @@ -427,7 +427,7 @@ var schema = ` { "type": "object", "properties": { - "Param": {"type": "string", "minLength": 1}, + "param": {"type": "string", "minLength": 1}, "trim": {"type": "string", "minLength": 1} }, "additionalProperties": false @@ -437,7 +437,7 @@ var schema = ` { "type": "object", "properties": { - "Param": {"type": "string", "minLength": 1}, + "param": {"type": "string", "minLength": 1}, "trim": {"type": "string", "minLength": 1} }, "additionalProperties": false diff --git a/pkg/event/formatter.go b/pkg/event/formatter.go index 3acd05e6e..620ec1f3f 100644 --- a/pkg/event/formatter.go +++ b/pkg/event/formatter.go @@ -64,8 +64,8 @@ var ( // tmplNormRegepx defines the regular expression for normalizing the template. This basically consists in removing // the brackets and trailing/leading spaces from the field name. tmplNormRegexp = regexp.MustCompile(`({{2}\s*([A-Za-z.]+)\s*}{2})`) - // tmplExpandKparamsRegexp determines whether Params. fields are expanded - tmplExpandKparamsRegexp = regexp.MustCompile(`{{\s*.Params.\S+}}`) + // tmplExpandParamsRegexp determines whether Params. fields are expanded + tmplExpandParamsRegexp = regexp.MustCompile(`{{\s*.Params.\S+}}`) ) var fields = map[string]bool{ @@ -104,8 +104,8 @@ func hintFields() string { // Formatter deals with producing event's output that is dictated by the template. type Formatter struct { - t *fasttemplate.Template - expandKparamsDot bool + t *fasttemplate.Template + expandParamsDot bool } // NewFormatter builds a new instance of event's formatter. @@ -142,8 +142,8 @@ func NewFormatter(template string) (*Formatter, error) { return nil, fmt.Errorf("invalid template format %q: %v", norm, err) } return &Formatter{ - t: t, - expandKparamsDot: tmplExpandKparamsRegexp.MatchString(norm), + t: t, + expandParamsDot: tmplExpandParamsRegexp.MatchString(norm), }, nil } diff --git a/pkg/event/formatter_windows.go b/pkg/event/formatter_windows.go index 7369c0f3e..c441ae650 100644 --- a/pkg/event/formatter_windows.go +++ b/pkg/event/formatter_windows.go @@ -22,7 +22,7 @@ import ( "strconv" ) -// Format applies the template on the provided kernel event. +// Format applies the template on the provided event. func (f *Formatter) Format(evt *Event) []byte { if evt == nil { return []byte{} @@ -65,7 +65,7 @@ func (f *Formatter) Format(evt *Event) []byte { values[cstack] = evt.Callstack.String() } - if f.expandKparamsDot { + if f.expandParamsDot { // expand all parameters into the map, so we can ask // for specific parameter names in the template for _, par := range evt.Params { diff --git a/pkg/event/marshaller_test.go b/pkg/event/marshaller_test.go index 3e4773906..13f4870e3 100644 --- a/pkg/event/marshaller_test.go +++ b/pkg/event/marshaller_test.go @@ -105,7 +105,7 @@ func TestMarshaller(t *testing.T) { assert.Equal(t, "barzz", clone.Metadata["fooz"]) } -func TestKeventMarshalJSON(t *testing.T) { +func TestEventMarshalJSON(t *testing.T) { evt := &Event{ Type: CreateFile, Tid: 2484, @@ -282,7 +282,7 @@ func TestUnmarshalHugeHandles(t *testing.T) { require.NotNil(t, clone) } -func TestKeventMarshalJSONMultiple(t *testing.T) { +func TestEventMarshalJSONMultiple(t *testing.T) { for i := 0; i < 10; i++ { seq := uint64(i + 1) evt := &Event{ @@ -364,7 +364,7 @@ func TestKeventMarshalJSONMultiple(t *testing.T) { } } -func BenchmarkKeventMarshalJSON(b *testing.B) { +func BenchmarkEventMarshalJSON(b *testing.B) { evt := &Event{ Type: CreateFile, Tid: 2484, @@ -448,7 +448,7 @@ func BenchmarkKeventMarshalJSON(b *testing.B) { } } -func BenchmarkKeventMarshalJSONStdlib(b *testing.B) { +func BenchmarkEventMarshalJSONStdlib(b *testing.B) { evt := &Event{ Type: CreateFile, Tid: 2484, diff --git a/pkg/event/marshaller_windows.go b/pkg/event/marshaller_windows.go index 5566f9329..80405ff8b 100644 --- a/pkg/event/marshaller_windows.go +++ b/pkg/event/marshaller_windows.go @@ -410,7 +410,7 @@ func (e *Event) UnmarshalRaw(b []byte, ver capver.Version) error { // read process state sec := section.Read(b[inc(idx, 14)+offset:]) if sec.Size() != 0 { - ps, err := ptypes.NewFromKcap(b[inc(idx, 24)+offset:], sec) + ps, err := ptypes.NewFromCapture(b[inc(idx, 24)+offset:], sec) if err != nil { return err } diff --git a/pkg/event/queue.go b/pkg/event/queue.go index cf185f4ee..31801ab5c 100644 --- a/pkg/event/queue.go +++ b/pkg/event/queue.go @@ -29,8 +29,8 @@ import ( // removed from the cache. const backlogCacheSize = 800 -// keventsEnqueued counts the number of events that are pushed to the queue -var keventsEnqueued = expvar.NewInt("kstream.kevents.enqueued") +// eventsEnqueued counts the number of events that are pushed to the queue +var eventsEnqueued = expvar.NewInt("eventsource.events.enqueued") // Listener is the minimal interface that all event listeners need to implement. type Listener interface { @@ -163,7 +163,7 @@ func (q *Queue) push(e *Event) error { } if enqueue || len(q.listeners) == 0 { q.q <- e - keventsEnqueued.Add(1) + eventsEnqueued.Add(1) } return nil } diff --git a/pkg/event/types_windows.go b/pkg/event/types_windows.go index db44c512e..8a2c67bf3 100644 --- a/pkg/event/types_windows.go +++ b/pkg/event/types_windows.go @@ -29,12 +29,12 @@ import ( // Remember to increment if a new event source is introduced. const ProvidersCount = 12 -// EventSource is the type that designates the provenance of the event -type EventSource uint8 +// Source is the type that designates the provenance of the event +type Source uint8 const ( // SystemLogger event is emitted by the system provider - SystemLogger EventSource = iota + SystemLogger Source = iota // AuditAPICallsLogger event is emitted by Audit API calls provider AuditAPICallsLogger // DNSLogger event is emitted by DNS provider @@ -576,7 +576,7 @@ func (t *Type) HookID() uint16 { } // Source designates the provenance of this event type. -func (t Type) Source() EventSource { +func (t Type) Source() Source { switch t { case OpenProcess, OpenThread, SetThreadContext, CreateSymbolicLinkObject: return AuditAPICallsLogger diff --git a/pkg/filament/_fixtures/test_filter.py b/pkg/filament/_fixtures/test_filter.py index 2211180a9..c6e346b83 100644 --- a/pkg/filament/_fixtures/test_filter.py +++ b/pkg/filament/_fixtures/test_filter.py @@ -18,10 +18,10 @@ Tests the filter expression. """ -kevents = [] +events = [] def on_init(): - kfilter('ps.name in (%s)' % ','.join(["'svchost.exe'", "'cmd.exe'", "'mimikatz.exe'"])) + set_filter('ps.name in (%s)' % ','.join(["'svchost.exe'", "'cmd.exe'", "'mimikatz.exe'"])) -def on_next_kevent(Event): - pass \ No newline at end of file +def on_next_event(event): + pass diff --git a/pkg/filament/_fixtures/test_on_next_kevent.py b/pkg/filament/_fixtures/test_on_next_event.py similarity index 77% rename from pkg/filament/_fixtures/test_on_next_kevent.py rename to pkg/filament/_fixtures/test_on_next_event.py index 080ae9132..3c326b070 100644 --- a/pkg/filament/_fixtures/test_on_next_kevent.py +++ b/pkg/filament/_fixtures/test_on_next_event.py @@ -15,20 +15,20 @@ # under the License. """ -Tests the on_next_kevent function. +Tests the on_next_event function. """ -kevents = [] +events = [] def on_init(): interval(1) columns(['Key', '#Seq']) sort_by('#Seq') -def on_next_kevent(Event): - kevents.append({'key_name': Event['params']['key_name'], 'seq': Event['seq'], 'dip': Event['params']['dip']}) +def on_next_event(event): + events.append({'key_name': event['params']['key_name'], 'seq': event['seq'], 'dip': event['params']['dip']}) def on_interval(): - for key in kevents: + for key in events: add_row([key['key_name'], key['seq']]) - render_table() \ No newline at end of file + render_table() diff --git a/pkg/filament/_fixtures/top_hives_io.py b/pkg/filament/_fixtures/top_hives_io.py index 7855e5922..7a12b4417 100644 --- a/pkg/filament/_fixtures/top_hives_io.py +++ b/pkg/filament/_fixtures/top_hives_io.py @@ -29,6 +29,6 @@ def on_init(): sort_by("1") -def on_next_kevent(Event): +def on_next_event(event): pass diff --git a/pkg/filament/_fixtures/top_keys_io_table.py b/pkg/filament/_fixtures/top_keys_io_table.py index ae039b73f..d32ba1f08 100644 --- a/pkg/filament/_fixtures/top_keys_io_table.py +++ b/pkg/filament/_fixtures/top_keys_io_table.py @@ -29,7 +29,7 @@ def on_init(): sort_by('#Ops') -def on_next_kevent(Event): +def on_next_event(event): pass diff --git a/pkg/filament/cpython/_fixtures/top_hives_io.py b/pkg/filament/cpython/_fixtures/top_hives_io.py index 5621a4cb1..2a133e542 100644 --- a/pkg/filament/cpython/_fixtures/top_hives_io.py +++ b/pkg/filament/cpython/_fixtures/top_hives_io.py @@ -32,8 +32,8 @@ def on_init(): #set_interval(1) -def on_next_kevent(Event): - print("Event\n", Event["params"]) +def on_next_event(event): + print("Event\n", event["params"]) #raise Exception('eggs', 'eggs') def on_interval(): diff --git a/pkg/filament/dict_test.go b/pkg/filament/dict_test.go index 069f72c26..7d714cf25 100644 --- a/pkg/filament/dict_test.go +++ b/pkg/filament/dict_test.go @@ -95,11 +95,11 @@ func TestProduceEventDictWithIPAddresses(t *testing.T) { require.NoError(t, err) require.NotNil(t, dict) - kpars := dict.Get(cpython.PyUnicodeFromString("params")) - kparamsDict := cpython.NewDictFromObject(kpars) + pars := dict.Get(cpython.PyUnicodeFromString("params")) + paramsDict := cpython.NewDictFromObject(pars) - assert.Equal(t, "216.58.201.174", kparamsDict.Get(cpython.PyUnicodeFromString("dip")).String()) - assert.Equal(t, "2001:db8:85a3::8a2e:370:7334", kparamsDict.Get(cpython.PyUnicodeFromString("sip")).String()) + assert.Equal(t, "216.58.201.174", paramsDict.Get(cpython.PyUnicodeFromString("dip")).String()) + assert.Equal(t, "2001:db8:85a3::8a2e:370:7334", paramsDict.Get(cpython.PyUnicodeFromString("sip")).String()) } func BenchmarkTestProduceEventDict(b *testing.B) { diff --git a/pkg/filament/filament.go b/pkg/filament/filament.go index 2a2c5f7a8..4490d8ad2 100644 --- a/pkg/filament/filament.go +++ b/pkg/filament/filament.go @@ -74,10 +74,10 @@ const ( ) var ( - keventErrors = expvar.NewMap("filament.event.errors") - keventProcessErrors = expvar.NewInt("filament.event.process.errors") - kdictErrors = expvar.NewInt("filament.kdict.errors") - batchFlushes = expvar.NewInt("filament.event.batch.flushes") + eventErrors = expvar.NewMap("filament.event.errors") + eventProcessErrors = expvar.NewInt("filament.event.process.errors") + dictErrors = expvar.NewInt("filament.dict.errors") + batchFlushes = expvar.NewInt("filament.event.batch.flushes") errFilamentsDir = func(path string) error { return fmt.Errorf("%s does not exist or is not a directory", path) } @@ -90,17 +90,17 @@ var ( tableOutput io.Writer ) -type kbatch []*event.Event +type batch []*event.Event -func (k *kbatch) append(evt *event.Event) { - if *k == nil { - *k = make([]*event.Event, 0) +func (b *batch) append(evt *event.Event) { + if *b == nil { + *b = make([]*event.Event, 0) } - *k = append(*k, evt) + *b = append(*b, evt) } -func (k *kbatch) reset() { *k = nil } -func (k kbatch) len() int { return len(k) } +func (b *batch) reset() { *b = nil } +func (b batch) len() int { return len(b) } type filament struct { name string @@ -123,8 +123,8 @@ type filament struct { initErrors []error - onNextKevent *cpython.PyObject - onStop *cpython.PyObject + onNextEvent *cpython.PyObject + onStop *cpython.PyObject table tab } @@ -228,19 +228,19 @@ func New( } f := &filament{ - name: name, - mod: mod, - config: config, - psnap: psnap, - hsnap: hsnap, - close: make(chan struct{}, 1), - fnerrs: make(chan error, 100), - gil: cpython.NewGIL(), - columns: make([]string, 0), - onNextKevent: onNextEvent, - interval: time.Second, - initErrors: make([]error, 0), - table: newTable(), + name: name, + mod: mod, + config: config, + psnap: psnap, + hsnap: hsnap, + close: make(chan struct{}, 1), + fnerrs: make(chan error, 100), + gil: cpython.NewGIL(), + columns: make([]string, 0), + onNextEvent: onNextEvent, + interval: time.Second, + initErrors: make([]error, 0), + table: newTable(), } if mod.HasAttr(onStopFn) { @@ -368,8 +368,8 @@ func New( return f, nil } -func (f *filament) Run(kevents <-chan *event.Event, errs <-chan error) error { - var batch kbatch +func (f *filament) Run(eventsc <-chan *event.Event, errs <-chan error) error { + var b batch var flusher = time.NewTicker(time.Second) for { select { @@ -380,19 +380,19 @@ func (f *filament) Run(kevents <-chan *event.Event, errs <-chan error) error { } select { - case evt := <-kevents: - batch.append(evt) + case evt := <-eventsc: + b.append(evt) case err := <-errs: - keventErrors.Add(err.Error(), 1) + eventErrors.Add(err.Error(), 1) case <-flusher.C: batchFlushes.Add(1) - if batch.len() > 0 { - err := f.pushEvents(batch) + if b.len() > 0 { + err := f.pushEvents(b) if err != nil { - log.Warnf("on_next_kevent failed: %v", err) - keventProcessErrors.Add(1) + log.Warnf("on_next_event failed: %v", err) + eventProcessErrors.Add(1) } - batch.reset() + b.reset() } case err := <-f.fnerrs: return err @@ -403,17 +403,17 @@ func (f *filament) Run(kevents <-chan *event.Event, errs <-chan error) error { } } -func (f *filament) pushEvents(b kbatch) error { +func (f *filament) pushEvents(b batch) error { f.gil.Lock() defer f.gil.Unlock() for _, evt := range b { dict, err := newEventDict(evt) if err != nil { dict.DecRef() - kdictErrors.Add(1) + dictErrors.Add(1) continue } - r := f.onNextKevent.Call(dict.Object()) + r := f.onNextEvent.Call(dict.Object()) if r != nil { r.DecRef() } diff --git a/pkg/filament/filament_test.go b/pkg/filament/filament_test.go index 862238fd6..0cb394fc8 100644 --- a/pkg/filament/filament_test.go +++ b/pkg/filament/filament_test.go @@ -53,18 +53,18 @@ func init() { tableOutput = &buf } -func TestOnNextKevent(t *testing.T) { +func TestOnNextEvent(t *testing.T) { // this test crashes in the CI. Reenable once // we investigate why this happens t.SkipNow() - filament, err := New("test_on_next_kevent", nil, nil, &config.Config{Filament: config.FilamentConfig{FlushPeriod: time.Millisecond * 250, Path: "_fixtures"}}) + filament, err := New("test_on_next_event", nil, nil, &config.Config{Filament: config.FilamentConfig{FlushPeriod: time.Millisecond * 250, Path: "_fixtures"}}) require.NoError(t, err) require.NotNil(t, filament) time.AfterFunc(time.Millisecond*1050, func() { filament.Close() }) - kevents := make(chan *event.Event, 100) + events := make(chan *event.Event, 100) errs := make(chan error, 10) for i := 1; i <= 100; i++ { evt := &event.Event{ @@ -83,9 +83,9 @@ func TestOnNextKevent(t *testing.T) { params.NetDIP: {Name: params.NetDIP, Type: params.IPv4, Value: net.ParseIP("216.58.201.174")}, }, } - kevents <- evt + events <- evt } - err = filament.Run(kevents, errs) + err = filament.Run(events, errs) require.Nil(t, err) sn := bufio.NewScanner(strings.NewReader(buf.String())) const headerOffset = 4 diff --git a/pkg/filter/filter_test.go b/pkg/filter/filter_test.go index aefce2002..956324da9 100644 --- a/pkg/filter/filter_test.go +++ b/pkg/filter/filter_test.go @@ -45,14 +45,14 @@ import ( ) var cfg = &config.Config{ - Kstream: config.KstreamConfig{ - EnableHandleKevents: true, - EnableNetKevents: true, - EnableRegistryKevents: true, - EnableFileIOKevents: true, - EnableImageKevents: true, - EnableThreadKevents: true, - EnableMemKevents: true, + EventSource: config.EventSourceConfig{ + EnableHandleEvents: true, + EnableNetEvents: true, + EnableRegistryEvents: true, + EnableFileIOEvents: true, + EnableImageEvents: true, + EnableThreadEvents: true, + EnableMemEvents: true, EnableDNSEvents: true, EnableThreadpoolEvents: true, }, @@ -690,7 +690,7 @@ func TestFileInfoFilter(t *testing.T) { } } -func TestKeventFilter(t *testing.T) { +func TestEventFilter(t *testing.T) { evt := &event.Event{ Type: event.CreateFile, Tid: 2484, diff --git a/pkg/filter/filter_windows.go b/pkg/filter/filter_windows.go index a38b8b06d..007d14b76 100644 --- a/pkg/filter/filter_windows.go +++ b/pkg/filter/filter_windows.go @@ -57,34 +57,34 @@ func New(expr string, config *config.Config, options ...Option) Filter { // PE metadata newPEAccessor(), } - kconfig := config.Kstream + fconfig := config.Filters - if kconfig.EnableThreadKevents { + if config.EventSource.EnableThreadEvents { accessors = append(accessors, newThreadAccessor()) } - if kconfig.EnableImageKevents { + if config.EventSource.EnableImageEvents { accessors = append(accessors, newImageAccessor()) } - if kconfig.EnableFileIOKevents { + if config.EventSource.EnableFileIOEvents { accessors = append(accessors, newFileAccessor()) } - if kconfig.EnableRegistryKevents { + if config.EventSource.EnableRegistryEvents { accessors = append(accessors, newRegistryAccessor()) } - if kconfig.EnableNetKevents { + if config.EventSource.EnableNetEvents { accessors = append(accessors, newNetworkAccessor()) } - if kconfig.EnableHandleKevents { + if config.EventSource.EnableHandleEvents { accessors = append(accessors, newHandleAccessor()) } - if kconfig.EnableMemKevents { + if config.EventSource.EnableMemEvents { accessors = append(accessors, newMemAccessor()) } - if kconfig.EnableDNSEvents { + if config.EventSource.EnableDNSEvents { accessors = append(accessors, newDNSAccessor()) } - if kconfig.EnableThreadpoolEvents { + if config.EventSource.EnableThreadpoolEvents { accessors = append(accessors, newThreadpoolAccessor()) } diff --git a/pkg/filter/ql/functions/yara_unsupported.go b/pkg/filter/ql/functions/yara_unsupported.go index 66f310e74..6f80b7f36 100644 --- a/pkg/filter/ql/functions/yara_unsupported.go +++ b/pkg/filter/ql/functions/yara_unsupported.go @@ -23,7 +23,7 @@ package functions import ( "fmt" - kerrors "github.com/rabbitstack/fibratus/pkg/errors" + errs "github.com/rabbitstack/fibratus/pkg/errors" ) // Yara unsupported function @@ -40,7 +40,7 @@ func (f Yara) Desc() FunctionDesc { {Keyword: "vars", Types: []ArgType{Field, BoundField, Func, String}}, }, ArgsValidationFunc: func(args []string) error { - return fmt.Errorf("yara function is not supported. %w", kerrors.ErrFeatureUnsupported("yara")) + return fmt.Errorf("yara function is not supported. %w", errs.ErrFeatureUnsupported("yara")) }, } return desc diff --git a/pkg/handle/snapshotter.go b/pkg/handle/snapshotter.go index 172905d3c..dfcaebb2b 100644 --- a/pkg/handle/snapshotter.go +++ b/pkg/handle/snapshotter.go @@ -127,8 +127,8 @@ func NewSnapshotter(config *config.Config, fn SnapshotBuildCompleted) Snapshotte return s } -// NewFromKcap builds the handle snapshotter from cap state. -func NewFromKcap(handles []htypes.Handle) Snapshotter { +// NewFromCapture builds the handle snapshotter from the capture state. +func NewFromCapture(handles []htypes.Handle) Snapshotter { s := &snapshotter{ handlesByObject: make(map[uint64]htypes.Handle), capture: true, diff --git a/pkg/handle/types/types.go b/pkg/handle/types/types.go index 2d98349f2..e280f5a8c 100644 --- a/pkg/handle/types/types.go +++ b/pkg/handle/types/types.go @@ -87,8 +87,8 @@ func (h Handle) Len() int { return l } -// NewFromKcap restores handle state from the cap buffer. -func NewFromKcap(buf []byte) (Handle, error) { +// NewFromCapture restores handle state from the capture buffer. +func NewFromCapture(buf []byte) (Handle, error) { h := Handle{} err := h.Unmarshal(buf) if err != nil { diff --git a/pkg/outputs/amqp/amqp_test.go b/pkg/outputs/amqp/amqp_test.go index 6181b95ab..9a1539635 100644 --- a/pkg/outputs/amqp/amqp_test.go +++ b/pkg/outputs/amqp/amqp_test.go @@ -148,13 +148,13 @@ func consumeEvents(t *testing.T, amqpURI string, done chan struct{}) error { return err } deliveries, err := channel.Consume( - queue.Name, // name - "kevents-consumer", // consumerTag, - false, // noAck - false, // exclusive - false, // noLocal - false, // noWait - nil, // arguments + queue.Name, // name + "events-consumer", // consumerTag, + false, // noAck + false, // exclusive + false, // noLocal + false, // noWait + nil, // arguments ) require.NoError(t, err) @@ -165,15 +165,15 @@ func consumeEvents(t *testing.T, amqpURI string, done chan struct{}) error { done <- struct{}{} t.Error("got empty AMQP message") } - var kevents []*event.Event - err := json.Unmarshal(body, &kevents) + var events []*event.Event + err := json.Unmarshal(body, &events) if err != nil { done <- struct{}{} t.Error(err) } - if len(kevents) != 3 { + if len(events) != 3 { done <- struct{}{} - t.Errorf("expected 3 events in body but got %d", len(kevents)) + t.Errorf("expected 3 events in body but got %d", len(events)) } err = d.Ack(false) if err != nil { diff --git a/pkg/outputs/http/http_test.go b/pkg/outputs/http/http_test.go index 82c113ef7..c3995b560 100644 --- a/pkg/outputs/http/http_test.go +++ b/pkg/outputs/http/http_test.go @@ -54,12 +54,12 @@ func TestHttpPublish(t *testing.T) { http.Error(w, err.Error(), http.StatusInternalServerError) return } - var kevents []*event.Event - if err := json.Unmarshal(body, &kevents); err != nil { + var events []*event.Event + if err := json.Unmarshal(body, &events); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - assert.Equal(t, 3, len(kevents)) + assert.Equal(t, 3, len(events)) assert.Equal(t, "aaabbbaaa", r.Header.Get("API-Key")) assert.Equal(t, "fibratus/", r.Header.Get("User-Agent")) assert.Equal(t, "1.1", r.Header.Get("Version")) @@ -114,12 +114,12 @@ func TestHttpGzipPublish(t *testing.T) { http.Error(w, err.Error(), http.StatusInternalServerError) return } - var kevents []*event.Event - if err := json.Unmarshal(body, &kevents); err != nil { + var events []*event.Event + if err := json.Unmarshal(body, &events); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - assert.Equal(t, 3, len(kevents)) + assert.Equal(t, 3, len(events)) w.WriteHeader(http.StatusOK) }) diff --git a/pkg/pe/marshaller.go b/pkg/pe/marshaller.go index c4944afac..38ed57a06 100644 --- a/pkg/pe/marshaller.go +++ b/pkg/pe/marshaller.go @@ -282,8 +282,8 @@ func (pe *PE) Unmarshal(b []byte, ver capver.Version) error { return nil } -// NewFromKcap restores the PE metadata from the byte stream. -func NewFromKcap(b []byte, ver capver.Version) (*PE, error) { +// NewFromCapture restores the PE metadata from the byte stream. +func NewFromCapture(b []byte, ver capver.Version) (*PE, error) { pe := &PE{ Sections: make([]Sec, 0), Symbols: make([]string, 0), diff --git a/pkg/pe/marshaller_test.go b/pkg/pe/marshaller_test.go index 07ac3f8a0..ce4bf94a3 100644 --- a/pkg/pe/marshaller_test.go +++ b/pkg/pe/marshaller_test.go @@ -22,7 +22,7 @@ package pe import ( - kcapver "github.com/rabbitstack/fibratus/pkg/cap/version" + capver "github.com/rabbitstack/fibratus/pkg/cap/version" "github.com/rabbitstack/fibratus/pkg/sys" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -64,7 +64,7 @@ func TestPEMarshal(t *testing.T) { b := pe.Marshal() newPE := &PE{VersionResources: make(map[string]string)} - err := newPE.Unmarshal(b, kcapver.PESecV2) + err := newPE.Unmarshal(b, capver.PESecV2) require.NoError(t, err) assert.Equal(t, uint16(7), newPE.NumberOfSections) diff --git a/pkg/ps/types/marshaller_windows.go b/pkg/ps/types/marshaller_windows.go index 613107525..97a1d22c9 100644 --- a/pkg/ps/types/marshaller_windows.go +++ b/pkg/ps/types/marshaller_windows.go @@ -212,7 +212,7 @@ func (ps *PS) Unmarshal(b []byte, psec section.Section) error { // read handle length l := uint32(bytes.ReadUint16(b[idx+offset+hoffset:])) off := idx + 2 + hoffset + offset - handle, err := htypes.NewFromKcap(b[off : off+l]) + handle, err := htypes.NewFromCapture(b[off : off+l]) if err != nil { return err } @@ -272,7 +272,7 @@ readpe: } var err error - ps.PE, err = pe.NewFromKcap(b[idx+offset:], sec.Version()) + ps.PE, err = pe.NewFromCapture(b[idx+offset:], sec.Version()) if err != nil { return err } diff --git a/pkg/ps/types/marshaller_windows_test.go b/pkg/ps/types/marshaller_windows_test.go index 83f279ebd..c4f26822d 100644 --- a/pkg/ps/types/marshaller_windows_test.go +++ b/pkg/ps/types/marshaller_windows_test.go @@ -23,7 +23,7 @@ import ( "golang.org/x/sys/windows" "github.com/rabbitstack/fibratus/pkg/cap/section" - kcapver "github.com/rabbitstack/fibratus/pkg/cap/version" + capver "github.com/rabbitstack/fibratus/pkg/cap/version" "github.com/rabbitstack/fibratus/pkg/pe" "github.com/stretchr/testify/assert" @@ -83,8 +83,8 @@ func TestPSMarshaler(t *testing.T) { } b := ps.Marshal() - sec := section.New(section.Process, kcapver.ProcessSecV4, 0, 0) - clone, err := NewFromKcap(b, sec) + sec := section.New(section.Process, capver.ProcessSecV4, 0, 0) + clone, err := NewFromCapture(b, sec) require.NoError(t, err) assert.Equal(t, uint32(2436), clone.PID) @@ -177,8 +177,8 @@ func TestPSMarshalerWithPE(t *testing.T) { } b := ps.Marshal() - sec := section.New(section.Process, kcapver.ProcessSecV3, 0, 0) - clone, err := NewFromKcap(b, sec) + sec := section.New(section.Process, capver.ProcessSecV3, 0, 0) + clone, err := NewFromCapture(b, sec) require.NoError(t, err) assert.Equal(t, uint32(2436), clone.PID) diff --git a/pkg/ps/types/types_windows.go b/pkg/ps/types/types_windows.go index e837646fc..bc372b312 100644 --- a/pkg/ps/types/types_windows.go +++ b/pkg/ps/types/types_windows.go @@ -401,8 +401,8 @@ func New(pid, ppid uint32, name, cmndline, exe string, sid *windows.SID, session return ps } -// NewFromKcap reconstructs the state of the process from the capture file. -func NewFromKcap(buf []byte, sec section.Section) (*PS, error) { +// NewFromCapture reconstructs the state of the process from the capture file. +func NewFromCapture(buf []byte, sec section.Section) (*PS, error) { ps := PS{ Args: make([]string, 0), Envs: make(map[string]string), diff --git a/pkg/rules/engine_test.go b/pkg/rules/engine_test.go index 7c70f3a4e..6484fb19a 100644 --- a/pkg/rules/engine_test.go +++ b/pkg/rules/engine_test.go @@ -81,16 +81,15 @@ func init() { } func newConfig(fromFiles ...string) *config.Config { - var kstreamConfig = config.KstreamConfig{ - EnableHandleKevents: true, - EnableNetKevents: true, - EnableRegistryKevents: true, - EnableFileIOKevents: true, - EnableImageKevents: true, - EnableThreadKevents: true, - } c := &config.Config{ - Kstream: kstreamConfig, + EventSource: config.EventSourceConfig{ + EnableHandleEvents: true, + EnableNetEvents: true, + EnableRegistryEvents: true, + EnableFileIOEvents: true, + EnableImageEvents: true, + EnableThreadEvents: true, + }, Filters: &config.Filters{ Rules: config.Rules{ FromPaths: fromFiles, diff --git a/pkg/rules/sequence_test.go b/pkg/rules/sequence_test.go index 7cbedc5ae..1f300ae0e 100644 --- a/pkg/rules/sequence_test.go +++ b/pkg/rules/sequence_test.go @@ -46,7 +46,7 @@ func TestSequenceState(t *testing.T) { |evt.name = 'CreateProcess' and ps.name = 'cmd.exe'| by ps.exe |evt.name = 'CreateFile' and file.path icontains 'temp'| by file.path |evt.name = 'CreateProcess'| by ps.child.exe`, - &config.Config{Kstream: config.KstreamConfig{}, Filters: &config.Filters{}}) + &config.Config{EventSource: config.EventSourceConfig{}, Filters: &config.Filters{}}) require.NoError(t, f.Compile()) @@ -187,7 +187,7 @@ func TestSimpleSequence(t *testing.T) { maxspan 100ms |evt.name = 'CreateProcess' and ps.name = 'cmd.exe'| by ps.exe |evt.name = 'CreateFile' and file.path icontains 'temp'| by file.path - `, &config.Config{Kstream: config.KstreamConfig{EnableFileIOKevents: true}, Filters: &config.Filters{}}) + `, &config.Config{EventSource: config.EventSourceConfig{EnableFileIOEvents: true}, Filters: &config.Filters{}}) require.NoError(t, f.Compile()) ss := newSequenceState(f, c, new(ps.SnapshotterMock)) @@ -273,7 +273,7 @@ func TestSimpleSequenceMultiplePartials(t *testing.T) { by ps.pid |evt.name = 'CreateProcess' and ps.name = 'cmd.exe'| |evt.name = 'CreateFile' and file.path icontains 'temp'| - `, &config.Config{Kstream: config.KstreamConfig{EnableFileIOKevents: true}, Filters: &config.Filters{}}) + `, &config.Config{EventSource: config.EventSourceConfig{EnableFileIOEvents: true}, Filters: &config.Filters{}}) require.NoError(t, f.Compile()) ss := newSequenceState(f, c, new(ps.SnapshotterMock)) @@ -379,7 +379,7 @@ func TestSimpleSequenceDeadline(t *testing.T) { maxspan 100ms |evt.name = 'CreateProcess' and ps.name = 'cmd.exe'| by ps.exe |evt.name = 'CreateFile' and file.path icontains 'temp'| by file.path - `, &config.Config{Kstream: config.KstreamConfig{EnableFileIOKevents: true}, Filters: &config.Filters{}}) + `, &config.Config{EventSource: config.EventSourceConfig{EnableFileIOEvents: true}, Filters: &config.Filters{}}) require.NoError(t, f.Compile()) ss := newSequenceState(f, c, new(ps.SnapshotterMock)) @@ -450,7 +450,7 @@ func TestComplexSequence(t *testing.T) { |evt.name = 'CreateProcess' and ps.child.name in ('firefox.exe', 'chrome.exe', 'edge.exe')| by ps.child.pid |evt.name = 'CreateFile' and file.operation = 'CREATE' and file.extension = '.exe'| by ps.pid |evt.name in ('Send', 'Connect')| by ps.pid - `, &config.Config{Kstream: config.KstreamConfig{EnableFileIOKevents: true}, Filters: &config.Filters{}}) + `, &config.Config{EventSource: config.EventSourceConfig{EnableFileIOEvents: true}, Filters: &config.Filters{}}) require.NoError(t, f.Compile()) ss := newSequenceState(f, c, new(ps.SnapshotterMock)) @@ -543,7 +543,7 @@ func TestSequenceOOO(t *testing.T) { maxspan 2m |evt.name = 'OpenProcess' and evt.arg[exe] imatches '?:\\Windows\\System32\\lsass.exe'| by ps.uuid |evt.name = 'CreateFile' and file.operation = 'CREATE' and file.extension = '.dmp'| by ps.uuid - `, &config.Config{Kstream: config.KstreamConfig{EnableFileIOKevents: true}, Filters: &config.Filters{}}) + `, &config.Config{EventSource: config.EventSourceConfig{EnableFileIOEvents: true}, Filters: &config.Filters{}}) require.NoError(t, f.Compile()) ss := newSequenceState(f, c, new(ps.SnapshotterMock)) @@ -603,7 +603,7 @@ func TestSequenceGC(t *testing.T) { by ps.uuid |evt.name = 'OpenProcess' and evt.arg[exe] imatches '?:\\Windows\\System32\\lsass.exe'| |evt.name = 'CreateFile' and file.operation = 'CREATE' and file.extension = '.dmp'| - `, &config.Config{Kstream: config.KstreamConfig{EnableFileIOKevents: true}, Filters: &config.Filters{}}) + `, &config.Config{EventSource: config.EventSourceConfig{EnableFileIOEvents: true}, Filters: &config.Filters{}}) require.NoError(t, f.Compile()) ss := newSequenceState(f, c, new(ps.SnapshotterMock)) @@ -752,7 +752,7 @@ func TestSequenceExpire(t *testing.T) { for _, tt := range tests { t.Run(tt.expr, func(t *testing.T) { - f := filter.New(tt.expr, &config.Config{Kstream: config.KstreamConfig{EnableFileIOKevents: true}, Filters: &config.Filters{}}) + f := filter.New(tt.expr, &config.Config{EventSource: config.EventSourceConfig{EnableFileIOEvents: true}, Filters: &config.Filters{}}) require.NoError(t, f.Compile()) ss := newSequenceState(f, tt.c, new(ps.SnapshotterMock)) @@ -784,7 +784,7 @@ func TestSequenceBoundFields(t *testing.T) { |evt.name = 'CreateProcess' and ps.name = 'cmd.exe'| as e1 |evt.name = 'CreateFile' and file.path icontains 'temp' and $e1.ps.sid = ps.sid| as e2 |evt.name = 'Connect' and ps.sid != $e2.ps.sid and ps.sid = $e1.ps.sid| - `, &config.Config{Kstream: config.KstreamConfig{EnableFileIOKevents: true}, Filters: &config.Filters{}}) + `, &config.Config{EventSource: config.EventSourceConfig{EnableFileIOEvents: true}, Filters: &config.Filters{}}) require.NoError(t, f.Compile()) ss := newSequenceState(f, c, new(ps.SnapshotterMock)) @@ -879,7 +879,7 @@ func TestSequenceBoundFieldsWithFunctions(t *testing.T) { |evt.name = 'RegSetValue' and registry.path ~= 'HKEY_CURRENT_USER\\Volatile Environment\\Notification Packages' and get_reg_value(registry.path) iin (base($e1.file.path, false))| - `, &config.Config{Kstream: config.KstreamConfig{EnableFileIOKevents: true, EnableRegistryKevents: true}, Filters: &config.Filters{}}) + `, &config.Config{EventSource: config.EventSourceConfig{EnableFileIOEvents: true, EnableRegistryEvents: true}, Filters: &config.Filters{}}) require.NoError(t, f.Compile()) ss := newSequenceState(f, c, new(ps.SnapshotterMock)) @@ -939,7 +939,7 @@ func TestIsExpressionEvaluable(t *testing.T) { maxspan 100ms |evt.name = 'CreateProcess' and ps.name = 'cmd.exe'| by ps.exe |evt.name = 'CreateFile' and file.path icontains 'temp'| by file.path - `, &config.Config{Kstream: config.KstreamConfig{EnableFileIOKevents: true}, Filters: &config.Filters{}}) + `, &config.Config{EventSource: config.EventSourceConfig{EnableFileIOEvents: true}, Filters: &config.Filters{}}) require.NoError(t, f.Compile()) ss := newSequenceState(f, c, new(ps.SnapshotterMock)) diff --git a/pkg/yara/scanner_unsupported.go b/pkg/yara/scanner_unsupported.go index 63f28b77f..828b2a3c0 100644 --- a/pkg/yara/scanner_unsupported.go +++ b/pkg/yara/scanner_unsupported.go @@ -22,12 +22,12 @@ package yara import ( - kerrors "github.com/rabbitstack/fibratus/pkg/errors" + errs "github.com/rabbitstack/fibratus/pkg/errors" "github.com/rabbitstack/fibratus/pkg/ps" "github.com/rabbitstack/fibratus/pkg/yara/config" ) // NewScanner returns unsupported scanner error. func NewScanner(psnap ps.Snapshotter, config config.Config) (Scanner, error) { - return nil, kerrors.ErrFeatureUnsupported("yara") + return nil, errs.ErrFeatureUnsupported("yara") } From d9e690e3fc204cb5b8624b330dd43e8fe2987094 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Wed, 28 May 2025 19:09:00 +0200 Subject: [PATCH 03/42] refactor: Deprecate kevt.* filter fields --- pkg/filter/accessor.go | 44 ++++---- pkg/filter/fields/fields_windows.go | 168 ++++++++++++++++++++-------- pkg/filter/filter_test.go | 2 +- pkg/filter/ql/error_test.go | 2 +- pkg/filter/ql/literal.go | 2 +- pkg/filter/ql/parser_test.go | 4 +- pkg/rules/compiler.go | 2 +- pkg/rules/engine.go | 6 +- 8 files changed, 153 insertions(+), 77 deletions(-) diff --git a/pkg/filter/accessor.go b/pkg/filter/accessor.go index 27124c495..21e05dc72 100644 --- a/pkg/filter/accessor.go +++ b/pkg/filter/accessor.go @@ -64,51 +64,51 @@ const dateFmt = "2006-01-02" func (k *evtAccessor) Get(f Field, evt *event.Event) (params.Value, error) { switch f.Name { - case fields.KevtSeq: + case fields.EvtSeq, fields.KevtSeq: return evt.Seq, nil - case fields.KevtPID: + case fields.EvtPID, fields.KevtPID: return evt.PID, nil - case fields.KevtTID: + case fields.EvtTID, fields.KevtTID: return evt.Tid, nil - case fields.KevtCPU: + case fields.EvtCPU, fields.KevtCPU: return evt.CPU, nil - case fields.KevtName: + case fields.EvtName, fields.KevtName: return evt.Name, nil - case fields.KevtCategory: + case fields.EvtCategory, fields.KevtCategory: return string(evt.Category), nil - case fields.KevtDesc: + case fields.EvtDesc, fields.KevtDesc: return evt.Description, nil - case fields.KevtHost: + case fields.EvtHost, fields.KevtHost: return evt.Host, nil - case fields.KevtTime: + case fields.EvtTime, fields.KevtTime: return evt.Timestamp.Format(timeFmt), nil - case fields.KevtTimeHour: + case fields.EvtTimeHour, fields.KevtTimeHour: return uint8(evt.Timestamp.Hour()), nil - case fields.KevtTimeMin: + case fields.EvtTimeMin, fields.KevtTimeMin: return uint8(evt.Timestamp.Minute()), nil - case fields.KevtTimeSec: + case fields.EvtTimeSec, fields.KevtTimeSec: return uint8(evt.Timestamp.Second()), nil - case fields.KevtTimeNs: + case fields.EvtTimeNs, fields.KevtTimeNs: return evt.Timestamp.UnixNano(), nil - case fields.KevtDate: + case fields.EvtDate, fields.KevtDate: return evt.Timestamp.Format(dateFmt), nil - case fields.KevtDateDay: + case fields.EvtDateDay, fields.KevtDateDay: return uint8(evt.Timestamp.Day()), nil - case fields.KevtDateMonth: + case fields.EvtDateMonth, fields.KevtDateMonth: return uint8(evt.Timestamp.Month()), nil - case fields.KevtDateTz: + case fields.EvtDateTz, fields.KevtDateTz: tz, _ := evt.Timestamp.Zone() return tz, nil - case fields.KevtDateYear: + case fields.EvtDateYear, fields.KevtDateYear: return uint32(evt.Timestamp.Year()), nil - case fields.KevtDateWeek: + case fields.EvtDateWeek, fields.KevtDateWeek: _, week := evt.Timestamp.ISOWeek() return uint8(week), nil - case fields.KevtDateWeekday: + case fields.EvtDateWeekday, fields.KevtDateWeekday: return evt.Timestamp.Weekday().String(), nil - case fields.KevtNparams: + case fields.EvtNparams, fields.KevtNparams: return uint64(evt.Params.Len()), nil - case fields.KevtArg: + case fields.EvtArg, fields.KevtArg: // lookup the parameter from the field argument // and depending on the parameter type, return // the respective value. The field format is diff --git a/pkg/filter/fields/fields_windows.go b/pkg/filter/fields/fields_windows.go index 4970397cb..4c50c826f 100644 --- a/pkg/filter/fields/fields_windows.go +++ b/pkg/filter/fields/fields_windows.go @@ -300,52 +300,95 @@ const ( // PePsChildFileName represents the original file name of the child process executable provided at compile-time PePsChildFileName Field = "pe.ps.child.file.name" + // EvtSeq is the event sequence number + EvtSeq Field = "evt.seq" + // EvtPID is the process identifier that generated the event + EvtPID Field = "evt.pid" + // EvtTID is the thread identifier that generated the event + EvtTID Field = "evt.tid" + // EvtCPU is the CPU core where the event was generated + EvtCPU Field = "evt.cpu" + // EvtDesc represents the event description + EvtDesc Field = "evt.desc" + // EvtHost represents the host where the event was produced + EvtHost Field = "evt.host" + // EvtTime is the event time + EvtTime Field = "evt.time" + // EvtTimeHour is the hour part of the event time + EvtTimeHour Field = "evt.time.h" + // EvtTimeMin is the minute part of the event time + EvtTimeMin Field = "evt.time.m" + // EvtTimeSec is the second part of the event time + EvtTimeSec Field = "evt.time.s" + // EvtTimeNs is the nanosecond part of the event time + EvtTimeNs Field = "evt.time.ns" + // EvtDate is the event date + EvtDate Field = "evt.date" + // EvtDateDay is the day of event date + EvtDateDay Field = "evt.date.d" + // EvtDateMonth is the month of event date + EvtDateMonth Field = "evt.date.m" + // EvtDateYear is the year of event date + EvtDateYear Field = "evt.date.y" + // EvtDateTz is the time zone of event timestamp + EvtDateTz Field = "evt.date.tz" + // EvtDateWeek is the event week number + EvtDateWeek Field = "evt.date.week" + // EvtDateWeekday is the event week day + EvtDateWeekday Field = "evt.date.weekday" + // EvtName is the event name + EvtName Field = "evt.name" + // EvtCategory is the event category + EvtCategory Field = "evt.category" + // EvtNparams is the number of event parameters + EvtNparams Field = "evt.nparams" + // EvtArg represents the field sequence for generic argument access + EvtArg Field = "evt.arg" + // KevtSeq is the event sequence number - KevtSeq Field = "evt.seq" + KevtSeq Field = "kevt.seq" // KevtPID is the process identifier that generated the event - KevtPID Field = "evt.pid" + KevtPID Field = "kevt.pid" // KevtTID is the thread identifier that generated the event - KevtTID Field = "evt.tid" + KevtTID Field = "kevt.tid" // KevtCPU is the CPU core where the event was generated - KevtCPU Field = "evt.cpu" + KevtCPU Field = "kevt.cpu" // KevtDesc represents the event description - KevtDesc Field = "evt.desc" + KevtDesc Field = "kevt.desc" // KevtHost represents the host where the event was produced - KevtHost Field = "evt.host" + KevtHost Field = "kevt.host" // KevtTime is the event time - KevtTime Field = "evt.time" + KevtTime Field = "kevt.time" // KevtTimeHour is the hour part of the event time - KevtTimeHour Field = "evt.time.h" + KevtTimeHour Field = "kevt.time.h" // KevtTimeMin is the minute part of the event time - KevtTimeMin Field = "evt.time.m" + KevtTimeMin Field = "kevt.time.m" // KevtTimeSec is the second part of the event time - KevtTimeSec Field = "evt.time.s" + KevtTimeSec Field = "kevt.time.s" // KevtTimeNs is the nanosecond part of the event time - KevtTimeNs Field = "evt.time.ns" + KevtTimeNs Field = "kevt.time.ns" // KevtDate is the event date - KevtDate Field = "evt.date" + KevtDate Field = "kevt.date" // KevtDateDay is the day of event date - KevtDateDay Field = "evt.date.d" + KevtDateDay Field = "kevt.date.d" // KevtDateMonth is the month of event date - KevtDateMonth Field = "evt.date.m" + KevtDateMonth Field = "kevt.date.m" // KevtDateYear is the year of event date - KevtDateYear Field = "evt.date.y" + KevtDateYear Field = "kevt.date.y" // KevtDateTz is the time zone of event timestamp - KevtDateTz Field = "evt.date.tz" + KevtDateTz Field = "kevt.date.tz" // KevtDateWeek is the event week number - KevtDateWeek Field = "evt.date.week" + KevtDateWeek Field = "kevt.date.week" // KevtDateWeekday is the event week day - KevtDateWeekday Field = "evt.date.weekday" + KevtDateWeekday Field = "kevt.date.weekday" // KevtName is the event name - KevtName Field = "evt.name" + KevtName Field = "kevt.name" // KevtCategory is the event category - KevtCategory Field = "evt.category" - // KevtMeta is the event metadata - KevtMeta Field = "evt.meta" + KevtCategory Field = "kevt.category" // KevtNparams is the number of event parameters - KevtNparams Field = "evt.nparams" + KevtNparams Field = "kevt.nparams" // KevtArg represents the field sequence for generic argument access - KevtArg Field = "evt.arg" + KevtArg Field = "kevt.arg" // HandleID represents the handle identifier within the process address space HandleID Field = "handle.id" @@ -734,28 +777,61 @@ func IsPseudoField(f Field) bool { func (f Field) IsPeSectionsPseudo() bool { return f == PeSections } var fields = map[Field]FieldInfo{ - KevtSeq: {KevtSeq, "event sequence number", params.Uint64, []string{"evt.seq > 666"}, nil, nil}, - KevtPID: {KevtPID, "process identifier generating the kernel event", params.Uint32, []string{"evt.pid = 6"}, nil, nil}, - KevtTID: {KevtTID, "thread identifier generating the kernel event", params.Uint32, []string{"evt.tid = 1024"}, nil, nil}, - KevtCPU: {KevtCPU, "logical processor core where the event was generated", params.Uint8, []string{"evt.cpu = 2"}, nil, nil}, - KevtName: {KevtName, "symbolical kernel event name", params.AnsiString, []string{"evt.name = 'CreateThread'"}, nil, nil}, - KevtCategory: {KevtCategory, "event category", params.AnsiString, []string{"evt.category = 'registry'"}, nil, nil}, - KevtDesc: {KevtDesc, "event description", params.AnsiString, []string{"evt.desc contains 'Creates a new process'"}, nil, nil}, - KevtHost: {KevtHost, "host name on which the event was produced", params.UnicodeString, []string{"evt.host contains 'kitty'"}, nil, nil}, - KevtTime: {KevtTime, "event timestamp as a time string", params.Time, []string{"evt.time = '17:05:32'"}, nil, nil}, - KevtTimeHour: {KevtTimeHour, "hour within the day on which the event occurred", params.Time, []string{"evt.time.h = 23"}, nil, nil}, - KevtTimeMin: {KevtTimeMin, "minute offset within the hour on which the event occurred", params.Time, []string{"evt.time.m = 54"}, nil, nil}, - KevtTimeSec: {KevtTimeSec, "second offset within the minute on which the event occurred", params.Time, []string{"evt.time.s = 0"}, nil, nil}, - KevtTimeNs: {KevtTimeNs, "nanoseconds specified by event timestamp", params.Int64, []string{"evt.time.ns > 1591191629102337000"}, nil, nil}, - KevtDate: {KevtDate, "event timestamp as a date string", params.Time, []string{"evt.date = '2018-03-03'"}, nil, nil}, - KevtDateDay: {KevtDateDay, "day of the month on which the event occurred", params.Time, []string{"evt.date.d = 12"}, nil, nil}, - KevtDateMonth: {KevtDateMonth, "month of the year on which the event occurred", params.Time, []string{"evt.date.m = 11"}, nil, nil}, - KevtDateYear: {KevtDateYear, "year on which the event occurred", params.Uint32, []string{"evt.date.y = 2020"}, nil, nil}, - KevtDateTz: {KevtDateTz, "time zone associated with the event timestamp", params.AnsiString, []string{"evt.date.tz = 'UTC'"}, nil, nil}, - KevtDateWeek: {KevtDateWeek, "week number within the year on which the event occurred", params.Uint8, []string{"evt.date.week = 2"}, nil, nil}, - KevtDateWeekday: {KevtDateWeekday, "week day on which the event occurred", params.AnsiString, []string{"evt.date.weekday = 'Monday'"}, nil, nil}, - KevtNparams: {KevtNparams, "number of parameters", params.Int8, []string{"evt.nparams > 2"}, nil, nil}, - KevtArg: {KevtArg, "event parameter", params.Object, []string{"evt.arg[cmdline] istartswith 'C:\\Windows'"}, nil, &Argument{Optional: false, Pattern: "[a-z0-9_]+", ValidationFunc: func(s string) bool { + EvtSeq: {EvtSeq, "event sequence number", params.Uint64, []string{"evt.seq > 666"}, nil, nil}, + EvtPID: {EvtPID, "process identifier generating the event", params.Uint32, []string{"evt.pid = 6"}, nil, nil}, + EvtTID: {EvtTID, "thread identifier generating the event", params.Uint32, []string{"evt.tid = 1024"}, nil, nil}, + EvtCPU: {EvtCPU, "logical processor core where the event was generated", params.Uint8, []string{"evt.cpu = 2"}, nil, nil}, + EvtName: {EvtName, "symbolical event name", params.AnsiString, []string{"evt.name = 'CreateThread'"}, nil, nil}, + EvtCategory: {EvtCategory, "event category", params.AnsiString, []string{"evt.category = 'registry'"}, nil, nil}, + EvtDesc: {EvtDesc, "event description", params.AnsiString, []string{"evt.desc contains 'Creates a new process'"}, nil, nil}, + EvtHost: {EvtHost, "host name on which the event was produced", params.UnicodeString, []string{"evt.host contains 'kitty'"}, nil, nil}, + EvtTime: {EvtTime, "event timestamp as a time string", params.Time, []string{"evt.time = '17:05:32'"}, nil, nil}, + EvtTimeHour: {EvtTimeHour, "hour within the day on which the event occurred", params.Time, []string{"evt.time.h = 23"}, nil, nil}, + EvtTimeMin: {EvtTimeMin, "minute offset within the hour on which the event occurred", params.Time, []string{"evt.time.m = 54"}, nil, nil}, + EvtTimeSec: {EvtTimeSec, "second offset within the minute on which the event occurred", params.Time, []string{"evt.time.s = 0"}, nil, nil}, + EvtTimeNs: {EvtTimeNs, "nanoseconds specified by event timestamp", params.Int64, []string{"evt.time.ns > 1591191629102337000"}, nil, nil}, + EvtDate: {EvtDate, "event timestamp as a date string", params.Time, []string{"evt.date = '2018-03-03'"}, nil, nil}, + EvtDateDay: {EvtDateDay, "day of the month on which the event occurred", params.Time, []string{"evt.date.d = 12"}, nil, nil}, + EvtDateMonth: {EvtDateMonth, "month of the year on which the event occurred", params.Time, []string{"evt.date.m = 11"}, nil, nil}, + EvtDateYear: {EvtDateYear, "year on which the event occurred", params.Uint32, []string{"evt.date.y = 2020"}, nil, nil}, + EvtDateTz: {EvtDateTz, "time zone associated with the event timestamp", params.AnsiString, []string{"evt.date.tz = 'UTC'"}, nil, nil}, + EvtDateWeek: {EvtDateWeek, "week number within the year on which the event occurred", params.Uint8, []string{"evt.date.week = 2"}, nil, nil}, + EvtDateWeekday: {EvtDateWeekday, "week day on which the event occurred", params.AnsiString, []string{"evt.date.weekday = 'Monday'"}, nil, nil}, + EvtNparams: {EvtNparams, "number of parameters", params.Int8, []string{"evt.nparams > 2"}, nil, nil}, + EvtArg: {EvtArg, "event parameter", params.Object, []string{"evt.arg[cmdline] istartswith 'C:\\Windows'"}, nil, &Argument{Optional: false, Pattern: "[a-z0-9_]+", ValidationFunc: func(s string) bool { + for _, c := range s { + switch { + case unicode.IsLower(c): + case unicode.IsNumber(c): + case c == '_': + default: + return false + } + } + return true + }}}, + KevtSeq: {KevtSeq, "event sequence number", params.Uint64, []string{"kevt.seq > 666"}, &Deprecation{Since: "3.0.0", Fields: []Field{EvtSeq}}, nil}, + KevtPID: {KevtPID, "process identifier generating the event", params.Uint32, []string{"kevt.pid = 6"}, &Deprecation{Since: "3.0.0", Fields: []Field{EvtPID}}, nil}, + KevtTID: {KevtTID, "thread identifier generating the event", params.Uint32, []string{"kevt.tid = 1024"}, &Deprecation{Since: "3.0.0", Fields: []Field{EvtTID}}, nil}, + KevtCPU: {KevtCPU, "logical processor core where the event was generated", params.Uint8, []string{"kevt.cpu = 2"}, &Deprecation{Since: "3.0.0", Fields: []Field{EvtCPU}}, nil}, + KevtName: {KevtName, "symbolical event name", params.AnsiString, []string{"kevt.name = 'CreateThread'"}, &Deprecation{Since: "3.0.0", Fields: []Field{EvtName}}, nil}, + KevtCategory: {KevtCategory, "event category", params.AnsiString, []string{"kevt.category = 'registry'"}, &Deprecation{Since: "3.0.0", Fields: []Field{EvtCategory}}, nil}, + KevtDesc: {KevtDesc, "event description", params.AnsiString, []string{"kevt.desc contains 'Creates a new process'"}, &Deprecation{Since: "3.0.0", Fields: []Field{EvtDesc}}, nil}, + KevtHost: {KevtHost, "host name on which the event was produced", params.UnicodeString, []string{"kevt.host contains 'kitty'"}, &Deprecation{Since: "3.0.0", Fields: []Field{EvtHost}}, nil}, + KevtTime: {KevtTime, "event timestamp as a time string", params.Time, []string{"kevt.time = '17:05:32'"}, &Deprecation{Since: "3.0.0", Fields: []Field{EvtTime}}, nil}, + KevtTimeHour: {KevtTimeHour, "hour within the day on which the event occurred", params.Time, []string{"kevt.time.h = 23"}, &Deprecation{Since: "3.0.0", Fields: []Field{EvtTimeHour}}, nil}, + KevtTimeMin: {KevtTimeMin, "minute offset within the hour on which the event occurred", params.Time, []string{"kevt.time.m = 54"}, &Deprecation{Since: "3.0.0", Fields: []Field{EvtTimeMin}}, nil}, + KevtTimeSec: {KevtTimeSec, "second offset within the minute on which the event occurred", params.Time, []string{"kevt.time.s = 0"}, &Deprecation{Since: "3.0.0", Fields: []Field{EvtTimeSec}}, nil}, + KevtTimeNs: {KevtTimeNs, "nanoseconds specified by event timestamp", params.Int64, []string{"kevt.time.ns > 1591191629102337000"}, &Deprecation{Since: "3.0.0", Fields: []Field{EvtTimeNs}}, nil}, + KevtDate: {KevtDate, "event timestamp as a date string", params.Time, []string{"kevt.date = '2018-03-03'"}, &Deprecation{Since: "3.0.0", Fields: []Field{EvtDate}}, nil}, + KevtDateDay: {KevtDateDay, "day of the month on which the event occurred", params.Time, []string{"kevt.date.d = 12"}, &Deprecation{Since: "3.0.0", Fields: []Field{EvtDateDay}}, nil}, + KevtDateMonth: {KevtDateMonth, "month of the year on which the event occurred", params.Time, []string{"kevt.date.m = 11"}, &Deprecation{Since: "3.0.0", Fields: []Field{EvtDateMonth}}, nil}, + KevtDateYear: {KevtDateYear, "year on which the event occurred", params.Uint32, []string{"kevt.date.y = 2020"}, &Deprecation{Since: "3.0.0", Fields: []Field{EvtDateYear}}, nil}, + KevtDateTz: {KevtDateTz, "time zone associated with the event timestamp", params.AnsiString, []string{"kevt.date.tz = 'UTC'"}, &Deprecation{Since: "3.0.0", Fields: []Field{EvtDateTz}}, nil}, + KevtDateWeek: {KevtDateWeek, "week number within the year on which the event occurred", params.Uint8, []string{"kevt.date.week = 2"}, &Deprecation{Since: "3.0.0", Fields: []Field{EvtDateWeek}}, nil}, + KevtDateWeekday: {KevtDateWeekday, "week day on which the event occurred", params.AnsiString, []string{"kevt.date.weekday = 'Monday'"}, &Deprecation{Since: "3.0.0", Fields: []Field{EvtDateWeekday}}, nil}, + KevtNparams: {KevtNparams, "number of parameters", params.Int8, []string{"kevt.nparams > 2"}, &Deprecation{Since: "3.0.0", Fields: []Field{EvtNparams}}, nil}, + KevtArg: {KevtArg, "event parameter", params.Object, []string{"kevt.arg[cmdline] istartswith 'C:\\Windows'"}, &Deprecation{Since: "3.0.0", Fields: []Field{EvtArg}}, &Argument{Optional: false, Pattern: "[a-z0-9_]+", ValidationFunc: func(s string) bool { for _, c := range s { switch { case unicode.IsLower(c): diff --git a/pkg/filter/filter_test.go b/pkg/filter/filter_test.go index 956324da9..a105f25d7 100644 --- a/pkg/filter/filter_test.go +++ b/pkg/filter/filter_test.go @@ -104,7 +104,7 @@ func TestStringFields(t *testing.T) { f := New(`ps.name = 'cmd.exe' and evt.name = 'CreateProcess' or evt.name in ('TerminateProcess', 'CreateFile')`, cfg) require.NoError(t, f.Compile()) assert.Len(t, f.GetStringFields(), 2) - assert.Len(t, f.GetStringFields()[fields.KevtName], 3) + assert.Len(t, f.GetStringFields()[fields.EvtName], 3) assert.Len(t, f.GetStringFields()[fields.PsName], 1) } diff --git a/pkg/filter/ql/error_test.go b/pkg/filter/ql/error_test.go index ca131c384..fe27d25aa 100644 --- a/pkg/filter/ql/error_test.go +++ b/pkg/filter/ql/error_test.go @@ -59,7 +59,7 @@ func TestParseError(t *testing.T) { registry.key.name icontains ( CurrentVersion\\Run', -╭─────────────^ +╭──────────────^ | | 'Policies\\Explorer\\Run', | 'Group Policy\\Scripts', diff --git a/pkg/filter/ql/literal.go b/pkg/filter/ql/literal.go index 78ebf75ed..208d9268a 100644 --- a/pkg/filter/ql/literal.go +++ b/pkg/filter/ql/literal.go @@ -338,7 +338,7 @@ func (e *SequenceExpr) walk() { // initialize event type/category buckets for every such field for name, values := range stringFields { - if name == fields.KevtName || name == fields.KevtCategory { + if name == fields.EvtName || name == fields.EvtCategory { for _, v := range values { e.buckets[hashers.FnvUint32([]byte(v))] = true if etype := event.NameToType(v); etype.Exists() { diff --git a/pkg/filter/ql/parser_test.go b/pkg/filter/ql/parser_test.go index 30105584d..7199df993 100644 --- a/pkg/filter/ql/parser_test.go +++ b/pkg/filter/ql/parser_test.go @@ -64,9 +64,9 @@ func TestParser(t *testing.T) { {expr: `ps.envs imatches 'C:\\Program Files'`}, {expr: `ps.pid[1] = 'svchost.exe'`, err: errors.New("ps.pid[1] = 'svchost.exe'\n╭──────^\n|\n|\n╰─────────────────── expected field without argument")}, {expr: `ps.envs[ProgramFiles = 'svchost.exe'`, err: errors.New("ps.envs[ProgramFiles = 'svchost.exe'\n╭───────────────────^\n|\n|\n╰─────────────────── expected ]")}, - {expr: `evt.arg = 'svchost.exe'`, err: errors.New("evt.arg = 'svchost.exe'\n╭───────^\n|\n|\n╰─────────────────── expected field argument")}, + {expr: `evt.arg = 'svchost.exe'`, err: errors.New("evt.arg = 'svchost.exe'\n╭──────^\n|\n|\n╰─────────────────── expected field argument")}, {expr: `evt.arg[name] = 'svchost.exe'`}, - {expr: `evt.arg[Name$] = 'svchost.exe'`, err: errors.New("evt.arg[Name$] = 'svchost.exe'\n╭────────^\n|\n|\n╰─────────────────── expected a valid field argument matching the pattern [a-z0-9_]+")}, + {expr: `evt.arg[Name$] = 'svchost.exe'`, err: errors.New("evt.arg[Name$] = 'svchost.exe'\n╭───────^\n|\n|\n╰─────────────────── expected a valid field argument matching the pattern [a-z0-9_]+")}, {expr: `ps.ancestor[0] = 'svchost.exe'`}, {expr: `ps.ancestor[l0l] = 'svchost.exe'`, err: errors.New("ps.ancestor[l0l] = 'svchost.exe'\n╭───────────^\n|\n|\n╰─────────────────── expected a valid field argument matching the pattern [0-9]+")}, } diff --git a/pkg/rules/compiler.go b/pkg/rules/compiler.go index 7016341e5..7d964f3c2 100644 --- a/pkg/rules/compiler.go +++ b/pkg/rules/compiler.go @@ -121,7 +121,7 @@ func (c *compiler) buildCompileResult(filters map[*config.FilterConfig]filter.Fi rs.NumberRules++ for name, values := range f.GetStringFields() { for _, v := range values { - if name == fields.KevtName || name == fields.KevtCategory { + if name == fields.EvtName || name == fields.EvtCategory { types := event.NameToTypes(v) for _, typ := range types { switch typ.Category() { diff --git a/pkg/rules/engine.go b/pkg/rules/engine.go index 2cd74569e..e9b6cc294 100644 --- a/pkg/rules/engine.go +++ b/pkg/rules/engine.go @@ -151,7 +151,7 @@ func newCompiledFilter(f filter.Filter, c *config.FilterConfig, ss *sequenceStat // conditions. func (f *compiledFilter) isScoped() bool { for name := range f.filter.GetStringFields() { - if name == fields.KevtName || name == fields.KevtCategory { + if name == fields.EvtName || name == fields.EvtCategory { return true } } @@ -233,8 +233,8 @@ func (e *Engine) Compile() (*config.RulesCompileResult, error) { // or event category hash for name, values := range f.GetStringFields() { for _, v := range values { - if name == fields.KevtName || name == fields.KevtCategory { - if name == fields.KevtCategory { + if name == fields.EvtName || name == fields.EvtCategory { + if name == fields.EvtCategory { e.hashCache.lookupCategory = true } hash := hashers.FnvUint32([]byte(v)) From 80798687b0fa0d55af3fc25b0fc12899aa1ad218 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Fri, 30 May 2025 18:56:54 +0200 Subject: [PATCH 04/42] refactor(rules): Adapt rules to use the `evt.` filter field --- rules/README.md | 2 +- ...ail_access_file_access_to_sam_database.yml | 4 +- ...ntial_access_from_backups_via_rundll32.yml | 4 +- ...cess_credential_discovery_via_vaultcmd.yml | 4 +- ..._lsass_access_from_unsigned_executable.yml | 6 +- ..._access_lsass_handle_leak_via_seclogon.yml | 6 +- ...mp_preparation_via_silent_process_exit.yml | 4 +- ...sass_memory_dump_via_minidumpwritedump.yml | 6 +- ...ntial_access_lsass_memory_dump_via_wer.yml | 4 +- ...credential_access_lsass_memory_dumping.yml | 6 +- ..._process_clone_creation_via_reflection.yml | 4 +- ...tial_access_potential_sam_hive_dumping.yml | 4 +- ...cess_remote_thread_creation_into_lsass.yml | 6 +- ...ss_to_active_directory_domain_database.yml | 4 +- ...ous_access_to_unattended_panther_files.yml | 4 +- ...us_access_to_windows_dpapi_master_keys.yml | 4 +- ...icious_access_to_windows_manager_files.yml | 4 +- ...spicious_access_to_windows_vault_files.yml | 4 +- ...cious_security_package_loaded_by_lsass.yml | 4 +- ...ccess_suspicious_vault_client_dll_load.yml | 4 +- ...tial_access_unusual_access_to_ssh_keys.yml | 4 +- ...ccess_to_web_browser_credential_stores.yml | 4 +- ...l_access_to_windows_credential_history.yml | 4 +- ...jection_via_clr_search_order_hijacking.yml | 4 +- rules/defense_evasion_clear_eventlog.yml | 6 +- ...fense_evasion_dll_loaded_via_apc_queue.yml | 4 +- ...asion_dll_loaded_via_callback_function.yml | 4 +- ..._dll_loaded_via_ldrpkernel32_overwrite.yml | 4 +- ...sion_dll_sideloading_via_copied_binary.yml | 4 +- ...ding_via_microsoft_office_dropped_file.yml | 4 +- ...t_assembly_loaded_by_unmanaged_process.yml | 4 +- ...e_evasion_hidden_registry_key_creation.yml | 6 +- ...vasion_image_load_via_ntfs_transaction.yml | 6 +- ...tential_injection_via_dotnet_debugging.yml | 4 +- ...tential_process_creation_via_shellcode.yml | 4 +- ...ential_process_doppelganging_injection.yml | 4 +- ..._potential_process_hollowing_injection.yml | 4 +- ...s_injection_via_tainted_memory_section.yml | 8 +-- ...llcode_execution_via_etw_logger_thread.yml | 6 +- ...n_potential_thread_execution_hijacking.yml | 4 +- ...ss_execution_from_self_deleting_binary.yml | 4 +- ...sion_process_spawned_via_remote_thread.yml | 4 +- ...e_evasion_regsvr32_scriptlet_execution.yml | 4 +- ...on_suspicious_access_to_the_hosts_file.yml | 4 +- ..._dll_loaded_via_memory_section_mapping.yml | 6 +- ...ious_html_application_script_execution.yml | 4 +- ...spicious_object_symbolic_link_creation.yml | 10 +-- ...ender_exclusions_registry_modification.yml | 4 +- ...vasion_suspicious_xsl_script_execution.yml | 4 +- ...em_binary_proxy_execution_via_rundll32.yml | 4 +- ...hread_context_set_from_unbacked_memory.yml | 4 +- ...signed_dll_injection_via_remote_thread.yml | 4 +- ...nder_protection_tampering_via_registry.yml | 4 +- ...acro_enabled_microsoft_office_document.yml | 4 +- ...execution_via_microsoft_office_process.yml | 4 +- ...macro_execution_via_script_interpreter.yml | 4 +- ..._file_execution_via_script_interpreter.yml | 4 +- ...icrosoft_office_file_execution_via_wmi.yml | 4 +- ...lickfix_infection_chain_via_run_window.yml | 4 +- ...acro_enabled_microsoft_office_document.yml | 4 +- ...dll_loaded_by_microsoft_office_process.yml | 4 +- ..._via_wmi_from_microsoft_office_process.yml | 4 +- ...cious_microsoft_office_embedded_object.yml | 4 +- rules/macros/macros.yml | 66 +++++++++---------- ...e_file_dropped_by_unsigned_service_dll.yml | 6 +- ...sistence_hidden_local_account_creation.yml | 4 +- ...ia_startup_folder_executable_or_script.yml | 4 +- ..._persistence_via_registry_modification.yml | 4 +- rules/persistence_rid_hijacking.yml | 4 +- ...reter_or_untrusted_process_persistence.yml | 6 +- ...spicious_microsoft_office_addin_loaded.yml | 4 +- ...e_suspicious_microsoft_office_template.yml | 4 +- ..._suspicious_netsh_helper_dll_execution.yml | 4 +- ..._persistence_via_registry_modification.yml | 4 +- ...istence_suspicious_port_monitor_loaded.yml | 4 +- ...ence_suspicious_print_processor_loaded.yml | 4 +- ...ious_startup_shell_folder_modification.yml | 4 +- ...unusual_file_written_in_startup_folder.yml | 4 +- ...sual_process_modified_registry_run_key.yml | 4 +- ...e_escalation_via_phantom_dll_hijacking.yml | 6 +- ...vulnerable_or_malicious_driver_dropped.yml | 4 +- ..._vulnerable_or_malicious_driver_loaded.yml | 4 +- 82 files changed, 212 insertions(+), 212 deletions(-) diff --git a/rules/README.md b/rules/README.md index a64c82605..f735475b4 100644 --- a/rules/README.md +++ b/rules/README.md @@ -51,7 +51,7 @@ As highlighted in the previous paragraph, all rules should have the event type c ### Prefer macros over raw conditions -Fibratus comes with a [macros](https://www.fibratus.io/#/filters/rules?id=macros) library to promote the reusability and modularization of rule conditions and lists. Before trying to spell out a raw rule condition, explore the library to check if there's already a macro you can pull into the rule. For example, detecting file accesses could be accomplished by declaring the `kevt.name = 'CreateFile' and file.operation = 'open'` expression. However, the macro library comes with the `open_file` macro that you can directly call in any rule. If you can't encounter a particular macro in the library, please consider creating it. Future detection engineers and rule writers could profit from those macros. +Fibratus comes with a [macros](https://www.fibratus.io/#/filters/rules?id=macros) library to promote the reusability and modularization of rule conditions and lists. Before trying to spell out a raw rule condition, explore the library to check if there's already a macro you can pull into the rule. For example, detecting file accesses could be accomplished by declaring the `evt.name = 'CreateFile' and file.operation = 'open'` expression. However, the macro library comes with the `open_file` macro that you can directly call in any rule. If you can't encounter a particular macro in the library, please consider creating it. Future detection engineers and rule writers could profit from those macros. ### Formatting styles diff --git a/rules/credentail_access_file_access_to_sam_database.yml b/rules/credentail_access_file_access_to_sam_database.yml index d9fd3de8a..af7d03164 100644 --- a/rules/credentail_access_file_access_to_sam_database.yml +++ b/rules/credentail_access_file_access_to_sam_database.yml @@ -1,6 +1,6 @@ name: File access to SAM database id: e3dace20-4962-4381-884e-40dcdde66626 -version: 1.0.3 +version: 1.0.4 description: | Identifies access to the Security Account Manager on-disk database. labels: @@ -32,4 +32,4 @@ condition: > '?:\\Windows\\System32\\srtasks.exe' ) -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/credential_access_credential_access_from_backups_via_rundll32.yml b/rules/credential_access_credential_access_from_backups_via_rundll32.yml index d8a9301aa..5ed966629 100644 --- a/rules/credential_access_credential_access_from_backups_via_rundll32.yml +++ b/rules/credential_access_credential_access_from_backups_via_rundll32.yml @@ -1,6 +1,6 @@ name: Credentials access from backups via Rundll32 id: ff43852c-486c-4870-a318-ce976d2231a5 -version: 1.0.0 +version: 1.0.1 description: | Detects an attempt to obtain credentials from credential backups. labels: @@ -21,4 +21,4 @@ condition: > and (ps.child.args iin ('keymgr.dll') and ps.child.args iin ('KRShowKeyMgr')) -min-engine-version: 2.0.0 +min-engine-version: 3.0.0 diff --git a/rules/credential_access_credential_discovery_via_vaultcmd.yml b/rules/credential_access_credential_discovery_via_vaultcmd.yml index ccae2d63c..bf9abb78a 100644 --- a/rules/credential_access_credential_discovery_via_vaultcmd.yml +++ b/rules/credential_access_credential_discovery_via_vaultcmd.yml @@ -1,6 +1,6 @@ name: Credential discovery via VaultCmd tool id: 2ce607d3-5a14-4628-be8a-22bcde97dab5 -version: 1.1.0 +version: 1.1.1 description: | Detects the usage of the VaultCmd tool to list Windows Credentials. VaultCmd creates, displays and deletes stored credentials. An adversary may abuse this to list or dump @@ -23,4 +23,4 @@ condition: > severity: medium -min-engine-version: 2.0.0 +min-engine-version: 3.0.0 diff --git a/rules/credential_access_lsass_access_from_unsigned_executable.yml b/rules/credential_access_lsass_access_from_unsigned_executable.yml index 66329dfff..2de3fd1a3 100644 --- a/rules/credential_access_lsass_access_from_unsigned_executable.yml +++ b/rules/credential_access_lsass_access_from_unsigned_executable.yml @@ -1,6 +1,6 @@ name: LSASS access from unsigned executable id: 348bf896-2201-444f-b1c9-e957a1f063bf -version: 1.0.0 +version: 1.0.1 description: | Detects attempts by an unsigned process to access the Local Security Authority Subsystem Service (LSASS). Adversaries may try to dump credential information stored in the process memory of LSASS. @@ -21,7 +21,7 @@ condition: > maxspan 7m by ps.uuid |load_unsigned_executable| - |((open_process) or (open_thread)) and kevt.arg[exe] imatches '?:\\Windows\\System32\\lsass.exe'| + |((open_process) or (open_thread)) and evt.arg[exe] imatches '?:\\Windows\\System32\\lsass.exe'| action: - name: kill @@ -29,4 +29,4 @@ output: > Unsigned executable %1.image.path attempted to access Local Security Authority Subsystem Service severity: high -min-engine-version: 2.2.0 +min-engine-version: 3.0.0 diff --git a/rules/credential_access_lsass_handle_leak_via_seclogon.yml b/rules/credential_access_lsass_handle_leak_via_seclogon.yml index 920d2175a..f66b7aa9f 100644 --- a/rules/credential_access_lsass_handle_leak_via_seclogon.yml +++ b/rules/credential_access_lsass_handle_leak_via_seclogon.yml @@ -1,6 +1,6 @@ name: LSASS handle leak via Seclogon id: 5d55c938-875e-49e1-ae53-fa196d4445eb -version: 1.0.0 +version: 1.0.1 description: | Identifies suspicious access to LSASS process from a callstack pointing to seclogon.dll that may indicate an attempt to leak an LSASS handle via abusing the Secondary Logon service in @@ -19,10 +19,10 @@ references: - https://splintercod3.blogspot.com/p/the-hidden-side-of-seclogon-part-3.html condition: > - open_process and kevt.arg[exe] imatches '?:\\Windows\\System32\\lsass.exe' and ps.name ~= 'svchost.exe' + open_process and evt.arg[exe] imatches '?:\\Windows\\System32\\lsass.exe' and ps.name ~= 'svchost.exe' and ps.access.mask.names in ('CREATE_PROCESS', 'DUP_HANDLE') and thread.callstack.modules imatches ('*seclogon.dll') severity: high -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/credential_access_lsass_memory_dump_preparation_via_silent_process_exit.yml b/rules/credential_access_lsass_memory_dump_preparation_via_silent_process_exit.yml index dcf2248a0..36f66de07 100644 --- a/rules/credential_access_lsass_memory_dump_preparation_via_silent_process_exit.yml +++ b/rules/credential_access_lsass_memory_dump_preparation_via_silent_process_exit.yml @@ -1,6 +1,6 @@ name: LSASS memory dump preparation via SilentProcessExit id: d325e426-f89a-4f7c-b655-3874dad07986 -version: 1.0.2 +version: 1.0.3 description: | Adversaries may exploit the SilentProcessExit debugging technique to conduct LSASS memory dump via WerFault.exe (Windows Error Reporting) binary by creating @@ -27,4 +27,4 @@ references: condition: > modify_registry and registry.path imatches 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\SilentProcessExit\\lsass*' -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/credential_access_lsass_memory_dump_via_minidumpwritedump.yml b/rules/credential_access_lsass_memory_dump_via_minidumpwritedump.yml index f17e0b35b..26f88a1f0 100644 --- a/rules/credential_access_lsass_memory_dump_via_minidumpwritedump.yml +++ b/rules/credential_access_lsass_memory_dump_via_minidumpwritedump.yml @@ -1,6 +1,6 @@ name: LSASS memory dump via MiniDumpWriteDump id: fd7ced77-4a95-4658-80f6-6b9d7b5e3777 -version: 1.0.0 +version: 1.0.1 description: | Identifies access to the Local Security Authority Subsystem Service (LSASS) process to dump the memory via MiniDumpWriteDump API. @@ -20,7 +20,7 @@ references: - https://www.ired.team/offensive-security/credential-access-and-credential-dumping/dumping-lsass-passwords-without-mimikatz-minidumpwritedump-av-signature-bypass condition: > - ((open_process) or (open_thread)) and kevt.arg[exe] imatches '?:\\Windows\\System32\\lsass.exe' + ((open_process) or (open_thread)) and evt.arg[exe] imatches '?:\\Windows\\System32\\lsass.exe' and (thread.callstack.modules imatches ('*dbgcore.dll', '*comsvcs.dll') or thread.callstack.symbols imatches ('*MiniDumpWriteDump')) action: @@ -30,4 +30,4 @@ output: > LSASS memory dump attempt by process %ps.exe via MiniDumpWriteDump severity: high -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/credential_access_lsass_memory_dump_via_wer.yml b/rules/credential_access_lsass_memory_dump_via_wer.yml index b8f8357d7..447b79aa0 100644 --- a/rules/credential_access_lsass_memory_dump_via_wer.yml +++ b/rules/credential_access_lsass_memory_dump_via_wer.yml @@ -1,6 +1,6 @@ name: LSASS memory dump via Windows Error Reporting id: 7b4a74e2-c7a7-4c1f-b2ce-0e0273c3add7 -version: 1.0.2 +version: 1.0.3 description: | Adversaries may abuse Windows Error Reporting service to dump LSASS memory. The ALPC protocol can send a message to report an exception on LSASS and @@ -24,4 +24,4 @@ condition: > |spawn_process and ps.child.name iin ('WerFault.exe', 'WerFaultSecure.exe')| by ps.child.uuid |create_file and file.path icontains 'lsass'| by ps.uuid -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/credential_access_lsass_memory_dumping.yml b/rules/credential_access_lsass_memory_dumping.yml index abfbee094..eb724f20f 100644 --- a/rules/credential_access_lsass_memory_dumping.yml +++ b/rules/credential_access_lsass_memory_dumping.yml @@ -1,6 +1,6 @@ name: LSASS memory dumping via legitimate or offensive tools id: 335795af-246b-483e-8657-09a30c102e63 -version: 1.0.2 +version: 1.0.3 description: | Detects an attempt to dump the LSAAS memory to the disk by employing legitimate tools such as procdump, Task Manager, Process Explorer or built-in Windows tools @@ -25,7 +25,7 @@ condition: > by ps.uuid |open_process and ps.access.mask.names in ('ALL_ACCESS', 'CREATE_PROCESS', 'VM_READ', 'DUP_HANDLE') and - kevt.arg[exe] imatches '?:\\Windows\\System32\\lsass.exe' + evt.arg[exe] imatches '?:\\Windows\\System32\\lsass.exe' and ps.exe not imatches ( @@ -41,4 +41,4 @@ output: > and subsequently write the `%2.file.path` dump file to the disk device severity: critical -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/credential_access_lsass_process_clone_creation_via_reflection.yml b/rules/credential_access_lsass_process_clone_creation_via_reflection.yml index fe5933f78..452489e32 100644 --- a/rules/credential_access_lsass_process_clone_creation_via_reflection.yml +++ b/rules/credential_access_lsass_process_clone_creation_via_reflection.yml @@ -1,6 +1,6 @@ name: LSASS process clone creation via reflection id: cdf3810a-4832-446a-ac9d-d108cf2e313c -version: 1.0.0 +version: 1.0.1 description: | Identifies the creation of an LSASS clone process via RtlCreateProcessReflection API function. Adversaries can use this technique to dump credentials material from the LSASS fork and evade @@ -28,4 +28,4 @@ action: severity: high -min-engine-version: 2.2.0 +min-engine-version: 3.0.0 diff --git a/rules/credential_access_potential_sam_hive_dumping.yml b/rules/credential_access_potential_sam_hive_dumping.yml index 48079940d..d1a8dc93c 100644 --- a/rules/credential_access_potential_sam_hive_dumping.yml +++ b/rules/credential_access_potential_sam_hive_dumping.yml @@ -1,6 +1,6 @@ name: Potential SAM hive dumping id: 2f326557-0291-4eb1-a87a-7a17b7d941cb -version: 1.0.4 +version: 1.0.5 description: Identifies access to the Security Account Manager registry hives. labels: @@ -70,4 +70,4 @@ condition: > ) | by ps.uuid -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/credential_access_remote_thread_creation_into_lsass.yml b/rules/credential_access_remote_thread_creation_into_lsass.yml index bb974e07b..41e74f904 100644 --- a/rules/credential_access_remote_thread_creation_into_lsass.yml +++ b/rules/credential_access_remote_thread_creation_into_lsass.yml @@ -1,6 +1,6 @@ name: Remote thread creation into LSASS id: e3ce8d6f-c260-48d6-9398-3c1c71726297 -version: 1.0.1 +version: 1.0.2 description: | Identifies the creation of a remote thread in LSASS (Local Security And Authority Subsystem Service) by untrusted or suspicious processes. This may indicate attempts to execute code inside the LSASS process @@ -17,8 +17,8 @@ labels: subtechnique.ref: https://attack.mitre.org/techniques/T1003/001/ condition: > - create_remote_thread and kevt.arg[exe] imatches '?:\\Windows\\System32\\lsass.exe' + create_remote_thread and evt.arg[exe] imatches '?:\\Windows\\System32\\lsass.exe' and (ps.name iin script_interpreters or ps.name ~= 'rundll32.exe' or pe.is_signed = false or pe.is_trusted = false) -min-engine-version: 2.0.0 +min-engine-version: 3.0.0 diff --git a/rules/credential_access_suspicious_access_to_active_directory_domain_database.yml b/rules/credential_access_suspicious_access_to_active_directory_domain_database.yml index 86558a328..0503e0825 100644 --- a/rules/credential_access_suspicious_access_to_active_directory_domain_database.yml +++ b/rules/credential_access_suspicious_access_to_active_directory_domain_database.yml @@ -1,6 +1,6 @@ name: Suspicious access to Active Directory domain database id: a30c100e-28d0-4aa0-b98d-0d38025c2c29 -version: 1.0.2 +version: 1.0.3 description: | Detects suspicious access to the Active Directory domain database. Adversaries may attempt to access or create a copy of the Active Directory @@ -31,4 +31,4 @@ condition: > '?:\\ProgramData\\Microsoft\\Windows Defender\\*\\MsMpEng.exe' ) -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/credential_access_suspicious_access_to_unattended_panther_files.yml b/rules/credential_access_suspicious_access_to_unattended_panther_files.yml index 9413441de..a9868f7f2 100644 --- a/rules/credential_access_suspicious_access_to_unattended_panther_files.yml +++ b/rules/credential_access_suspicious_access_to_unattended_panther_files.yml @@ -1,6 +1,6 @@ name: Suspicious access to Unattended Panther files id: d305fb15-6ad1-4d61-a84b-ada462f23a55 -version: 1.0.2 +version: 1.0.3 description: | Identifies suspicious to access to unattend.xml files where credentials are commonly stored within the Panther directory. Adversaries may search local @@ -34,4 +34,4 @@ condition: > '?:\\ProgramData\\Microsoft\\Windows Defender\\*\\MsMpEng.exe' ) -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/credential_access_suspicious_access_to_windows_dpapi_master_keys.yml b/rules/credential_access_suspicious_access_to_windows_dpapi_master_keys.yml index 723d40107..67b60e95d 100644 --- a/rules/credential_access_suspicious_access_to_windows_dpapi_master_keys.yml +++ b/rules/credential_access_suspicious_access_to_windows_dpapi_master_keys.yml @@ -1,6 +1,6 @@ name: Suspicious access to Windows DPAPI Master Keys id: b1d5732a-5ad4-4cdd-8791-c22e34c591e5 -version: 1.0.2 +version: 1.0.3 description: | Detects suspicious processes accessing the Windows Data Protection API Master keys which is a sign of potential credential stealing. @@ -41,4 +41,4 @@ condition: > '?:\\Windows\\SysWOW64\\*' ) -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/credential_access_suspicious_access_to_windows_manager_files.yml b/rules/credential_access_suspicious_access_to_windows_manager_files.yml index 0d917fb7d..62f9aa076 100644 --- a/rules/credential_access_suspicious_access_to_windows_manager_files.yml +++ b/rules/credential_access_suspicious_access_to_windows_manager_files.yml @@ -1,6 +1,6 @@ name: Suspicious access to Windows Credential Manager files id: 4ab688f7-94e2-481b-9c7f-c49f3a79a379 -version: 1.0.2 +version: 1.0.3 description: | Identifies suspicious processes trying to acquire credentials from the Windows Credential Manager. labels: @@ -30,4 +30,4 @@ condition: > '?:\\Windows\\System32\\lsass.exe' ) -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/credential_access_suspicious_access_to_windows_vault_files.yml b/rules/credential_access_suspicious_access_to_windows_vault_files.yml index 622b878ce..16ac6729f 100644 --- a/rules/credential_access_suspicious_access_to_windows_vault_files.yml +++ b/rules/credential_access_suspicious_access_to_windows_vault_files.yml @@ -1,6 +1,6 @@ name: Suspicious access to Windows Vault files id: 44400221-f98d-424a-9388-497c75b18924 -version: 1.0.2 +version: 1.0.3 description: | Identifies attempts from adversaries to acquire credentials from Vault files. labels: @@ -33,4 +33,4 @@ condition: > '?:\\Windows\\System32\\svchost.exe' ) -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/credential_access_suspicious_security_package_loaded_by_lsass.yml b/rules/credential_access_suspicious_security_package_loaded_by_lsass.yml index 1472acec9..0c6e93b36 100644 --- a/rules/credential_access_suspicious_security_package_loaded_by_lsass.yml +++ b/rules/credential_access_suspicious_security_package_loaded_by_lsass.yml @@ -1,6 +1,6 @@ name: Suspicious security package DLL loaded id: 2c74f176-9a95-4344-a1aa-15aa06e16919 -version: 1.1.1 +version: 1.1.2 description: | Attackers can abuse Windows Security Support Provider and Authentication Packages to dynamically inject a Security Package into the Local Security Authority Subsystem Service @@ -24,4 +24,4 @@ condition: > and (load_unsigned_or_untrusted_module) -min-engine-version: 2.0.0 +min-engine-version: 3.0.0 diff --git a/rules/credential_access_suspicious_vault_client_dll_load.yml b/rules/credential_access_suspicious_vault_client_dll_load.yml index 7681838b0..344308055 100644 --- a/rules/credential_access_suspicious_vault_client_dll_load.yml +++ b/rules/credential_access_suspicious_vault_client_dll_load.yml @@ -1,6 +1,6 @@ name: Suspicious Vault client DLL load id: 64af2e2e-2309-4079-9c0f-985f1dd930f5 -version: 1.0.1 +version: 1.0.2 description: | Identifies loading of the Vault client DLL by an unusual process. Adversaries can abuse the functions provided by the Credential Vault Client Library to enumerate or harvest saved credentials. @@ -59,4 +59,4 @@ output: > Suspicious process %2.ps.exe loaded the Credential Vault Client DLL for potential credentials harvesting severity: high -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/credential_access_unusual_access_to_ssh_keys.yml b/rules/credential_access_unusual_access_to_ssh_keys.yml index cf5159cd4..4dcdf89d1 100644 --- a/rules/credential_access_unusual_access_to_ssh_keys.yml +++ b/rules/credential_access_unusual_access_to_ssh_keys.yml @@ -1,6 +1,6 @@ name: Unusual access to SSH keys id: 90f5c1bd-abd6-4d1b-94e0-229f04473d60 -version: 1.0.3 +version: 1.0.4 description: | Identifies access by unusual process to saved SSH keys. labels: @@ -33,4 +33,4 @@ condition: > 'WinSCP.exe' ) -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/credential_access_unusual_access_to_web_browser_credential_stores.yml b/rules/credential_access_unusual_access_to_web_browser_credential_stores.yml index 6eb093b0b..d9c4af1fb 100644 --- a/rules/credential_access_unusual_access_to_web_browser_credential_stores.yml +++ b/rules/credential_access_unusual_access_to_web_browser_credential_stores.yml @@ -1,6 +1,6 @@ name: Unusual access to Web Browser Credential stores id: 9d889b2b-ca13-4a04-8919-ff1151f23a71 -version: 1.0.2 +version: 1.0.3 description: | Identifies access to Web Browser Credential stores by unusual processes. labels: @@ -29,4 +29,4 @@ condition: > '?:\\ProgramData\\Microsoft\\Windows Defender\\*\\MpCopyAccelerator.exe' ) -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/credential_access_unusual_access_to_windows_credential_history.yml b/rules/credential_access_unusual_access_to_windows_credential_history.yml index e21056612..aa4b3e6c7 100644 --- a/rules/credential_access_unusual_access_to_windows_credential_history.yml +++ b/rules/credential_access_unusual_access_to_windows_credential_history.yml @@ -1,6 +1,6 @@ name: Unusual access to Windows Credential history files id: 9d94062f-2cf3-407c-bd65-4072fe4b167f -version: 1.0.3 +version: 1.0.4 description: | Detects unusual accesses to the Windows Credential history file. The CREDHIST file contains all previous password-linked master key hashes used by @@ -28,4 +28,4 @@ condition: > '?:\\Windows\\ccmcache\\*.exe' ) -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/defense_evasion_appdomain_manager_injection_via_clr_search_order_hijacking.yml b/rules/defense_evasion_appdomain_manager_injection_via_clr_search_order_hijacking.yml index 7effb3613..56372b30e 100644 --- a/rules/defense_evasion_appdomain_manager_injection_via_clr_search_order_hijacking.yml +++ b/rules/defense_evasion_appdomain_manager_injection_via_clr_search_order_hijacking.yml @@ -1,6 +1,6 @@ name: AppDomain Manager injection via CLR search order hijacking id: 9319fafd-b7dc-4d85-b41a-54a8d4f1ab18 -version: 1.0.2 +version: 1.0.4 description: | Adversaries may execute their own malicious payloads by hijacking how the .NET AppDomainManager loads assemblies. The .NET framework uses the AppDomainManager class to create and manage one or more isolated runtime environments @@ -33,4 +33,4 @@ output: > Process %ps.exe loaded untrusted .NET assembly %image.path from suspicious location severity: high -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/defense_evasion_clear_eventlog.yml b/rules/defense_evasion_clear_eventlog.yml index 7adfe1725..c3f999b1f 100644 --- a/rules/defense_evasion_clear_eventlog.yml +++ b/rules/defense_evasion_clear_eventlog.yml @@ -1,6 +1,6 @@ name: Clear Eventlog id: 692d3143-e1fb-4dab-8c9c-3109ff80ec85 -version: 1.0.2 +version: 1.0.3 description: | Identifies attempts to clear Windows event log stores. Adversaries attempt to evade detection or destroy forensic evidence on a system to cover their trails and slow down incident response. @@ -19,11 +19,11 @@ condition: > sequence maxspan 1m by file.object - |set_file_information and kevt.pid != 4 and file.info_class = 'EOF' and file.info.eof_size > 50000 and file.path imatches '?:\\Windows\\System32\\winevt\\Logs\\*.evtx'| + |set_file_information and evt.pid != 4 and file.info_class = 'EOF' and file.info.eof_size > 50000 and file.path imatches '?:\\Windows\\System32\\winevt\\Logs\\*.evtx'| |set_file_information and file.info_class = 'Allocation' and file.info.allocation_size > 50000| output: > Windows Eventlog store %1.file.path was cleared severity: medium -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/defense_evasion_dll_loaded_via_apc_queue.yml b/rules/defense_evasion_dll_loaded_via_apc_queue.yml index 80b0cfdf4..860890144 100644 --- a/rules/defense_evasion_dll_loaded_via_apc_queue.yml +++ b/rules/defense_evasion_dll_loaded_via_apc_queue.yml @@ -1,6 +1,6 @@ name: DLL loaded via APC queue id: e1ee3912-ad7c-4acb-80f4-84db87e54d5e -version: 1.0.1 +version: 1.0.2 description: | Identifies loading of a DLL with a callstack originating from the thread alertable state that led to the execution of an APC routine. This may be @@ -30,4 +30,4 @@ condition: > and thread.callstack.symbols imatches ('KernelBase.dll!Sleep*') -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/defense_evasion_dll_loaded_via_callback_function.yml b/rules/defense_evasion_dll_loaded_via_callback_function.yml index ff5aaf820..d9cbfedbb 100644 --- a/rules/defense_evasion_dll_loaded_via_callback_function.yml +++ b/rules/defense_evasion_dll_loaded_via_callback_function.yml @@ -1,6 +1,6 @@ name: DLL loaded via a callback function id: c7f46d0a-10b2-421a-b33c-f4df79599f2e -version: 1.0.1 +version: 1.0.2 description: | Identifies module proxying as a method to conceal suspicious callstacks. Adversaries use module proxying the hide the origin of the LoadLibrary call from the callstack by loading the library from the callback @@ -39,4 +39,4 @@ output: > %2.image.path loaded from callback function by process %ps.exe severity: high -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/defense_evasion_dll_loaded_via_ldrpkernel32_overwrite.yml b/rules/defense_evasion_dll_loaded_via_ldrpkernel32_overwrite.yml index 7eb785c08..3709248bb 100644 --- a/rules/defense_evasion_dll_loaded_via_ldrpkernel32_overwrite.yml +++ b/rules/defense_evasion_dll_loaded_via_ldrpkernel32_overwrite.yml @@ -1,6 +1,6 @@ name: DLL loaded via LdrpKernel32 overwrite id: 56739eda-210f-4a30-a114-d55ca60976df -version: 1.0.1 +version: 1.0.2 description: | Detects attempts to bypass the standard NTDLL bootstrap process by loading a malicious DLL early through hijacking. The malicious DLL, containing attacker-controlled code, is loaded in place of the legitimate kernel32 DLL. @@ -36,4 +36,4 @@ output: > DLL %image.path loaded via LdrpKernel32 overwrite evasion by process %ps.exe severity: high -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/defense_evasion_dll_sideloading_via_copied_binary.yml b/rules/defense_evasion_dll_sideloading_via_copied_binary.yml index 033a9a306..311b610dc 100644 --- a/rules/defense_evasion_dll_sideloading_via_copied_binary.yml +++ b/rules/defense_evasion_dll_sideloading_via_copied_binary.yml @@ -1,6 +1,6 @@ name: DLL Side-Loading via a copied binary id: 80798e2c-6c37-472b-936c-1d2d6b95ff3c -version: 1.0.2 +version: 1.0.3 description: | Identifies when a binary is copied to a directory and shortly followed by the loading of an unsigned DLL from the same directory. Adversaries may @@ -29,4 +29,4 @@ condition: > (image.signature.type = 'NONE' or image.signature.level = 'UNCHECKED' or image.signature.level = 'UNSIGNED') | by ps.exe -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/defense_evasion_dll_sideloading_via_microsoft_office_dropped_file.yml b/rules/defense_evasion_dll_sideloading_via_microsoft_office_dropped_file.yml index 388357c60..a40bde259 100644 --- a/rules/defense_evasion_dll_sideloading_via_microsoft_office_dropped_file.yml +++ b/rules/defense_evasion_dll_sideloading_via_microsoft_office_dropped_file.yml @@ -1,6 +1,6 @@ name: DLL Side-Loading via Microsoft Office dropped file id: d808175d-c4f8-459d-b17f-ca9a88890c04 -version: 1.0.0 +version: 1.0.1 description: | Identifies Microsoft Office process creating a DLL or other variant of an executable object which is later loaded by a trusted binary. Adversaries may exploit this behavior by delivering malicious @@ -36,4 +36,4 @@ output: > Suspicious DLL %1.file.path dropped by Microsoft Office process %1.ps.exe and subsequently loaded by process %2.ps.exe severity: high -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/defense_evasion_dotnet_assembly_loaded_by_unmanaged_process.yml b/rules/defense_evasion_dotnet_assembly_loaded_by_unmanaged_process.yml index 88f353d77..e6c31313d 100644 --- a/rules/defense_evasion_dotnet_assembly_loaded_by_unmanaged_process.yml +++ b/rules/defense_evasion_dotnet_assembly_loaded_by_unmanaged_process.yml @@ -1,6 +1,6 @@ name: .NET assembly loaded by unmanaged process id: 34be8bd1-1143-4fa8-bed4-ae2566b1394a -version: 1.0.6 +version: 1.0.7 description: | Identifies the loading of the .NET assembly by an unmanaged process. Adversaries can load the CLR runtime inside unmanaged process and execute the assembly via the ICLRRuntimeHost::ExecuteInDefaultAppDomain method. @@ -41,4 +41,4 @@ output: > .NET assembly %image.path loaded by unmanaged process %ps.exe severity: high -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/defense_evasion_hidden_registry_key_creation.yml b/rules/defense_evasion_hidden_registry_key_creation.yml index ac2464ead..45abfebf5 100644 --- a/rules/defense_evasion_hidden_registry_key_creation.yml +++ b/rules/defense_evasion_hidden_registry_key_creation.yml @@ -1,6 +1,6 @@ name: Hidden registry key creation id: 65deda38-9b1d-42a0-9f40-a68903e81b49 -version: 1.1.4 +version: 1.1.5 description: | Identifies the creation of a hidden registry key. Adversaries can utilize the native NtSetValueKey API to create a hidden registry key and conceal payloads @@ -16,7 +16,7 @@ references: - https://github.com/outflanknl/SharpHide condition: > - set_value and kevt.pid != 4 and registry.path endswith '\\' + set_value and evt.pid != 4 and registry.path endswith '\\' and thread.callstack.symbols imatches ('ntdll.dll!NtSetValueKey', 'ntdll.dll!ZwSetValueKey') and @@ -43,4 +43,4 @@ output: > Hidden registry key %registry.path created by process %ps.exe severity: high -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/defense_evasion_image_load_via_ntfs_transaction.yml b/rules/defense_evasion_image_load_via_ntfs_transaction.yml index 3f115ff34..eaf988a13 100644 --- a/rules/defense_evasion_image_load_via_ntfs_transaction.yml +++ b/rules/defense_evasion_image_load_via_ntfs_transaction.yml @@ -1,6 +1,6 @@ name: Image load via NTFS transaction id: ce8de3d0-0768-41a7-bab9-4eca27ed1e3c -version: 1.0.0 +version: 1.0.1 description: | Identifies image loading of a file written to disk via NTFS transaction. Adversaries may exploit the transactional API to execute code in the address space of the running process without committing @@ -19,10 +19,10 @@ condition: > sequence maxspan 2m |create_file and thread.callstack.symbols imatches ('kernel32.dll!CreateFileTransacted*', 'ntdll.dll!RtlSetCurrentTransaction')| by file.name - |load_module and kevt.pid != 4| by image.name + |load_module and evt.pid != 4| by image.name output: > Image %2.image.name written via transactional NTFS and loaded afterward severity: high -min-engine-version: 2.0.0 +min-engine-version: 3.0.0 diff --git a/rules/defense_evasion_potential_injection_via_dotnet_debugging.yml b/rules/defense_evasion_potential_injection_via_dotnet_debugging.yml index 5a7c048d9..99c7a2494 100644 --- a/rules/defense_evasion_potential_injection_via_dotnet_debugging.yml +++ b/rules/defense_evasion_potential_injection_via_dotnet_debugging.yml @@ -1,6 +1,6 @@ name: Potential injection via .NET debugging id: 193ebf2f-e365-4f57-a639-275b7cdf0319 -version: 1.0.2 +version: 1.0.3 description: | Identifies creation of a process on behalf of the CLR debugging facility which may be indicative of code injection. The CLR interface utilizes the OpenVirtualProcess @@ -33,4 +33,4 @@ output: > Process %ps.exe attached the .NET debugger to process %ps.child.exe for potential code injection severity: high -min-engine-version: 2.0.0 +min-engine-version: 3.0.0 diff --git a/rules/defense_evasion_potential_process_creation_via_shellcode.yml b/rules/defense_evasion_potential_process_creation_via_shellcode.yml index f505dc4bf..fa091e184 100644 --- a/rules/defense_evasion_potential_process_creation_via_shellcode.yml +++ b/rules/defense_evasion_potential_process_creation_via_shellcode.yml @@ -1,6 +1,6 @@ name: Potential process creation via shellcode id: 7a918532-12d1-4aa2-8c46-8769c67cac07 -version: 1.0.0 +version: 1.0.1 description: | Identifies the creation of a process with stack frames originating from floating memory area while invoking commonly used Windows API functions like WinExec. This behavior is a typical indicator of @@ -24,4 +24,4 @@ output: > Process %ps.child.exe created via potential shellcode injection by process %ps.exe severity: high -min-engine-version: 2.2.0 +min-engine-version: 3.0.0 diff --git a/rules/defense_evasion_potential_process_doppelganging_injection.yml b/rules/defense_evasion_potential_process_doppelganging_injection.yml index cd7bb866e..aeff0f8f0 100644 --- a/rules/defense_evasion_potential_process_doppelganging_injection.yml +++ b/rules/defense_evasion_potential_process_doppelganging_injection.yml @@ -1,6 +1,6 @@ name: Potential Process Doppelganging id: eb34cf6e-ccc3-4bce-bbcf-013720640a28 -version: 1.0.0 +version: 1.0.1 description: | Adversaries may inject malicious code into process via process doppelganging in order to evade process-based defenses as well as possibly elevate privileges. @@ -56,4 +56,4 @@ condition: > action: - name: kill -min-engine-version: 2.2.0 +min-engine-version: 3.0.0 diff --git a/rules/defense_evasion_potential_process_hollowing_injection.yml b/rules/defense_evasion_potential_process_hollowing_injection.yml index fe31cfb28..744770a62 100644 --- a/rules/defense_evasion_potential_process_hollowing_injection.yml +++ b/rules/defense_evasion_potential_process_hollowing_injection.yml @@ -1,6 +1,6 @@ name: Potential Process Hollowing id: 2a3fbae8-5e8c-4b71-b9da-56c3958c0d53 -version: 1.1.4 +version: 1.1.5 description: | Adversaries may inject malicious code into suspended and hollowed processes in order to evade process-based defenses. Process hollowing is a method of executing arbitrary code @@ -41,4 +41,4 @@ condition: > action: - name: kill -min-engine-version: 2.0.0 +min-engine-version: 3.0.0 diff --git a/rules/defense_evasion_potential_process_injection_via_tainted_memory_section.yml b/rules/defense_evasion_potential_process_injection_via_tainted_memory_section.yml index b57d05593..5f2178685 100644 --- a/rules/defense_evasion_potential_process_injection_via_tainted_memory_section.yml +++ b/rules/defense_evasion_potential_process_injection_via_tainted_memory_section.yml @@ -1,6 +1,6 @@ name: Potential process injection via tainted memory section id: 8e4182f3-02e7-4e95-afc3-93d18c9a9c09 -version: 1.0.3 +version: 1.0.4 description: | Identifies potential process injection when the adversary creates and maps a memory section with RW protection rights followed by mapping of the same memory section in @@ -22,7 +22,7 @@ references: condition: > sequence maxspan 1m - |map_view_of_section and file.view.protection = 'READWRITE' and kevt.pid != 4 and file.view.size >= 4096 and ps.exe not imatches + |map_view_of_section and file.view.protection = 'READWRITE' and evt.pid != 4 and file.view.size >= 4096 and ps.exe not imatches ( '?:\\Program Files\\*.exe', '?:\\Program Files (x86)\\*.exe', @@ -37,7 +37,7 @@ condition: > '?:\\WINDOWS\\System32\\services.exe' ) | as e1 - |map_view_of_section and file.view.protection = 'READONLY|EXECUTE' and file.key = $e1.file.key and kevt.pid != $e1.kevt.pid and ps.exe not imatches + |map_view_of_section and file.view.protection = 'READONLY|EXECUTE' and file.key = $e1.file.key and evt.pid != $e1.evt.pid and ps.exe not imatches ( '?:\\Program Files\\Mozilla Firefox\\firefox.exe', '?:\\Program Files (x86)\\Mozilla Firefox\\firefox.exe' @@ -46,4 +46,4 @@ condition: > action: - name: kill -min-engine-version: 2.2.0 +min-engine-version: 3.0.0 diff --git a/rules/defense_evasion_potential_shellcode_execution_via_etw_logger_thread.yml b/rules/defense_evasion_potential_shellcode_execution_via_etw_logger_thread.yml index e6c26de1f..78c9709ae 100644 --- a/rules/defense_evasion_potential_shellcode_execution_via_etw_logger_thread.yml +++ b/rules/defense_evasion_potential_shellcode_execution_via_etw_logger_thread.yml @@ -1,6 +1,6 @@ name: Potential shellcode execution via ETW logger thread id: 3e915273-5ea0-4576-afc9-b018e2d53545 -version: 1.0.0 +version: 1.0.1 description: | Adversaries may employ the undocumented EtwpCreateEtwThread function to execute shellcode within the local process address space. @@ -16,7 +16,7 @@ references: - https://github.com/Ne0nd0g/go-shellcode/tree/master?tab=readme-ov-file#EtwpCreateEtwThread condition: > - create_thread and kevt.pid != 4 and thread.callstack.symbols iin ('ntdll.dll!EtwpCreateEtwThread') + create_thread and evt.pid != 4 and thread.callstack.symbols iin ('ntdll.dll!EtwpCreateEtwThread') and not (ps.exe imatches @@ -32,4 +32,4 @@ output: > Potential shellcode execution via EtwpCreateEtwThread API initiated by process %ps.exe severity: high -min-engine-version: 2.2.0 +min-engine-version: 3.0.0 diff --git a/rules/defense_evasion_potential_thread_execution_hijacking.yml b/rules/defense_evasion_potential_thread_execution_hijacking.yml index 29ebb0dd9..6017a00af 100644 --- a/rules/defense_evasion_potential_thread_execution_hijacking.yml +++ b/rules/defense_evasion_potential_thread_execution_hijacking.yml @@ -1,6 +1,6 @@ name: Potential thread execution hijacking id: 8b9f6d47-e9ba-4b3a-9da2-d7bf27e08ca9 -version: 1.0.1 +version: 1.0.2 description: | Adversaries may inject malicious code into hijacked processes in order to evade process-based defenses as well as possibly elevate privileges. Thread Execution Hijacking is a method of @@ -40,4 +40,4 @@ condition: > action: - name: kill -min-engine-version: 2.0.0 +min-engine-version: 3.0.0 diff --git a/rules/defense_evasion_process_execution_from_self_deleting_binary.yml b/rules/defense_evasion_process_execution_from_self_deleting_binary.yml index da42abcd4..713a805fd 100644 --- a/rules/defense_evasion_process_execution_from_self_deleting_binary.yml +++ b/rules/defense_evasion_process_execution_from_self_deleting_binary.yml @@ -1,6 +1,6 @@ name: Process execution from a self-deleting binary id: 0f0da517-b22c-4d14-9adc-36baeb621cf7 -version: 1.0.4 +version: 1.0.3 description: | Identifies the execution of the process from a self-deleting binary. The attackers can abuse undocumented API functions to create a process from a file-backed section. The file @@ -43,4 +43,4 @@ output: > Process %2.image.path spawned from self-deleting binary severity: high -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/defense_evasion_process_spawned_via_remote_thread.yml b/rules/defense_evasion_process_spawned_via_remote_thread.yml index 02c59878e..76fd1fcf3 100644 --- a/rules/defense_evasion_process_spawned_via_remote_thread.yml +++ b/rules/defense_evasion_process_spawned_via_remote_thread.yml @@ -1,6 +1,6 @@ name: Process spawned via remote thread id: 9a2c7b40-4e5f-4edf-b02e-79cd33c9a137 -version: 1.0.2 +version: 1.0.3 description: | Identifies the creation of a process with the parent call stack not revealing normal API functions for process creation. This may be a @@ -22,4 +22,4 @@ condition: > action: - name: kill -min-engine-version: 2.2.0 +min-engine-version: 3.0.0 diff --git a/rules/defense_evasion_regsvr32_scriptlet_execution.yml b/rules/defense_evasion_regsvr32_scriptlet_execution.yml index 16942a489..35667033c 100644 --- a/rules/defense_evasion_regsvr32_scriptlet_execution.yml +++ b/rules/defense_evasion_regsvr32_scriptlet_execution.yml @@ -1,6 +1,6 @@ name: Regsvr32 scriptlet execution id: 128f5254-67c9-43ac-b901-18b3731b1d0b -version: 1.0.1 +version: 1.0.2 description: | Identifies the execution of a scriptlet file by regsvr32.exe process. regsvr32.exe allows attackers to run arbitrary scripts to proxy execution of malicious code. @@ -67,4 +67,4 @@ condition: > '?:\\Program Files (x86)\\*.exe' ) -min-engine-version: 2.2.0 +min-engine-version: 3.0.0 diff --git a/rules/defense_evasion_suspicious_access_to_the_hosts_file.yml b/rules/defense_evasion_suspicious_access_to_the_hosts_file.yml index 5d894c9e0..f53ad80fe 100644 --- a/rules/defense_evasion_suspicious_access_to_the_hosts_file.yml +++ b/rules/defense_evasion_suspicious_access_to_the_hosts_file.yml @@ -1,6 +1,6 @@ name: Suspicious access to the hosts file id: f7b2c9d3-99e7-41d5-bb4a-6ea1a5f7f9e2 -version: 1.0.2 +version: 1.0.3 description: > Identifies suspicious process accessing the Windows hosts file for potential tampering. Adversaries can hijack the hosts files to block traffic to download/update servers or redirect the @@ -36,4 +36,4 @@ output: > Suspicious process %1.ps.child.exe accessed the hosts file for potential tampering severity: medium -min-engine-version: 2.2.0 +min-engine-version: 3.0.0 diff --git a/rules/defense_evasion_suspicious_dll_loaded_via_memory_section_mapping.yml b/rules/defense_evasion_suspicious_dll_loaded_via_memory_section_mapping.yml index 5b463ff25..9a5257f91 100644 --- a/rules/defense_evasion_suspicious_dll_loaded_via_memory_section_mapping.yml +++ b/rules/defense_evasion_suspicious_dll_loaded_via_memory_section_mapping.yml @@ -1,6 +1,6 @@ name: Suspicious DLL loaded via memory section mapping id: b06653fb-227e-4e63-9a69-55a5a90c79e5 -version: 1.0.1 +version: 1.0.2 description: | Identifies the mapping of a memory section with RX protection followed by unsigned DLL loading. Adversaries may inject dynamic-link libraries (DLLs) into processes in order to evade process-based defenses @@ -21,7 +21,7 @@ condition: > sequence maxspan 2m by ps.uuid - |map_view_of_section and file.view.protection = 'READONLY|EXECUTE' and kevt.pid != 4 and file.view.size >= 4096 + |map_view_of_section and file.view.protection = 'READONLY|EXECUTE' and evt.pid != 4 and file.view.size >= 4096 and ps.exe not imatches ( @@ -32,4 +32,4 @@ condition: > action: - name: kill -min-engine-version: 2.2.0 +min-engine-version: 3.0.0 diff --git a/rules/defense_evasion_suspicious_html_application_script_execution.yml b/rules/defense_evasion_suspicious_html_application_script_execution.yml index 85b51de9b..81b947740 100644 --- a/rules/defense_evasion_suspicious_html_application_script_execution.yml +++ b/rules/defense_evasion_suspicious_html_application_script_execution.yml @@ -1,6 +1,6 @@ name: Suspicious HTML Application script execution id: 4ec64ac2-851d-41b4-b7d2-910c21de334d -version: 1.0.1 +version: 1.0.2 description: | Identifies the execution of scripts via Microsoft HTML Application Host interpreter. Adversaries can proxy the execution of arbitrary script code through a trusted, signed utility to evade defenses. @@ -60,4 +60,4 @@ output: > Suspicious HTML Application script execution by mshta process with command line arguments %ps.child.cmdline severity: high -min-engine-version: 2.2.0 +min-engine-version: 3.0.0 diff --git a/rules/defense_evasion_suspicious_object_symbolic_link_creation.yml b/rules/defense_evasion_suspicious_object_symbolic_link_creation.yml index 367b49b37..e762c4811 100644 --- a/rules/defense_evasion_suspicious_object_symbolic_link_creation.yml +++ b/rules/defense_evasion_suspicious_object_symbolic_link_creation.yml @@ -1,6 +1,6 @@ name: Suspicious object symbolic link creation id: f9306355-1f5f-4a06-9779-195aa681db80 -version: 1.0.2 +version: 1.0.3 description: | Identifies the creation of the object symbolic link inside the object manager namespace by untrusted or unusual processes. @@ -18,7 +18,7 @@ references: - https://www.elastic.co/kr/blog/detect-block-unknown-knowndlls-windows-acl-hardening-attacks-cache-poisoning-escalation condition: > - create_symbolic_link_object and kevt.pid != 4 + create_symbolic_link_object and evt.pid != 4 and (pe.is_signed = false or pe.is_trusted = false or ps.exe not imatches ( @@ -32,10 +32,10 @@ condition: > ) ) and - kevt.arg[target] not imatches '\\Sessions\\*\\AppContainerNamedObjects\\*' + evt.arg[target] not imatches '\\Sessions\\*\\AppContainerNamedObjects\\*' output: > - Suspicious object symbolic link %kevt.arg[target] created by process %ps.exe + Suspicious object symbolic link %evt.arg[target] created by process %ps.exe severity: high -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/defense_evasion_suspicious_windows_defender_exclusions_registry_modification.yml b/rules/defense_evasion_suspicious_windows_defender_exclusions_registry_modification.yml index 4c85755bd..c6b58d47a 100644 --- a/rules/defense_evasion_suspicious_windows_defender_exclusions_registry_modification.yml +++ b/rules/defense_evasion_suspicious_windows_defender_exclusions_registry_modification.yml @@ -1,6 +1,6 @@ name: Suspicious Windows Defender exclusions registry modification id: 92fdbbea-e177-494e-8a6a-d8b055daf0e9 -version: 1.0.0 +version: 1.0.1 description: | Identifies the modification of the Windows Defender process, path, or IP address registry key exclusions by suspicious processes. Adversaries may alter the Windows Defender exclusions to bypass defenses. @@ -45,4 +45,4 @@ output: > Windows Defender exclusion %registry.path added by suspicious process %ps.exe severity: high -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/defense_evasion_suspicious_xsl_script_execution.yml b/rules/defense_evasion_suspicious_xsl_script_execution.yml index 14e821d8e..fb9e9e170 100644 --- a/rules/defense_evasion_suspicious_xsl_script_execution.yml +++ b/rules/defense_evasion_suspicious_xsl_script_execution.yml @@ -1,6 +1,6 @@ name: Suspicious XSL script execution id: 65136b30-14ae-46dd-b8e5-9dfa99690d74 -version: 1.0.1 +version: 1.0.2 description: | Identifies a suspicious execution of XSL script via Windows Management Instrumentation command line tool or XSL transformation utility. Adversaries may bypass application control and obscure the execution of code by embedding @@ -45,4 +45,4 @@ output: > Suspicious XSL script executed by process %1.ps.child.name with command line arguments %1.ps.child.args severity: high -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/defense_evasion_system_binary_proxy_execution_via_rundll32.yml b/rules/defense_evasion_system_binary_proxy_execution_via_rundll32.yml index 6e96ffd58..714357b33 100644 --- a/rules/defense_evasion_system_binary_proxy_execution_via_rundll32.yml +++ b/rules/defense_evasion_system_binary_proxy_execution_via_rundll32.yml @@ -1,6 +1,6 @@ name: System Binary Proxy Execution via Rundll32 id: 43d76718-cc46-485e-8f47-996eb7a9f83b -version: 1.0.1 +version: 1.0.2 description: | Detects the execution of rundll32.exe process with suspicious command line followed by the creation of a possibly malicious child process. @@ -63,4 +63,4 @@ condition: > action: - name: kill -min-engine-version: 2.0.0 +min-engine-version: 3.0.0 diff --git a/rules/defense_evasion_thread_context_set_from_unbacked_memory.yml b/rules/defense_evasion_thread_context_set_from_unbacked_memory.yml index 35de509a8..f56f4ca00 100644 --- a/rules/defense_evasion_thread_context_set_from_unbacked_memory.yml +++ b/rules/defense_evasion_thread_context_set_from_unbacked_memory.yml @@ -1,6 +1,6 @@ name: Thread context set from unbacked memory id: f8219274-ee68-416b-8489-4d2e635c7844 -version: 1.0.3 +version: 1.0.4 description: | Identifies manipulation of the thread context from unbacked memory region. This may be indicative of process injection. @@ -23,4 +23,4 @@ condition: > '?:\\Windows\\System32\\taskhostw.exe' ) -min-engine-version: 2.2.0 +min-engine-version: 3.0.0 diff --git a/rules/defense_evasion_unsigned_dll_injection_via_remote_thread.yml b/rules/defense_evasion_unsigned_dll_injection_via_remote_thread.yml index fd27e6af3..90be228aa 100644 --- a/rules/defense_evasion_unsigned_dll_injection_via_remote_thread.yml +++ b/rules/defense_evasion_unsigned_dll_injection_via_remote_thread.yml @@ -1,6 +1,6 @@ name: Unsigned DLL injection via remote thread id: 21bdd944-3bda-464b-9a72-58fd37ba9163 -version: 1.1.2 +version: 1.1.3 description: | Identifies unsigned DLL injection via remote thread creation. Adversaries may inject dynamic-link libraries (DLLs) into processes in order to evade process-based defenses @@ -46,4 +46,4 @@ condition: > ps.exe not imatches '?:\\Program Files\\Common Files\\microsoft shared\\ClickToRun\\Updates\\*\\OfficeClickToRun.exe' | by ps.pid -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/defense_evasion_windows_defender_protection_tampering_via_registry.yml b/rules/defense_evasion_windows_defender_protection_tampering_via_registry.yml index 615dad6c4..e7df57e36 100644 --- a/rules/defense_evasion_windows_defender_protection_tampering_via_registry.yml +++ b/rules/defense_evasion_windows_defender_protection_tampering_via_registry.yml @@ -1,6 +1,6 @@ name: Windows Defender protection tampering via registry id: 47ad962b-be0f-44f8-9467-34109f41e5ff -version: 1.0.0 +version: 1.0.1 description: | Detects suspicious processes modifying Windows Defender configuration settings via registry to disable protection features. @@ -62,4 +62,4 @@ output: > Suspicious process %ps.exe tampered Windows Defender security settings in registry value %registry.path severity: high -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/initial_access_executable_file_creation_from_macro_enabled_microsoft_office_document.yml b/rules/initial_access_executable_file_creation_from_macro_enabled_microsoft_office_document.yml index 939e2f389..7df2d39e9 100644 --- a/rules/initial_access_executable_file_creation_from_macro_enabled_microsoft_office_document.yml +++ b/rules/initial_access_executable_file_creation_from_macro_enabled_microsoft_office_document.yml @@ -1,6 +1,6 @@ name: Executable file creation from a macro-enabled Microsoft Office document id: fffcce75-2427-406e-9597-1f49b0c9ad5b -version: 1.0.1 +version: 1.0.2 description: | Identifies the Microsoft Office process writing an executable file type and the call stack reveals the file creation was originated from the Microsoft @@ -31,4 +31,4 @@ condition: > (file.is_exec or file.is_dll) ) -min-engine-version: 2.2.0 +min-engine-version: 3.0.0 diff --git a/rules/initial_access_execution_via_microsoft_office_process.yml b/rules/initial_access_execution_via_microsoft_office_process.yml index d5b166c1d..e8c30ba99 100644 --- a/rules/initial_access_execution_via_microsoft_office_process.yml +++ b/rules/initial_access_execution_via_microsoft_office_process.yml @@ -1,6 +1,6 @@ name: Execution via Microsoft Office process id: a10ebe66-1b55-4005-a374-840f1e2933a3 -version: 1.0.1 +version: 1.0.2 description: Identifies the execution of the file dropped by Microsoft Office process. labels: @@ -20,4 +20,4 @@ condition: > |create_file and (file.extension iin executable_extensions or file.is_exec) and ps.name iin msoffice_binaries| by file.path |spawn_process and ps.name iin msoffice_binaries| by ps.child.exe -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/initial_access_macro_execution_via_script_interpreter.yml b/rules/initial_access_macro_execution_via_script_interpreter.yml index c14a6f2e4..3bff927a8 100644 --- a/rules/initial_access_macro_execution_via_script_interpreter.yml +++ b/rules/initial_access_macro_execution_via_script_interpreter.yml @@ -1,6 +1,6 @@ name: Macro execution via script interpreter id: 845404de-df6f-472f-bd74-72148a7f5166 -version: 1.0.3 +version: 1.0.4 description: | Identifies the execution of the Windows scripting interpreter spawning a Microsoft Office process to execute suspicious Visual Basic macro. @@ -27,4 +27,4 @@ condition: > ) | by ps.uuid -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/initial_access_microsoft_office_file_execution_via_script_interpreter.yml b/rules/initial_access_microsoft_office_file_execution_via_script_interpreter.yml index 9db167e2f..6b4edea27 100644 --- a/rules/initial_access_microsoft_office_file_execution_via_script_interpreter.yml +++ b/rules/initial_access_microsoft_office_file_execution_via_script_interpreter.yml @@ -1,6 +1,6 @@ name: Microsoft Office file execution via script interpreter id: bf3ea547-1470-4bcc-9945-3b495d962c2c -version: 1.0.0 +version: 1.0.1 description: | Identifies the execution via Windows script interpreter of the executable file written by the Microsoft Office process. @@ -32,4 +32,4 @@ output: > Microsoft Office process %1.ps.exe wrote the file %1.file.path and subsequently executed it via script interpreter %2.ps.exe severity: high -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/initial_access_microsoft_office_file_execution_via_wmi.yml b/rules/initial_access_microsoft_office_file_execution_via_wmi.yml index 10e36a4ce..35cb21c15 100644 --- a/rules/initial_access_microsoft_office_file_execution_via_wmi.yml +++ b/rules/initial_access_microsoft_office_file_execution_via_wmi.yml @@ -1,6 +1,6 @@ name: Microsoft Office file execution via WMI id: 50f6efa2-4d7b-4fb7-b1a9-65c3a24d9152 -version: 1.0.0 +version: 1.0.1 description: | Identifies the execution via Windows Management Instrumentation (WMI) of the binary file written by the Microsoft Office process. Attackers can exploit WMI to silently execute malicious code. @@ -29,4 +29,4 @@ output: > Microsoft Office process %1.ps.exe wrote the file %1.file.path and subsequently executed it via WMI severity: high -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/initial_access_potential_clickfix_infection_chain_via_run_window.yml b/rules/initial_access_potential_clickfix_infection_chain_via_run_window.yml index f13bf04ec..65ef03f2d 100644 --- a/rules/initial_access_potential_clickfix_infection_chain_via_run_window.yml +++ b/rules/initial_access_potential_clickfix_infection_chain_via_run_window.yml @@ -1,6 +1,6 @@ name: Potential ClickFix infection chain via Run window id: ffe1fc54-2893-4760-ab50-51a83bd71d13 -version: 1.0.1 +version: 1.0.2 description: | Identifies the execution of the process via the Run command dialog box followed by spawning of the potential infostealer process. @@ -46,4 +46,4 @@ output: > Potential infostealer process %2.ps.child.exe executed via the Run command window by %1.ps.child.cmdline severity: high -min-engine-version: 2.2.0 +min-engine-version: 3.0.0 diff --git a/rules/initial_access_process_spawned_from_macro_enabled_microsoft_office_document.yml b/rules/initial_access_process_spawned_from_macro_enabled_microsoft_office_document.yml index 82fc9d9eb..1c3a4e573 100644 --- a/rules/initial_access_process_spawned_from_macro_enabled_microsoft_office_document.yml +++ b/rules/initial_access_process_spawned_from_macro_enabled_microsoft_office_document.yml @@ -1,6 +1,6 @@ name: Process spawned from macro-enabled Microsoft Office document id: 47521206-e19d-4608-9dbc-dc3a1df99db5 -version: 1.0.2 +version: 1.0.3 description: | Identifies the execution of the child process spawned by Microsoft Office parent process where the call stack contains the Visual Basic @@ -41,4 +41,4 @@ condition: > '?:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe' ) -min-engine-version: 2.2.0 +min-engine-version: 3.0.0 diff --git a/rules/initial_access_suspicious_dll_loaded_by_microsoft_office_process.yml b/rules/initial_access_suspicious_dll_loaded_by_microsoft_office_process.yml index 1c58c599f..93324020f 100644 --- a/rules/initial_access_suspicious_dll_loaded_by_microsoft_office_process.yml +++ b/rules/initial_access_suspicious_dll_loaded_by_microsoft_office_process.yml @@ -1,6 +1,6 @@ name: Suspicious DLL loaded by Microsoft Office process id: 5868518c-2a83-4b26-ad4b-f14f0b85e744 -version: 1.0.1 +version: 1.0.2 description: Identifies loading of recently dropped DLL by Microsoft Office process. labels: @@ -23,4 +23,4 @@ condition: > | by file.name |load_module and ps.name iin msoffice_binaries| by image.name -min-engine-version: 2.0.0 +min-engine-version: 3.0.0 diff --git a/rules/initial_access_suspicious_execution_via_wmi_from_microsoft_office_process.yml b/rules/initial_access_suspicious_execution_via_wmi_from_microsoft_office_process.yml index 2fafa2365..ff97c4942 100644 --- a/rules/initial_access_suspicious_execution_via_wmi_from_microsoft_office_process.yml +++ b/rules/initial_access_suspicious_execution_via_wmi_from_microsoft_office_process.yml @@ -1,6 +1,6 @@ name: Suspicious execution via WMI from a Microsoft Office process id: cc3f0bbe-ec53-40a7-9eed-f0a8a3f7d7fa -version: 1.0.0 +version: 1.0.1 description: | Identifies a suspicious process execution via Windows Management Instrumentation (WMI) originated from the Microsoft Office process loading an unusual WMI DLL. This technique @@ -91,4 +91,4 @@ output: > Suspicious process %2.ps.child.exe launched via WMI from Microsoft Office process %1.ps.cmdline severity: high -min-engine-version: 2.2.0 +min-engine-version: 3.0.0 diff --git a/rules/initial_access_suspicious_microsoft_office_embedded_object.yml b/rules/initial_access_suspicious_microsoft_office_embedded_object.yml index 724e262c4..2e4fc722e 100644 --- a/rules/initial_access_suspicious_microsoft_office_embedded_object.yml +++ b/rules/initial_access_suspicious_microsoft_office_embedded_object.yml @@ -1,6 +1,6 @@ name: Suspicious Microsoft Office embedded object id: 47368d49-1192-4059-9c55-6bbc4fd1a73a -version: 1.0.1 +version: 1.0.2 description: | Identifies Microsoft Office processes dropping a file with suspicious extension and with the call stack indicating operations to save or load @@ -27,4 +27,4 @@ condition: > (file.is_exec or file.is_dll) ) -min-engine-version: 2.2.0 +min-engine-version: 3.0.0 diff --git a/rules/macros/macros.yml b/rules/macros/macros.yml index 5bb9d4768..d0e70d2a4 100644 --- a/rules/macros/macros.yml +++ b/rules/macros/macros.yml @@ -1,89 +1,89 @@ - macro: spawn_process - expr: kevt.name = 'CreateProcess' + expr: evt.name = 'CreateProcess' - macro: create_thread - expr: kevt.name = 'CreateThread' + expr: evt.name = 'CreateThread' - macro: create_remote_thread - expr: create_thread and kevt.pid != 4 and kevt.pid != thread.pid + expr: create_thread and evt.pid != 4 and evt.pid != thread.pid - macro: open_process - expr: kevt.name = 'OpenProcess' and ps.access.status = 'Success' + expr: evt.name = 'OpenProcess' and ps.access.status = 'Success' - macro: open_thread - expr: kevt.name = 'OpenThread' and thread.access.status = 'Success' + expr: evt.name = 'OpenThread' and thread.access.status = 'Success' - macro: open_remote_thread - expr: open_thread and kevt.pid != 4 and kevt.pid != kevt.arg[pid] + expr: open_thread and evt.pid != 4 and evt.pid != evt.arg[pid] - macro: write_file - expr: kevt.name = 'WriteFile' + expr: evt.name = 'WriteFile' - macro: open_file - expr: kevt.name = 'CreateFile' and file.operation = 'OPEN' and file.status = 'Success' + expr: evt.name = 'CreateFile' and file.operation = 'OPEN' and file.status = 'Success' - macro: create_file - expr: kevt.name = 'CreateFile' and file.operation != 'OPEN' and file.status = 'Success' + expr: evt.name = 'CreateFile' and file.operation != 'OPEN' and file.status = 'Success' - macro: rename_file - expr: kevt.name = 'RenameFile' + expr: evt.name = 'RenameFile' - macro: read_file - expr: kevt.name = 'ReadFile' + expr: evt.name = 'ReadFile' - macro: delete_file - expr: kevt.name = 'DeleteFile' + expr: evt.name = 'DeleteFile' - macro: set_file_information - expr: kevt.name = 'SetFileInformation' + expr: evt.name = 'SetFileInformation' - macro: query_registry - expr: kevt.name in ('RegQueryKey', 'RegQueryValue') and registry.status = 'Success' + expr: evt.name in ('RegQueryKey', 'RegQueryValue') and registry.status = 'Success' - macro: open_registry - expr: kevt.name = 'RegOpenKey' and registry.status = 'Success' + expr: evt.name = 'RegOpenKey' and registry.status = 'Success' - macro: load_module - expr: kevt.name = 'LoadImage' + expr: evt.name = 'LoadImage' - macro: unload_module - expr: kevt.name = 'UnloadImage' + expr: evt.name = 'UnloadImage' - macro: set_value - expr: kevt.name = 'RegSetValue' and registry.status = 'Success' + expr: evt.name = 'RegSetValue' and registry.status = 'Success' - macro: create_key - expr: kevt.name = 'RegCreateKey' and registry.status = 'Success' + expr: evt.name = 'RegCreateKey' and registry.status = 'Success' - macro: modify_registry expr: ((set_value) or (create_key)) - macro: send_socket - expr: kevt.name = 'Send' + expr: evt.name = 'Send' - macro: recv_socket - expr: kevt.name = 'Recv' + expr: evt.name = 'Recv' - macro: connect_socket - expr: kevt.name = 'Connect' + expr: evt.name = 'Connect' - macro: accept_socket - expr: kevt.name = 'Accept' + expr: evt.name = 'Accept' - macro: set_thread_context - expr: kevt.name = 'SetThreadContext' and kevt.arg[status] = 'Success' + expr: evt.name = 'SetThreadContext' and evt.arg[status] = 'Success' - macro: virtual_alloc - expr: kevt.name = 'VirtualAlloc' + expr: evt.name = 'VirtualAlloc' - macro: virtual_free - expr: kevt.name = 'VirtualFree' + expr: evt.name = 'VirtualFree' - macro: map_view_file - expr: kevt.name = 'MapViewFile' + expr: evt.name = 'MapViewFile' - macro: unmap_view_file - expr: kevt.name = 'UnmapViewFile' + expr: evt.name = 'UnmapViewFile' - macro: map_view_of_section expr: map_view_file and file.view.type in ('IMAGE', 'IMAGE_NO_EXECUTE', 'PAGEFILE') @@ -92,19 +92,19 @@ expr: unmap_view_file and file.view.type in ('IMAGE', 'IMAGE_NO_EXECUTE') - macro: duplicate_handle - expr: kevt.name = 'DuplicateHandle' + expr: evt.name = 'DuplicateHandle' - macro: create_handle - expr: kevt.name = 'CreateHandle' + expr: evt.name = 'CreateHandle' - macro: query_dns - expr: kevt.name = 'QueryDns' + expr: evt.name = 'QueryDns' - macro: reply_dns - expr: kevt.name = 'ReplyDns' + expr: evt.name = 'ReplyDns' - macro: create_symbolic_link_object - expr: kevt.name = 'CreateSymbolicLinkObject' and kevt.arg[status] = 'Success' + expr: evt.name = 'CreateSymbolicLinkObject' and evt.arg[status] = 'Success' - macro: inbound_network expr: > diff --git a/rules/persistence_executable_file_dropped_by_unsigned_service_dll.yml b/rules/persistence_executable_file_dropped_by_unsigned_service_dll.yml index 14909f4fc..0334bbbea 100644 --- a/rules/persistence_executable_file_dropped_by_unsigned_service_dll.yml +++ b/rules/persistence_executable_file_dropped_by_unsigned_service_dll.yml @@ -1,6 +1,6 @@ name: Executable file dropped by an unsigned service DLL id: 3e29da58-0fc4-44c0-91c0-0dfc6af87e9d -version: 1.0.0 +version: 1.0.1 description: | Identifies the loading of an unsigned DLL by svchost process followed by creating an executable file. Adversaries may rely on Windows Services to repeatedly execute malicious @@ -23,7 +23,7 @@ condition: > sequence maxspan 3m |load_unsigned_dll and ps.exe imatches ('?:\\Windows\\System32\\svchost.exe', '?:\\Windows\\SysWOW64\\svchost.exe')| as e1 - |create_file and kevt.pid != 4 and ps.exe imatches ('?:\\Windows\\System32\\svchost.exe', '?:\\Windows\\SysWOW64\\svchost.exe') + |create_file and evt.pid != 4 and ps.exe imatches ('?:\\Windows\\System32\\svchost.exe', '?:\\Windows\\SysWOW64\\svchost.exe') and (file.extension iin ('.exe', '.dll', '.com', '.js', '.vbs', '.cmd', '.bat', '.vbe') or file.is_exec or file.is_dll or file.is_driver) and @@ -34,4 +34,4 @@ output: > Service %1.ps.cmdline loaded an unsigned DLL %1.image.path and subsequently dropped an executable file %2.file.path severity: high -min-engine-version: 2.2.0 +min-engine-version: 3.0.0 diff --git a/rules/persistence_hidden_local_account_creation.yml b/rules/persistence_hidden_local_account_creation.yml index 67bdd53d5..861ff93af 100644 --- a/rules/persistence_hidden_local_account_creation.yml +++ b/rules/persistence_hidden_local_account_creation.yml @@ -1,6 +1,6 @@ name: Hidden local account creation id: bfa83754-3730-4c46-a0fd-cc71365f64df -version: 1.0.1 +version: 1.0.2 description: | Identifies the creation of a hidden local account. Adversaries can create hidden accounts by appending the dollar sign to the account name. This technique renders the account name hidden @@ -25,4 +25,4 @@ condition: > severity: high -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/persistence_network_connection_via_startup_folder_executable_or_script.yml b/rules/persistence_network_connection_via_startup_folder_executable_or_script.yml index e737ddcc1..742eaeb49 100644 --- a/rules/persistence_network_connection_via_startup_folder_executable_or_script.yml +++ b/rules/persistence_network_connection_via_startup_folder_executable_or_script.yml @@ -1,6 +1,6 @@ name: Network connection via startup folder executable or script id: 09b7278d-42e3-4792-9f00-dee38baecfad -version: 1.0.2 +version: 1.0.3 description: | Identifies the execution of unsigned binary or script from the Startup folder followed by network inbound or outbound connection. @@ -26,4 +26,4 @@ condition: > | |((inbound_network) or (outbound_network)) and ps.cmdline imatches startup_locations| -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/persistence_potential_port_monitor_or_print_processor_persistence_via_registry_modification.yml b/rules/persistence_potential_port_monitor_or_print_processor_persistence_via_registry_modification.yml index 86b3aeab4..a038d86aa 100644 --- a/rules/persistence_potential_port_monitor_or_print_processor_persistence_via_registry_modification.yml +++ b/rules/persistence_potential_port_monitor_or_print_processor_persistence_via_registry_modification.yml @@ -1,6 +1,6 @@ name: Potential port monitor or print processor persistence via registry modification id: de04ae6b-8141-41af-9baa-15630b5954cc -version: 1.0.0 +version: 1.0.1 description: | Identifies port monitor or print process registry modifications that would allow adversaries to run malicious DLLs during system boot. @@ -30,4 +30,4 @@ output: > Port monitor or print processor DLL registered under registry key %registry.path by process %ps.exe severity: high -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/persistence_rid_hijacking.yml b/rules/persistence_rid_hijacking.yml index 61bc5883a..f92afbfd6 100644 --- a/rules/persistence_rid_hijacking.yml +++ b/rules/persistence_rid_hijacking.yml @@ -1,6 +1,6 @@ name: RID Hijacking id: 5c25666a-4a9f-4b7c-b02f-db0b5cdbde83 -version: 1.0.2 +version: 1.0.3 description: | RID (Relative ID part of security identifier) hijacking allows an attacker with SYSTEM level privileges to covertly replace the RID of a low privileged account effectively making @@ -23,4 +23,4 @@ condition: > and ps.exe not imatches '?:\\Windows\\System32\\lsass.exe' -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/persistence_script_interpreter_or_untrusted_process_persistence.yml b/rules/persistence_script_interpreter_or_untrusted_process_persistence.yml index eab21d60b..116f84632 100644 --- a/rules/persistence_script_interpreter_or_untrusted_process_persistence.yml +++ b/rules/persistence_script_interpreter_or_untrusted_process_persistence.yml @@ -1,6 +1,6 @@ name: Script interpreter host or untrusted process persistence id: cc41ee3a-6e44-4903-85a4-0147ec6a7eea -version: 1.1.0 +version: 1.1.1 description: | Identifies the script interpreter or untrusted process writing to commonly abused run keys or the Startup folder locations. @@ -16,7 +16,7 @@ labels: subtechnique.ref: https://attack.mitre.org/techniques/T1547/001/ condition: > - (((modify_registry) or (create_file)) and kevt.pid != 4) + (((modify_registry) or (create_file)) and evt.pid != 4) and (ps.name in script_interpreters or ps.parent.name in script_interpreters or pe.is_trusted = false) and @@ -46,4 +46,4 @@ condition: > action: - name: kill -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/persistence_suspicious_microsoft_office_addin_loaded.yml b/rules/persistence_suspicious_microsoft_office_addin_loaded.yml index 43eaead94..a477e205a 100644 --- a/rules/persistence_suspicious_microsoft_office_addin_loaded.yml +++ b/rules/persistence_suspicious_microsoft_office_addin_loaded.yml @@ -1,6 +1,6 @@ name: Suspicious Microsoft Office add-in loaded id: fe4daff8-d8aa-48d3-bf09-a9d868375a3c -version: 1.0.0 +version: 1.0.1 description: | Identifies attempts to load unsigned executables from known Microsoft Office add-ins directories, which adversaries may exploit to maintain persistence. @@ -29,4 +29,4 @@ output: Microsoft Office process %ps.name loaded a suspicious add-in %image.path severity: high -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/persistence_suspicious_microsoft_office_template.yml b/rules/persistence_suspicious_microsoft_office_template.yml index 4d5ab823b..d81f77fcb 100644 --- a/rules/persistence_suspicious_microsoft_office_template.yml +++ b/rules/persistence_suspicious_microsoft_office_template.yml @@ -1,6 +1,6 @@ name: Suspicious Microsoft Office template id: c4be3b30-9d23-4a33-b974-fb12e17487a2 -version: 1.0.2 +version: 1.0.3 description: | Detects when attackers drop macro-enabled files in specific folders to trigger their execution every time the victim user @@ -41,4 +41,4 @@ condition: > output: > Office template %file.path created by suspicious process %ps.exe -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/persistence_suspicious_netsh_helper_dll_execution.yml b/rules/persistence_suspicious_netsh_helper_dll_execution.yml index c61f1b686..af960b681 100644 --- a/rules/persistence_suspicious_netsh_helper_dll_execution.yml +++ b/rules/persistence_suspicious_netsh_helper_dll_execution.yml @@ -1,6 +1,6 @@ name: Suspicious Netsh Helper DLL execution id: bd17781d-38ca-4b9a-a12a-f807a1eb45e0 -version: 1.0.0 +version: 1.0.1 description: | Identifies the execution of a suspicious Netsh Helper DLL. Adversaries may establish persistence by executing malicious content triggered by Netsh Helper DLLs. Netsh.exe is a command-line scripting @@ -32,4 +32,4 @@ output: > Suspicious Netsh Helper DLL %2.thread.start_address.module executed severity: high -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/persistence_suspicious_persistence_via_registry_modification.yml b/rules/persistence_suspicious_persistence_via_registry_modification.yml index 8a2e3f692..4a15b2a3e 100644 --- a/rules/persistence_suspicious_persistence_via_registry_modification.yml +++ b/rules/persistence_suspicious_persistence_via_registry_modification.yml @@ -1,6 +1,6 @@ name: Suspicious persistence via registry modification id: 1f496a17-4f0c-491a-823b-7a70adb9919c -version: 1.0.2 +version: 1.0.3 description: | Adversaries may abuse the registry to achieve persistence by modifying the keys that are unlikely modified by legitimate @@ -29,4 +29,4 @@ condition: > and registry.path imatches registry_persistence_keys -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/persistence_suspicious_port_monitor_loaded.yml b/rules/persistence_suspicious_port_monitor_loaded.yml index f0bb210f4..8e3b4cffc 100644 --- a/rules/persistence_suspicious_port_monitor_loaded.yml +++ b/rules/persistence_suspicious_port_monitor_loaded.yml @@ -1,6 +1,6 @@ name: Suspicious port monitor loaded id: d6ab6bfa-1a97-46cb-a69a-7a6c98a699f1 -version: 1.0.1 +version: 1.0.2 description: | Identifies the loading of an unsigned DLL by the print spool service. Adversaries may use port monitors to run an adversary supplied DLL during system boot for persistence or privilege escalation. @@ -22,4 +22,4 @@ condition: > and thread.callstack.symbols imatches ('localspl.dll!SplAddMonitor*', 'spoolsv.exe!PrvAddMonitor*') -min-engine-version: 2.2.0 +min-engine-version: 3.0.0 diff --git a/rules/persistence_suspicious_print_processor_loaded.yml b/rules/persistence_suspicious_print_processor_loaded.yml index 7f67a9e21..643d8475c 100644 --- a/rules/persistence_suspicious_print_processor_loaded.yml +++ b/rules/persistence_suspicious_print_processor_loaded.yml @@ -1,6 +1,6 @@ name: Suspicious print processor loaded id: 3e0f5ef7-8a0a-4604-b2bf-d09606f45483 -version: 1.0.0 +version: 1.0.1 description: | Identifies when the print spooler service loads unsigned or untrusted DLL and the callstack pattern indicates the print processor is loaded. Adversaries may abuse print processors to run malicious DLLs @@ -29,4 +29,4 @@ output: > Print spooler service loaded suspicious print processor DLL %image.path severity: high -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/persistence_suspicious_startup_shell_folder_modification.yml b/rules/persistence_suspicious_startup_shell_folder_modification.yml index a864901df..92dfde71b 100644 --- a/rules/persistence_suspicious_startup_shell_folder_modification.yml +++ b/rules/persistence_suspicious_startup_shell_folder_modification.yml @@ -1,6 +1,6 @@ name: Suspicious Startup shell folder modification id: 7a4082f6-f7e3-49bd-9514-dbc8dd4e68ad -version: 1.0.2 +version: 1.0.3 description: | Detects when adversaries attempt to modify the default Startup folder path to to circumvent runtime rules that hunt for file @@ -26,4 +26,4 @@ condition: > registry.value imatches ('%ProgramData%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup') ) -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/persistence_unusual_file_written_in_startup_folder.yml b/rules/persistence_unusual_file_written_in_startup_folder.yml index 228df0891..6faa930e8 100644 --- a/rules/persistence_unusual_file_written_in_startup_folder.yml +++ b/rules/persistence_unusual_file_written_in_startup_folder.yml @@ -1,6 +1,6 @@ name: Unusual file written in Startup folder id: c5ffe15c-d94f-416b-bec7-c47f89843267 -version: 1.0.2 +version: 1.0.3 description: | Identifies suspicious files written to the startup folder that would allow adversaries to maintain persistence on the endpoint. @@ -35,4 +35,4 @@ condition: > '?:\\ProgramData\\Microsoft\\Windows Defender\\Platform\\*.exe' ) -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/persistence_unusual_process_modified_registry_run_key.yml b/rules/persistence_unusual_process_modified_registry_run_key.yml index 5b99617d6..b7d4d54a2 100644 --- a/rules/persistence_unusual_process_modified_registry_run_key.yml +++ b/rules/persistence_unusual_process_modified_registry_run_key.yml @@ -1,6 +1,6 @@ name: Unusual process modified registry run key id: 921508a5-b627-4c02-a295-6c6863c0897b -version: 1.0.4 +version: 1.0.5 description: | Identifies an attempt by unusual Windows native processes to modify the run key and gain persistence on users logons or machine reboots. @@ -44,4 +44,4 @@ condition: > '?:\\Windows\\System32\\CompatTelRunner.exe' ) -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/privilege_escalation_potential_privilege_escalation_via_phantom_dll_hijacking.yml b/rules/privilege_escalation_potential_privilege_escalation_via_phantom_dll_hijacking.yml index ca6f2add3..222d7e9f9 100644 --- a/rules/privilege_escalation_potential_privilege_escalation_via_phantom_dll_hijacking.yml +++ b/rules/privilege_escalation_potential_privilege_escalation_via_phantom_dll_hijacking.yml @@ -1,6 +1,6 @@ name: Potential privilege escalation via phantom DLL hijacking id: 5ccdb5c2-3a30-4e14-87d2-d7aeb4c45fad -version: 1.0.3 +version: 1.0.4 description: | Identifies the loading of the phantom DLL that was previously dropped to the System directory. Adversaries may exploit this flow to escalate @@ -27,7 +27,7 @@ references: condition: > sequence maxspan 10m - |create_file and kevt.pid != 4 and file.path imatches + |create_file and evt.pid != 4 and file.path imatches ( '?:\\Windows\\System32\\wow64log.dll', '?:\\Windows\\wbemcomn.dll', @@ -58,4 +58,4 @@ condition: > | by file.path |load_dll| by image.path -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/privilege_escalation_vulnerable_or_malicious_driver_dropped.yml b/rules/privilege_escalation_vulnerable_or_malicious_driver_dropped.yml index c7198e1a4..e76c23c9e 100644 --- a/rules/privilege_escalation_vulnerable_or_malicious_driver_dropped.yml +++ b/rules/privilege_escalation_vulnerable_or_malicious_driver_dropped.yml @@ -1,6 +1,6 @@ name: Vulnerable or malicious driver dropped id: d4742163-cf68-4ebd-b9a2-3ad17bbf63d5 -version: 1.0.1 +version: 1.0.2 description: | Detects when adversaries drop a vulnerable/malicious driver onto a compromised system as a preparation for vulnerability @@ -23,4 +23,4 @@ condition: > output: > Vulnerable or malicious %file.path driver dropped -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 diff --git a/rules/privilege_escalation_vulnerable_or_malicious_driver_loaded.yml b/rules/privilege_escalation_vulnerable_or_malicious_driver_loaded.yml index c5db94e30..e2e660f0b 100644 --- a/rules/privilege_escalation_vulnerable_or_malicious_driver_loaded.yml +++ b/rules/privilege_escalation_vulnerable_or_malicious_driver_loaded.yml @@ -1,6 +1,6 @@ name: Vulnerable or malicious driver loaded id: e8005f1d-b4ec-45ee-a3ea-4247eac123db -version: 1.0.1 +version: 1.0.2 description: | Detects when adversaries load a vulnerable/malicious driver into the compromised system to exploit the vulnerability and @@ -23,4 +23,4 @@ condition: > output: > Vulnerable or malicious %image.path driver loaded -min-engine-version: 2.4.0 +min-engine-version: 3.0.0 From 3af6696a3a294b7b41f06919fcab1dd156573d38 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Fri, 30 May 2025 19:18:08 +0200 Subject: [PATCH 05/42] refactor(docs): Use smaller logo image --- docs/_coverpage.md | 2 +- docs/logo.png | Bin 330721 -> 268509 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_coverpage.md b/docs/_coverpage.md index 80dfd04dc..f8132ec34 100755 --- a/docs/_coverpage.md +++ b/docs/_coverpage.md @@ -1,7 +1,7 @@
- +
# fibratus 2.4.0 diff --git a/docs/logo.png b/docs/logo.png index 715f8fa051c31066bf8c5985d54ffb4f5f93f4d9..be4a37902a34a32c40fbc3979eced744c05a135e 100644 GIT binary patch literal 268509 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]wFH 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%0D$dB;woE*0iyApq)C#KO3S*M@| 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

nbtBLF3=BRNr`?t)TGiw68 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 literal 330721 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%0DbQ0kr6CT17 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⪼`YS-Gk&uoTb;GkyQwI_4a_@TRo1#z>*wP}igNU5d>~rUz zHp~++{1*(9IYv=cv-@+faT0q#a|+#f23iT`q{@<2OK?#~am)pWNSD`l6DQsKP#%vq zKM)?zIFJX;mu{2A^jL3j3d3|sT4$jEu)V^1y6Nby2x|wa(*i;_kuXbNtC@3OJ zrvY!2se3fF(lw+;isX&?v#T2aaN;YFn9mq z2*71{8xSB?S6}rmpQb!`!-L4|4b8?(bXjCvUa^nr&E)hI*mhEMOKFf_kMnpZJ zh9SrRGM(*vyV>T5yrpd#T&*N>^z$$4rjd-LPB3tDH##ztq1wQ>Bcvq^A^CNG0`efq zbW{i=3t{gZY2q8rpbKaw?@ zFY+sUDD3;>eCm2t-LoG?cX&lesoK=}_s-iBFMVS9em}8lqm>&*@Ss}OgpgHUM-v|G zKM6Fl9WoK_Hw{kXH|yxRMWRQ!^$;prbait(C24RK^1U?{f%Cm~S2!lj;Fx+NV0LRu z!skREx*=jF}zYT>S53KWMOCrW+k2MvagjhYm64|u#XC5b!m@u6 zA@%|`rob?iZ6iBVvkGwM9yPe?=ybbop+VvMD6x{vqs0EuO(x?gF0>E%R(mms9t5(Q z^rU;Ca@n=S6T^$vdULcFG&vvRo*w{E)vIr2p|>&Kc0`axX0m4X-tK~F>LQQ4{rgY~ z<`N#L6LvX!%f&h5lTMKXBYtZV7m0#>PMm4O!B|^G(r2erqlS6lCQG^cVUwI%UeFAVb-PcW~$*T&kMsvFd6%^4V(%F92} zuAe~pGvMiM_jx~jFD<}FBZ!odW7aQ9!3zWuJfJjau6}+h$a%+kJkwah&n9c-?jEBL z9}p}5y22hf#()V9e5?mrIYqkPinWPKwn68{*!*P7g^)Mzr+JoyshzRY?jm}nyQhmOnWVu6r@KcS{P@_mXnltwvvBu zgi|K+x8y3|A^e`LJBX(8sq4q_J40$ql$K;Q#hw*GD{^JZ+1qxp`@5oujF0=s)AUz8 z&jZtm98W(hTolQI;IU7L_SMYyaH^OxTL=5kJPgPB-%&J`(JTfr200(V{)jPc9X{U) zGay_s#j2uyTPlrp54qR*$^3L>;0y6ni_vQ2aeia-uO*=5_^krEt#V_n6%DaSy3g^v z&&fa=dwmG=d8jQH1P~9ZfNk;qo}7X1fhH7Ke}|R0h{{VXK|+VraeYeaALE5-Hqr$F zLLQ(T*|JXd%{w$y`Wyt#n5_pp7$v;EY6W17Z2ff!3UxwNT{#h^lbjRyvovv08M?3So{!xNcg7jL|D9c#?0{qKZE zOtQAIfpeLIc0vp4knmCec2aH+X_YuM7un&>Qz@ zGtm-92jaHoMzs`Wt5OO#aBWoFXA+*Ehxb#ruG_Z$EB#GXyWB6w6u3nN^!uVc?vX&Q zpvR(QDh|Tsp5`Hf@)!cp0n|h z^8yd6)8K*y@O`cqoQ7~Z=fo3vg2@IKB?5&^4W7qnu$+~u%?0tQ^t%7U%Lv0n!yf=|C-eHoaOqiHhlA{gd{pkqN%3#nvsiS8m_*auT}i%tJbQ|#svf; zsQ#n~N{&^TcworXd@sn6C5H;;e`hV79TDNiUre{2ieW?9AS?L##5u(1_;&$jfqBWR zj3KXwx?s56)mGO;mHD^I`4PW{M*hYzpz$BRqBTM+@EiWwPI}f@SK9%%5ANc)%CJho zfc|wF?SrF8v9sst+>@0VJ+%FOf-w`@vW*N?;CgVpUS~dE_{84+cBQ}|KOhL_TM_$x zcbjptdB(97t+PSSYWww8_Tx>`aJj5XK8wuH-<+pg8R%}B9Utlo$4Vj(E?xDOrOHv= z?7-Zc@cK4I?gyJyVYDbkhy94jt2ver?l!M1j&>B2?-Ueg8XT4K4}6XnjCIS%NG3IA z0^@$I=!Vw7ZKwy8((f=a!;E4uwkD6!X{|DGWtG=ZCR#PB?mtxlhV?DgX}tsZh#BNu z%Z0Qg_FhC^nlPtQY!61AasIu;K0kI23-l+R@X)#!Hye*2Lags zZb=vL6-%ucCbrn~Zy;^MJ{()H{$9zM8Z;=z(ZL?mZ|=eRxOUx2OW|AADNb#nn@u|$ z^MYwMZEFZttKMJZ2U-SV=@-b)LPN@40WJ zKZij5;S!mWWwog(8U&_(=guT9QW%aQtYx)YBoo_@a|)8ukVhCfH>HNmF}75>sfA1uYRbQv?4#$Qid-6O%G~Xu4f2IwpOv;mtq0}Fu*jM;5tN2Y z%3pySxSx(?Y3H1jvjaAq5{`{u(gY7MtZ~8tS}F@OyxdG8--vbR`-Pn5cA2~$$Z0_+ zORzs^w95{?V}pS(Mc|9hjg6wV+R-2FQy)xt`PTD3gN?0S zU3<;=sJ>^qvict!5d>FK&>hCI07)0>PkIzpLa-Tvo(6uWAZtoJfv6kr4+wJ(8a5M2 zxkqsfg`u{*UNfN)Aw&Wv7!v_um(UK+y1AuYcRdMZShpqq-;+c`Kk(D$F`NM0-( z4o;+o4p|WZIn4ySq05MEW%`OqsPeYYTS$5VIQy6#F(%OPo@4p0F8|Wez_*#T3%EU!tV6sV@PyK zIe|!Up=1F!zc_pgr)q&+3$z3c-}gYZ#6`GUyOBYu-KyZ2Ir82YPg9B_W1ubt#lb^x?a&=&> zIA<&Yd1LUJHoGxhZKPNgo^61~WlD5AnOF&k4)4sX3yvDM+=58KQ7w1qMKi;=#srL$ z$&_P{OviU-g_y1LldA8^fb5Hh>)hOI{drf_ej}G2`0t;3-yi0>C6mu`8`%apDH{Q5 zw&|Pv1>=LNk9Ij|2s3|hZ0S6~-@IVDK`}PZ!4uBF z2nNlBT=YiC90zpZv{A6A&T28T;0*P##z)qF-|9j2+UCoCx}tUKiss{3Gia&9`&#%c z1n!zm9XFePnhruX%@{n3mE>Ces`(vIf-+6t^Rjk)sUklOYUIlfs=%wd5b;nO7gFJ(L?`E!D=|VZtxUEmq|R`u)l0Fzb*m#e zDQs?g=7*HCj+J=con7--Ushr8kCVId7Q24VrmtjpG-5w;N0b zYa7-*2*gV^@a$KEa*yEMpCgc{4JQ*?H(?(wK&_jk8)9w=WBvVu8BZx?@4IA|=G;)+ zjf!NdoEf;{8+?K{)oQ4K9(PsG>v4~{34b7m1$x8W9B&w?>+#jKXKDXa#3pWXoCD^^ zkFUbchGt9`Z?@AWT{u&Btk?P^1o&mE$#mL}S z@ZspXrH1nuM;$o^pP>%v7NxNlD~lOCQY8F*&}B3Hxthkvw7MNgQi;b(^jZdj^gmb& z>e+NQ@9cuzW5WDpiMvIU_aD*6zgm77Jz2n%q|j*z+bob*`bI&lrRJ$%=(15qA>S;U zOpxBZI=0DAmRlp=JvS&2H(SVqru(vkM=Q!MzB#Z2RAK4#*nXRMLdk?1hIea&h>m@a zn8b?*MG!Rpaa*|25)&|eBi2iqztfP!WrC%^hp0*Zi97lj_r>hEpAvF`!Fq4a`nl~2 zT#WSesNXd&6iL9kq-|_3EQ72WNcJ}~Ef~y?gxOjcjNG`3x!0YLkcVeHX?F2yo&yIKyeOUCYb424>zf|SE~O3 z#Vq_+O#x*Aopfn=HHe}Yn=d~cAsi(RFE{SF&T#tB{xzVD1)dk%zRNWcCVDix5>vOb zn(w%asnjhJo@R^zcebTn@>Tn4SFV|#NSaler1~&{h!YbimrW;YgR?k3p1V&7IvQ_h%4Y*h zysXVzO0we4m0lsaRpYTMMb3pT$tM}6H3~B(a(JjJbG9dIGcrFKp~aQaFNC7b z@e+L$Vw*k&+S`;x5bD`QC0-ZDK4TTza?-6By!SB2N^vCzbHn2vy4s*cF24=^AkQlW zu)3HUS?Dy+H9f9I=4*BF#ektGWgs7%BGR`{Xya5rCcr&;nE%#;Mw z!)Qi4A$k?(f-fIY(d`5jH}x{&qApS_O(@WFwGxuFc2`s0O?Q1Y_v!&^;4o=(4O__` zWKf9w=bIKkDx0iSVtt!?C(WeT3CC|HWwsn9> z^G=y4E--#oI)KP#SGzOg7a`c=laBVk{okr=$K>7}&^hw2@$s$*Q{nIqw{6Q)WMC$v zK`kvw^Z*3$RIstYDT0OZmVMz_0=#tZ*J>TtpGf_S^-WnFZE&I1+DzEfs`*F?x+$UA zzf64Sz|TL2Znu(lhwv))ju0XGY4lrixC{OwJ=k)%&!5Y`b$5mNqLpA1FLQ>Z>nxILD9U&XuPsAaWT*0L_=F@z;zA>nNT+ zBfW*H(qVQWV!k-;*mop=k-`{pQJO&J$ws^J;2y zK~J9dXx}(*O^5V$qAtuTdAD5C%l5*cjmSMbhrSduP8h0h2^1s~oapJ=YJW-LWBMR^ z*9(lhRb~Lve)txrdY6StkQxu{vmbxV7w4;tNsZ1?gIIn5nKWCVyW;IkGFO5#H5TTD zkv{bs78O-nGu_;{3wOJXv^CbHcljTQ6uKg931wXB*P1_1tm;}N8fEZya*&z{FH?LC zE#sHH>e8X}jim+0IrIXwSuo5LN<$OI>b zjloK0w*JlMSNf=gK;0<|;N=*x^DXm3KVUlo?#0;mve`wAmEHuNyslzoMETTI0&kGlSW7Lxv!4{d>m+O%*70jF|xNLwdXA zfE0wddWj)ef&-FkI)vIBdB^Jjqn_-!iV;f6w@^Yo!UYR5Cs>mVJz2L-l0H|x9mpsh zKHYz;e*{eGc|f$XfdhTUFxIJ)hhNYa78e{62-GP(x%{H2rja^JX;5s4(zvm4lfMEA z3z(~Kt$_;^$>L^U?-@%O993i+L7OkygW#1{ON%{=AY6ZOPK&eR$3%=$2bXDC8Qe4t59{~Ahs9Xhsd-5i0sEd*UfxT_D$a08WzHWvey_3eYKItxs?&i8F6APFkiz~@>;`ChQg;#a1C*#3aSfxa{XJqWB zhGPr?CNW9E*zN(39o{W^Yig}XQ~T5D1JbKa81U5?1H!`){Adi6oG`+e8x6yh>Hn%u z_|rAdyBF;j*0}yxG)RLk<-wxC(acFTc({9BZp?XJ**>_GXW5JcPv{-icF8T~lYFP1 ztMkj_%8Uq~u0ENl9S1AThzbm~rj*k+zOmAV1r4Iwz(z=IjS^HrE#o0{4^(zq2it+$ zo5}03&SzdU+svK`;tK<5hinjtHLd?1=^hO=`MRhm+ge_r%kk3~lamGjb~Rf?4Z-4@ z4Z@SdPVY+*w?Izi$ts%DP>w*8&wb0{hlr;yiOtvetOx(j-3tuI#>NA^r21bjE(QJr zJ+NOLal}wk$l<8CBt((wkqhJKtW0Xz(x_%hIcUDnjHlWR5kzt3OQL>`+o8Y=?1CZl zqw$mGY59b5+c7^=Z?0y#pZ&DpX)oFIk|rX!)A6F$F;-sDTE_6kaMN-c@o*iy=^(Pn zclg4+GD9I1SDDU6p^S$M`iUIoq08#4qu+T#A*$_jx#Kyv{JdT&vx+ZKJuc^`shOu(yOH!;!NiY zZk_0O&EiBspK2sq#Xfkt-?_{6P=YjFtE1f6I5ly|1udTh3(_0~OUxPLvW(^(dP+(x z009Ak&_oWO#^UU8?vAK*1N{62&;yDy;Xm5GVy^mz=6&6(y`>{o!MUX#lD(s1UTTg_@!`B|`MZSIM zue1i*DkZGtu$HsSpXFqn$&+bnRMPcYg#J98F+e^?m26aEkY(lO(rIGYa9W%_n|NX} z;VW@O4tVP`!lDsdElo7BRHN+gXX%)`wrwhD7>+nsgy&^QP$0d`0?UiYLP8;&+!%fz zOvP?|rKTsOAlxxR%}_}2o)%e8s8~P6kYYrR{K<{4u#xJxZ~rE-^!HDS9V=msCF5LW zK}){j7E0ly%F|n@W^&UY2?aKdrMLxEe5K{vYksR8W-EQS4>L(!$LV*)5Ajh7M7H0T zqF0FI;-TLlD~{jb7S|O{^K&-8y@ccAgdOvWQR1`qrG<@u61kcJrU8bAhM+~IrKMv+ zCM(8Gh1*S{mK8xSAHwv{N;DQqD2g4vu(Rw;bl`z?B?+T9Mi64bnW7vniS#Y=9LfbMWCb=R zBf{RS(}8w-WFOJ5Rlh)>r<;&9{NcuXcA?7aXl>w^Z~+0qf0pd%DsFy0?Q=*9=kxl$ zykQPbOG07dZviCb`bHxymty1epzF(@`ej;)SmEM2jrd7Yr=sX31+>`JKi?UBY^ihR z9@vazp8q6EIs9l22xF?5?l$iji^zGDZ*ohJfjy11?!P=U_Uc<4H-zZ&iT=uO=bPaZ zM^cL|sEsgX{vKoZQy#C+5|8fA{heJDClaTAS8FzVjR1Ah_pVHzc%Q@LW2>3rW@Pmm z!+yOq6dgw}fDJXozzgIRPlwO7esrjfiJVv|E0zD-@i+EMuK3ntje}DdNSj!SWm zEc$}=B%aUx;Z~Vq9WIfbb&LuSVf;2XYDGFPG#&WyzyVR+F|zf_*M_65JV$s0Qe}+y zcx@ew7{UA^`})K5Yl{IXNq!iW{Q38yGNwP8j^AE@b`#9^$LiFhN*;1cK@WRy{ zrl&zRZf6N84Gp!#Ti$^wRqfnLha^~s6SwUjHFfVC7*u@{Li2<7Pn!wkvjS%7Y6;#} z`rh*bfKd1aZTgz>hLJ|$r%!>Kj}%gTJ^HP~Y14#0_TebuNU|8+{fv(fKGG$!x37G^ ze@a4;{80UIXMJmXPGux^sRwhFe8{o(_ia#E9f*~%f*pu^x0=9_!SccSe-G=xbcvt` zDRWpHp7Gq%PxTV>nUkgX{1RUo|hPkhaF#&^o<+nG^T7h>KNTTo%i_(=McY-e>AEVU`Vy*R~CY(j3 zhviH0j;zRn*yJg1otxXV#si8z0IImuxH+GCK|C~&atmT2dP>6yoHmZPw7wdu7PM8O z1cgu7n&L~e00!Xi(5Az!qeo}1JXYv7it&2?jkO{tMLa=SWTYIx#KdGRxS`o1&|JzS zoZkb};BWn@q|Y;^x09R_jmOmPq>x?xsQCK*^Y>xjk5>6c=TGv?0QE6(x1n&YCHMwO z*uzP5p2h1YFA*$&->iw0ns$n|SW8sy=7xFOFT`w*G(6u6tVp{t-p@FmJPr7$Az5ao zGqR08(p=K*EmJE6)#jY{L5#8rXqHeT+dfOT#H?S(n6%#Wq?NIh{z^hxh@SdMw$=#P z3s0%4;g&ZkI<#-E-Jxf%MQjHe!7;aRcINNQV})4?-&N3j+p!uw*KCB@c&o3?7_H%` zlv7ckQ4-?}YfY94azQn=1qs5-DmO;nf1+7+VtzXt6dSl&QB#mxNWHk+ZZ9T84ke7M zycO8>?jY9GF?4=M?xwDp-#~0adz8<`f{=5XA8dWNC2dh4AumkM0-*k39?QQK`4Pj1 zZhamI`C?TmK?T4i^>2uR{&=SUTYtI9i*rLT{@-uN02{vAPeb9&H#2xMLLjvp(4O2w zO2R&+=yPb8|B+6<{nV6Es3r!YHC``4#A)r_1U_;Aj+n$5G*GTq_H;zA0v~oCof5YO z)tD&Y>sw&Fhat?=!tN>T3c$8&(yC9U9RDEC>}u-kFAau24o|;3Ccgq&Fnwx*M+n0! z&7H@Q2{`@%a|U%jp-nH^U2Ub!%6a$d>0m`EZPB&m<@bSl8)a7|uL7XuDT_9&&pnmc zB$IqT(qM<9@+bHB5i_+_DcKsJ)22tjs6*&5?CxsyN_&rB(iXMoT7!P?0DbR^Cz-0y zr@R)z7$702nLQPbN)mEJm!iT#WBWI+1dklJgWGxh4APy#LW%R%dtses7(ZXsII&rJ z1wV`}OohMRjTE6vPN?oFGq30Ph}X63K*#s)vbX;8ZdR9UeZa+J=?FxPgUvigejVSP zFDaQi&UhLMXq?S+JW)%)T#%%A!&&#!*Y<=bjQ#IJ7Ch_BY?d2-(R zQAQR0&GGpqQ(fI&(Ux!qY`k39nxSOr9Y_l+{a^BZQR|}@;9~IyY~QS0_D9M(cZtYz z=Kp;@h=BRFESir+%5_!v@@jzE#HhJT= z^!Ju47)6xayeofpxN;>TkcPmxw~W2?3^b#r)wqNM8~p?djPlukQZMRz0_%3UxJ^ad z9D-QhUaJhME*!)W2a39QP&PbI8Vp|+a91R0fNTo7v~=@YASN}~TX)8!UfW8U8y)HK z?ln9#XjKX^>?Epe8f6;#eV__>qU?K88fNyb>p`e;@{RJQ_PM0duH1-K*#SaMFjc0T z2ws7NxP|K0oHt)=n$&nXo85UKZQ6h~_8AJa?-~*jm*HAFT z5GhIjc~uUtZ-3TJKlcX#+xrmKT=%XrYyr8;4;5{hTL?}qb^fpOZUNL3f5bk+;YcYn z5&^}*yhXeftkJ@k(X_OL=LNNp1HFTQAUNGih_Sp)q2=WdUYqHyEjQdrq7P!y}63jCdCB%=BBq5MbB6>irpe+|7o5Q5=`+KU)D)h5=@ATsHwl|P19|jZor=yGUZd6Ph!#~OZ zA~r70QP3xV7x!L%k1zJy0lMXesrWn*2%gt2&qqbJeGvNNZIVUAxA?^sc|!7vZr?4G z&y{}}*K`dJNrLnX0orMq_1fBIH!6d45#Tb_dk0DPsh-8Ll8N`2UNuQQary)rBQI?N zA3tQ9?8ooH+b7cJ_I9aN%+C%(J>OyK(1*^rht~%*BkMtbfl<@6ZqJq(lRGb1_9fBp zAxtD2X$p!ZHNe#Pk3TfalHuYQXh@11!!&mai*r%{no?@1xve%Ok3wW(HW)W7grZsd z<&5jRaSp+^vK+$?C;g(O3N7OD?|r?Uy}$0u2_E3Y)n+n<#OnEr!am#Ct>Q-V%@z34 z8C-r*U-olp_BB zFI~uy7F=Fd8opj~u108xnh%;9q@-5hkNl9qT1baWj&=ulO%JcvQ`xKREg?2&BMspu z3#hB@hAnFX3~Ft|&I+x!4q6M^X2h)yEh7G)V}GWl4wndvKoF>;@t0PC5i3{QbzSGk znKjGb>a1$E%<9(0-d`LA-NtR^PQ3gLCxx4PGhx+C+6qVYY{_`Vc2xu~zfyg&X^+QD zc<{sW7br<@t%M(+?|;z%Hj0~&cMmH&--kLz%(}1x;!4D=aah6;(is7*m~jP3@-l;t z1Tzvb#w`=L-Aa#@`T~UceS3nu88a6b*Gnff8XeD^i^4ZBHYnROYSJ!U!9eTssPOUb z5AUE~rejBxr1=nXTFR2C;WskY^Z%SOxRU{u?Lx<{h{C9K7^g$UK(bfzpoJ6_Gl0L$&F3vOE50m+8 ztz~@Pv0VkA6vxtzWGh!xZ*qBqweDpr)KIWOSiFC{R{Vf`5BiqbfHQ%AvXnoe6q=Vc z9z%}XwCm)3)!F7Q@5DxxH#+gPOA=^=str(5aug0k^RUitBQRDk_sTK)G=NvR=~2#Q z$E2O{d`B&xP8J@^2$+3s_8j;C@6!IaY#Xo(Yr67V8J1pD_+6Zg22ZKJ4-oJDLd5T! zueUwjsQ_nZ!v|AatsgQisRa$e?@o;-MsVoD25|yi&GMA+@VkSr1xj&-ar-?QqqBFk zwmfsn)n>ybFMEp0H#r%6#&eEnbpS~P4JWK5wsNP&ql4p?G3q@DN`LxdiB&Ym!m4c`&}tBD^@NYVt3=vj|%#m6VAFi_!` z2KLF3quTJ8WH{Vpz;H^`3qptAPtM${U?W_D6sSmX8$uHNs%{mQ>lqfG;5Tz-ha!BH zWaHdQ|gPti!U_Vvv%S5`xnu-b$*IZy_lx zhd+}Z$eFvhPTKec8qLa!c$Ws8`xT3@P7>^xnLK{CuI|4OnwFxR-W%fXCFa#A36%Vx z!lx5AnV~NxH1z)Gqkm{_XpN|0Uwt4O+>qZmlGE%3FO-Dq=B(qMxd zFjvP%7!Sw=1@INIQ`0ZtYR(Aq+bpLLK_xT2J}=I`?KRCb7Z&AN%8UHjc-Tz1a6G$% zkSOL8?L)r@9c^kdfc_(VA;~Bycl9)@P8KYin3Y{6jS){$K!E5nfF`uy^+C{B3?8oB zr!c*z&Pz(c=SMpn06-WzJPBiUJn|$g#B#8}%jl9)(|T!n-LR!@)EG@@ENL_ag03u_ zwT!RPFvvC6d8HPf%p`v2Y@%&m$U@If8)@)ZT&{Npqo+k5G0;2GXspH9M0dS=px0IYK_y8~QFSsa^`Q48g*@z+t zPsS8dx}yYO7xFCaipb#alxWvRcWkKRhEA#R$5 zX8K%o# z;xkF2hUzd!jKKbk2vT!BUVFzHSO38H24LBEX?>H21hL=*zrC*6 zCz_K^vE6)I(HqedIezEHx`_fx034eF+8wOGMXKN4dSDxeQ@E$a)l}1MrJ2my3l;z~ z6KPJ1Wu3Ti{@YZ_YK?!ZF{e|Y(x925WUSh`y?VB=)Y1m1r+G+5o2UIEgRS8{)ycHx z+(f^iAsSdhfenj(wZ3n^!%md z1V4)GZ1MX??0VM|9IR9NJX90SHXD9k3!)U)IYEX(=QG!1Se$UGv4GqWJN_XC5PWs% zFlhX?H1g6{>qHSS8Lf&t@2t{KAg++%2V$qygvC`;VP5*;ri^zi?B+3tP{&57FsYyR zTuVcG^K`qs6ow(TK?T$)Zzcv|L#yaTk4)@Dg!;Lj3t*FPw2RP}iwh+=d}{_;VIdErXeH&9G-JoWJGEBJ3g_otdEL$(jJn(AVVjXY}}iJ`~q zs;wykaRZKwn}aHod$^T2D>)zR*DjWxKDXrgM-d2L^8cMtRPx?3=jivItg1nVky?C8 zatSOnG&DUeBBLmk6cWRfyP#pj2NQ)_*sPtUZS=W{^onjKgig6m25ua)>U6%V6NoPx6rM^$H z?%Q=gh!l6;0AnK%#PQ=sDeb24TndV0nVifMmv67VXhdj5!uzKpd8w;pkhMCV=xuO> zwRU6A$}3V?w4t1&6f`f9^KBfj_d|h<4Xe?}AcgUx{(HjFlQ3OLzMLw5k0UwbK!sd) zZME@DFKK4zK@z2>Y|;71Hw2s}93%zmpr;_5*(f*^hZNO>?Zk{EVusYd0 z_szDZQhJhyfQdB=Eq#FlgI_Tc|5I-*a!@ng!i0z@4y?lkq)^e1s0n(-*ZjBpC=ji!+Q5c-`lSl|4Q|Qt_P=qoD%F^8Y-v>-_tBg9lv^wsmGIb!#M=1&LI|JqB?B~#N`edPneX92E$ed8zEVLaWfHEF}*aH1M&XG=6V&B ztA^&$vQe%H{ky&JoD8R<#TEaj%_Ap5_=3K*#t`JjZZ;3A-SV(!{BEP=IFYfLP~LP+ zgq$d*tEz*l`Xl-ZkTDg%zJ9*hzN9=h2h(Q?X4C*9LhJx@U?D$&0yn=b$ilC7C)p&o z$8XE8N_yYFvLrem8!@>bm(vbxfaAbjj_pMroGO7X5MW+Czj24S}nSCn~H28H)^BF%wsbM9IN* zsOz^TFL$bIdK)Vf{aN| z87Y@T)t`6}W945C>fJa6C@ZL`MXf*MO<%zOY*4{8P4KzjvJkO*{CCN#7m>eZfi|L% z(V#kl*jXEN&-d9N_XP8LXQ~EG@aJiIZAy_ z?>FqpmtXMJ&>~_|k%AO5jL}XBjXtmeW0Xz#uRNFL@C2?p4L-z_! zb>*-hc4Y>fx3LMr-8}Iksn`4_EQ?u;Et273V1YPczKU2Jpjy=H)Q^*JL4Y3yYAh>L zW%BXO4O7)@66J^DnjP%q)of+?s>+IOthfPJ{tPh#O{lLru`FtZ%_3D50=oRYoUtx+cp*rNEA=Kp9}9PBu`1%di=`iGtm4lgq;qwyoAwlYS2S zxX-*ve6YHiM|K{V$9PNn# zm2(t`{U}3|)Wt!GFm&TesG5u~s&EP=#wQdZ6L>rvXHan^grVtCYn-KYSLkO_8y63^ z6MLUHR+wUyfSwCv+7N@ITcFR}X+~c61nXvNe z^ZZL9q{oN6Yc(0Fo91Zy6018`z|XiNaRtR^tye;FDXF;v`S95kf6Pu&{Of_0(rZk~ zQHh`B|6T3S5~kYIIIW29meQ8f7-9%xrRc-J>?m35`o=V|V<0Y-0zWlTs5mylpvldq zmPSJQ3cOjRwaT1?HSFo1*#(xP&wrsXh5(vK9~$E`mE5OH3&Ca_w)mr9N!@aEVrFlt z)t}|S85m!O-^1r`z+34B-Oju%4HYcCI1`A(cR`9ys*P7g6s+&o^1iZR>0mW_co}r~ z=E%5Q3(JM2ig*O!N$$H)i9xn~aA3Fscw1s?Me->#vru%F!bCulwy#hY$gj8pWpDgs z?5*7Ig_y$pcl5c7AbO>7;7+Pc?U@3Z{cvbp3n#l5BOlDL80W{a%DJW!y`lL{+wwtG z_W?6iv^XU6#PCQa#K;KMI{PK8R9tURzH1Z3RZ*zo9wZdREBq?uz4H-UFnQjEvRFL- zBRs1sX5&KaGyM3<*G7>$-*)d~+Im+VyC2cf4OMkg!Sxu~R74WTuyOZ;Jb;xT1SyR= zYHjlUXbp8r1P*)-9@RV)V2sO)dM`ZEN1TDN=o;OL}1+9?eAiH)eF)elxxl zm=ZWS6M%)IRp(@V;A!0d`)2qqw$`4g#KxEt6N6fO`dbE3jrg*!V>Aer0DUz)QCa3* zcMG3zR%dOYo26(I>L^vbnnFgDY!(+<_!|cG{E^u7pFwAQLQh6^2ZEu^i?r-9Ct^&| zft%E~?dt7Y+-#Kj%%eBHsulE`g$+t#HZp#9?{5SNzgzhPvNy{(X_JwCub)(mdj}zH z_~0uy3#kh$E1@ED&QlYHDZryNVpXDkCXP3uizexlGxt2K;9qIr`z-KZ$vTUuW^8{H zqv9;zYWnpXw~@`c?K8&r1f;TMzhM(Wu_0GGtE?#>xgAmTEYH4XHj#nqT0zwBHK16K z4qxdkgNGp}DF(x}G$RQ4^f39WsgxagWPuG)p~%lC88r(WX@DKXT+z^MbGZH z;QzdPl}kE?YpNSP_*su!(!DxuzQkOTCc1OAw-Fl8iUfcUUc~;Q0wsaEaAH3;rhEfs zpsNI;pg*jUKLha3<2kT9?*;bRLjI!RSHlouM8`8uKfoOXz7 z*FSOFt&k0B9;22eLrvf^9JiCl&pc|YRm!phG%w)iJo9?k3fkVfj)1uW?ucFVnm4X) zIIVha!OYbTEa+7=f2MAu;`;vk$@Ir!;MNmWL`}>Ok9<0dWXHa%b7gL(@F{*I_^;(5 zaurFc9MgINOlIDf!EXC(`;A%uV%P!T>?e6RNmF+fSZz!>xgDZ71*B z%}CPjrdgRjP~R(H2g%JMWo=mirUi9{Cw3{vEtiXV29H~>exCnb+4z^!Be_fZgjxhl z^I1wK8nM$on99Dv8{dX_X;W_nk{h6$RY)B=u)BAkLko+W4t1n;b)VJVuq zo<2H=4I}}l9BRFYZQuqym!U(K+{fs{!A}TVV~5NL>0)YG>=8T#g>`Be^rb95yZFZ% z5UP1~QtO{~?t5Wc+Ui)M;K1vzFqM*Vn^f5PQ#`)#+%A;~MNB;~f47f_p5^ZvZK23g;)qZuStpvfvFa4lO9dt>U2qTFMV~IYv-6Tb zD@sBOhNRjTGmVlsXaWVtWn2nVJJ&~c^z!w+RfXhn1 zhrh5X6%6w878O+?QLnVO4<`#Q@$sE()8C{JMCxhF9(-%B6iz3uiv z3D4(u4b+Gs8M-U|PTl^E;U@XyL0l_EhN`FuoYyc zCqdu#DDoy0y;#d(llVDg04j_AfdU;)nemSVIMRz9?JE-t9boAz%nCjzF17^LK=xOcDS->b#)3( zsgq)MpY7tPOc=ekYZ!I_%TWsyx(z@ftEVtun~hzas;X_gDxkCG-URVsxBV77f16Rk zTbX#`_56zqjCIk`-!%A=?=ce}uz(023=If0t2mN?*M0iHF+7x8=!9781z-64&TH+f z%i}ptt3|AVq1|Zq>*W93^wlAoPJHQM182(G=tOJ&gWrgu-&0DGCgNMqu@uBKzpEO; z=_0mLhMf8!{4UI=M?u+F>kYimZ2zsZV!sg(%y*^nes8M{y{S5yinTSy@M4{!?20f0 zZ_$k~H0s?yU#ZPNM^);ecnCZ21y=7e?tQPeK^Jj2PHi}SutL`My_&xI=S*pez{Gq} zM#EdcAX>=ENGP%3Lp@tS{S-68gHV`upa1RSccrx}mu3zcv1BO*4|!d_Z2p5vd>w!Y zQJCz39@Nuf-`$Yv8i{(aPX}hJ=NI!X@aTdt+gqsd*L|bClV9p{tu5tVcZAxo3!~7i zwh$-an-l~BZ~moK)DZXj@Nv}a^>p86V{$`#GlQxYHUbVdzH|7tMhFpZ;B;{?Y$E<> z25*ksELqlakrGgdof^6$+d2`m%pg@8KVT-z4UbkSubV#*5$yAdAn9>?`^MPf{nDz8rDlD^9H{~AnPDM)eLbw@uaN=mBqvpNt zyj>|{wa0xbQqd3*Yfi>*t#oAJ<9)6*OXbEr#Zhy!UlQj`KV=9Gth6<^Nd#VJ&oBlm z7ee$GiDvDAYQl||&i8ao3@xDV#W)EsY2C5ipE|kMA0#)A@+o19%}W&&@Pm7P?$47`58>MI;^FuQe1IaP}`6nhr-27S*h4?GB4CAdaqut&fvE zkcIv%ZlB6@F&oZa$q+k);*Bne+T#9IU;kwjL}upd5WoMv7(ako8_LV&>)SS?mI|-O zTkgcc|1`slsYat>L!Dxg`e?!hqn!;6O9n@;JY!R-B%bI#!%fSL@B5Vps09s^)W_JVfqQ> zkywPeI_IvBH|__&)phrL2H`23}Nu~o;F@SwWktf!9< zG?s5a=r9J%d4H)~U2eScMVdTQo2U|;N;jJY3DAffyhTK^A}7#7bMd=&uWYQC3L05B zG(@*Vz2Kq`T7Gk|n{diDf7WMn-C7Jl&GpB-6Cy6{2M%=AVD&})9akIvFrED=I9EiT zNscRxr*y2x2}YA6pFY|=YgKddcHZB?cmJK=CdI*V3vRDW8|LS{y=dAkI>_HR zXv|Rc?9@FpCSI)mTvF04MUYC4z!FhTqg3DHN{P*K>0~vWPg;}VE}-|p#NkOm z?IN!UIZYnd*RGnrFWyeYD(D{N{RcHSWuXR^(lUzYzToYpG3dTs z+uPb9x$ttSBY0U5-bCI9PiEERTgy-8KoM#b4y1Hj3*)g+?YkCN_`Z8UuJ7w;e)C>E zK7SG8uRYu$Xv##XMQnf5An-73zq%$Nn%2^4mii+11b>r*v#@gmGhf*5_I~f!8*g54 z)^pV=7FF{@2Tg^APAvQKHrEd8#Q^jGrl5FThztO9!J&c+zm{M_np}7jN!I!v#m-4M;M@Q67;~f~3$4?M zK(#s?&rlP1I_iKpii+*-kwx_D$LwYMh2?V_ZO>X%k>eEVgwAi~G~W3@=B+;7hx}SS z%*U$Lnm?KyaZ${=_AK@-9VfPffH)c=`Q`?3w=1}ShNMZi;TcLQ4O{vUlbzr?bIN#1 zy-DwfK^yNqsoZD8URe*12+nDaUf1L(7LI~r5PFED8@pqrN#bhGM}Rf@1iJQ}pP#qV zPr1|$st23-t1J7K^~`NmRbP}o$vPtTV)yb)SuQ!QmBSR0gjD|_HKgd4;7jvVu=k*Q zVEjyLwwi?Zu7_j56Ypj!FOabc7aq=fCCIxZ<)ACxrtd>|Yf z(rvRFyIQF_>YNl{7j(D$y3722OsaGDC7%8W&27W168n1U%bqS8`wkNG=bH+G zDr}l@JD8THlRsVHl_T0N*MG|6b~G06y`3FkqvQ-`%5@f?4)(8Hmfwe-=klkqo$ZXq zsHQbDpdLuUmES$JTURcEc&}t}J8iJg{PEPt0T($lfd`SQ2S}48@+BMy(MejSMvEE? z&OTg}kdoHx-UlYk7@_#H&MFf2+MWuFTCb1$p+{Hu5SGJh}#Af#U8&5dBR?3q9hJya} zXuYDXK_8BuY;8^gR$Foyo$^;XipPSQe@)cVBgx}Ijl|_&ek*Fl>PS}9BUAWrV)eIW zJZ!hm_P4uk%Ug4PG|X%%g4HsgTePJII!m-)At%OpYUkgSs19p*B>GRT_T!$q9`Kbz zaKg`ctO0kGRawz3xgI!cB#j)x?5&?X17wmApwe29k5lFm^+0rw3Vu$TS!^P{0km6G zCbScght)6m%Ymn|P^Wt3_;~UrG!{DO_<(5FzOGyjJ1YfgiM!gh&9F&-(w;|V9w1K) zidjAeH!cFx@)_IC`o#_wF{O-iJY z!!ofRz|5Ft9kwpieZJ#az|=gi(LD8#!k<-DJ6%XSos6OMEw-wyJ) z{r1TwSSuz@Q4Hr zj~Ajkl9cI^JFM1GA5-2l_PCQDvVe}oUm*#&Y0*dyEVRFEbUW+ii;L-g2e5NF_)%a= z3d)M6f+2aqzYT7(dBv4x9RwHxP$U|df2tWTrnp=6SLHzrieeUMB^;6R5}FLo<*iNY zfU0WHjkdNQ=eQN$f2S3j)PN;Z9mmVfi6?8wRW=1Cv_xZ4)Hd6WT`6)G7>!q$f@I0@ zEe#4-A5;G#3??^Y9}a1bbVD4#H(>!rMAMKrsa~R}q5%;Xf_3!Z*bF@>gSes{3XsCp zKCH`xGgY26vwS42e*|MT2uyr}A{>#34XTgyfG~}@2LEgPtTH8b(oY86sJu=e-{uEf-YKrN@plw?-MIEmW^Qpf&N#L`G~QTz)zuejtWMF}ph zqIv9qbukz;0!xqa^L97reH`K9$B`7KmPc~&*QfPBD>XZXT6A)l;h*9IW#4jXC7YsT zPZ^Hzafm|Zno5VvfE{>+WY}Y0Se0r2i-5?iYaq@?v-lA*9Z$Ell?wxrv8KJ5|43B84 zibi5)Z2K^n_&k#Me(6D$+TQwkOj~>J6A+EwF!&z*)?Usc1<%Tct!qNP_vZH?(;~la zoX!tMzqPuao{0rWi;rx<@#^2&YCT%mgqBoGO62VA8r~`4{ZUKS2CHfDq>x`23!^KAuNIvt7Z8rF=5v!ok)z6JiJu%xesfsUoqgmTAIGCF6; z57hA8FUL?;#{Yxtk7^p&>DP;nZic24Dn_r#F2+Hz$qkqbW$e-NW1^?Q9b&e<9!AvC zy1UYy_#YkZ{CaJrJZ84m198BNjA#DbBirKk_b>7y8}W1Kxb2%tcFfq~!_J4HFKpF5 zpBg`gG1l?HbpjAiV4HV6VWu@mh<%~^&K?|AX({_!H3ld`5lKy2Ack|SKr)osS9%-R z)yc*d2bYQZh6eFitoKoP)|b&T{zXx@Yg(pCD$7|REoJ*UkKIo@spy3qaGx>oEdayl zX#R`|Huko7&KA(+w_uT);1T1%qgw^2E}2W1wn-zuFCO@>HXM6gD$<&Iszj?f>JlA1 zK11VrV^vZR(f$G{1y2L7DR}08cJyZgeKn3mZNsSsskbaX!y&>>+XqTd>@O+_A}OXSQ}%0ssTyHZA#<=(SZ(C9AwH<4Wo2| zI}cx&qPjB2Ez21pm6LD~Q(U^DqF+&KD{Jx!(ox?nn*k;LjI#wHhgTfyd0GcW z4Hf$usu7Z{$H{JNP_DW7B-X+s9{XM1Y%*bY)8Fd zt5)HlFfA=8H#@NTy+?@DB~QBlw`M~^XHQYy>idnv51s-QPp}@wk;P~NhVY^{!3VcB z5(B3J2MC=@P=73un~Et(#80FdKioJdfR=Mi81$h&cXxZyu@95edQ7xP(tmKYko3KR zJ6(|$UL_n6m$f~eXdfD007_Wv-FbQ1>~mMvi=j&TEUBp=#epl0*57~t&{#@;Oe1 zuTB@0q{Tp86Eg552{%$tbu7R5lUMZPijVLXp%(@8S20zVpLBLCpe)1cqfZ(g#w{4_F$qRpN&-sc&@?|zFeat?Y2h#xOmd&OQE~EOa zeow8|y>Dm>X-lVu=7D)QJIDe8s~SwpcprDFDe&R<5+kApu-OL(qxt|pmHLjO5+`eK@@X|E<*r?`4U(z& zZ(~~EI$Z0S?#xQ>RAEMt-*U?6V*kMj?flIychz2b^ z8mL0r>Rx|twK>rFJ!rbK*L{O|;ns1@aP;Lq9@FP7`8Zxty5}ZWRI9_JS3p`4JBrM# zf;~17$vaML^D5!t>4&cL4&I$s(^|Uki2_rBXf1+Wh@yF=+wP7_LJ*?mpL3IK8D zoy2zb6UpeNc%Nq|m2oLHBE3xH{@L6cTI5$nPc(Vh-gsKIuW+{IDqhhR%gx zi@GjLi4aoIhvoMvjU#;J8IK`C*=T)b66abda(|=EKrv2Wjh+fmg{&{4W>gw_(7d!Sjg6e*86SPg&f65vEuM}X8EEv~BlhS0 zw+^|_f6JBw4>d{Ct4t*1KUgZf#+Cy9SSO4Q>079M%ZE2^O__%Rp*duo6XU<(*TxRWi!i-rDk>A$mfM< z*Rre8@NiokTKg%olk?oaT6K9eB_j_dD{rDo$QvMi=TVeU_nkumN*p}hB8G}BW{xD? zO*C-Vg4?k{e`Y=zY#Ky04~gmaCOtj-_dQ46W1-u81x(kV)Uh7|U!AwrvJ_O*yz(jZ zVqEa8(87S(ZYi2W>+VeHQ#~_(Iy|<#L>@!=AN~{XTVa_TmRd%oVtNXGdaeMGqA6xl z-(}cU&wu<4Y;~!nTFi1#KWGp<}yTdP%+S6CxI6vm8S8ufUE5dAM7i> z6nsC8;z-_c?7# zl*7XGv6LDQesjKW^CCWALdGEV5*}iV2_`KkVAF^TH7j&42@mIy^oy0%{HSMaG>M`NMj+7%qH)Ds}X?6vS?VOLK4rzcp2mu3SPOQSDZlh9djCT)q!bzv@o-5DG5I@iW2LjaksH zxk8%@x&ipncFfNB0ec+sn^F0(t3jGcN9`<0AIznPS4HXNGb1KcYijE@7Ho`shJFl5 zyQ$F70W>aEkt=|@N54cGJlSeDMyhwaOPq|w5=fMFZe9k0!zT+XOgOBrqk7ou&Mk+UYKK<0)OdOv9EIm=6 zKP=DhN|lH=?1at+VL_)~BYcjzHHStbWuhkPAoCy_FOhmPr4~8Y;gF|N>+jMRS%%Rg z7e3}G6GGb!VC@a#w+6h<8U&u{Dt)Kf0U)h*W|MwP)M|IR#;>GP!U{E;V;`zH%LkwUki zq$+ZcGQ07D{UAEMpusb*foH}*gZNJ9M0S14Ev+nwVDTA%L}5m?lz+%W3&ZIt_@%gU zt5c%XIBn%9jq_fH#TP`!oE{m}HuZ2gGK~`C+m0$(UCPd$rvSJEQ>_OYbAmk$}uqSi~M(lC^>7P zl~OGzI|podNgFehuKMsArS#f;o}gIXc6P!DEL30!Q7nt<*A_=-js{)LsxO$|Yg~7~ z2V`ttc}!%5*jh<9Hvi5ClFug)cRq6|O2Y7Qtd;6l6IAG(tlXvQ69=Bj|NVI(6x54( zc64`!IHy6@7n4d~t{o!hDh=Mm5|lfjCoaQkY@S5aR>sL~@0*8s$4e`qH9|)R!(hf# zMeWj`WX{8@;d=7j^}~OIImg6fSV-z``QUJH2{1{#oAd`HoOIh$z>pS!L##e;zTEFc zM9@NP?R7WD8n0q%e@M-+e+ni&igdgY3!Oj#xZ9y_enH!@$Y#e(XE~@lz5a=RKj6x{ z{GT%JN33dNFLfJTzFgSYtcESm=Fjlv^(Y@;BH%t&FCbrZAkxqZtzMQ2JYmdGJ2i{+{FVO6|*} zHMlHT;{a&ChS7lKTLsgMp_QFoFPF+-J8i;w=;c+b*BeItaYq#Aj~~~eU!D+7B{0n* z@H`F%-#VJ7C8fj-%x9(GXf&Y@DBF5{EqiJa4nkjd_Z3=m?*@HDp6;m=!QQL&@zqPm zcliF}$x>J#6;nFv=Sw0-E787id`h0%N~u-hg8!v{H143I5}rOV|t%B)4D3@xvO`rsxlX zGR`foE$RANz>iU~J1>F?TcgMReHm~nS(K_UGvB^KVn_uL3Z6+4Hc?izo)Qgkawj|) zu+4eVFQtUJ@Wb;S%b4>I$xa2BGGX+mx9=i_DMZE?MkKRZS%5V9aERf=$HDWH6rU*- z#Q~SYsA40pBi=%Ct^~&ii9zr56aY}!L$4$|^Tv7{l#U=7=2z(ES>^8)?2VE1<8N$^y=XVjJ$c%noYV**DUn@PbODMnbE_g{>oxn!q#O>E8tWAj*;rvDFVNK6!^miUI_3 zopQTcU}AE?d{AySdN{-@VBN{6%wDTq2sCF}LNBiqGx-D}VUaTI?=mrw)*+d5<3Zh( z?3`TC?j5{A?fu2aQ}w`KlLO*_Lt~A3v*Afr3j+kj%UmB|xiA7pQ!(GfKRrppQ_svAQK3)xxc&m#V= zgX(^9{AAK1DXDaBQGo8%=0XX92St^H_M81+=#Q<)Q0IaydH>h&=$4wYF()Hm6Z2!g z;s5kRhhbQ(5A`*&CTffxa|R9QRJ1-K*;JqAH_k8 z($oaJSFJcK5TPg2-2(L$7O6y+8_pqgeebIc*$1S5?v9z+Z2y%$MMhl+I?PC^vp zp~H=JjvA4(l=?1lj{c|fqFmB5v9)?PJP5l}pL&yeo6BsTyk3+6N!2-ZwRX_h z=jFycchcuSU^4!|B=WLP6?mAZFwg5IR)i^b+@eS+QQ=L7>4bfTRnCUE5h0DsMQV?q zCh$rC2_MB-T$vVp9IGcz|EuHta2`t!l=uKsNqn3>oxrC{lQQ6U#kmdxOCHIjLdBeQ zM;JDPH~5&%5&jt}Hz1eE_~nxiWWI$hy(Sip3Od|Ie$S9;UAiLC7jx>+h}Yvrd(}(B zLG{}1CgdwIdRp|UXbPYVy8Sa@(QKu)(2)K4MesDy(-WT2+u!}IBOa-Jopqve#BVx( z@rz=;5Ko_wUk=qZ2A~)8R)E$k_#iYXRg#1YAM-)jxEv|nz_OQ(+Af3`-G!yYg#)9a zvOwXl&er3n)zHvY&8^Ew4b+0(7bI|>9f@M#LV|3A4hZpw9ImVdv7{6>9toeI3s!V{lb{Td?qHzD)lL&2Wn`%BJCtdN|< zAUc8q&te7vZ07tw3_^<^DuzR|;n#^Ql-T7pVRFOZ`RVo%e^t!@D%;gqtzWd6@Au{z z>@5CM`*`vEzRi2$ZDzI9o%-ZyOq_`LUFC<)h{YNM{SRn#@olzzTJ_$b^q2xw3a(Bd zu?`bDG0NR2dX3-P@kKR3M(kp_c*owuOo2tq^Kb7~&yjFThn(rq{ra#XbQ_klH3zd+ zS~i^XQp~qmN~8+elrU?@jn4N{rV9=H<1|Y@cW3r7ku^+`qkxHEN5g2+mA~{e&eoK<*Yfu*z zNfHkn{Suw2p#-#`fSnQ%_H-x41d+@@^XBfZEanxaD-+6@*n_3w zAMlZ}$}lQJFYQSKeqt<=j^~|uVSc~cV>+t1HjAbh7E_(Jri20N-qrL@MD^EA`{ceo z)P8&(=RfChEI_{(DyJ8Kfa>ZzJ4*Q*mky|C8~Xl)f-JaE}j7f`IwWX`0c zSu@R1N;d&Yh@!5mrLdzMu8vYCaX?s@Ql45&ZB4fZsKffItQ*)KhjxGrr`#|#SAx%o zhmOA%KW<-Xzqd#lQU$49ap@iW@JRL3n{8+Oro5NT`HAgy)`of2t{#OL+R&vq?2nlk z8b`PkzDfJZ+XccF`_qZ&6Si5fDA|?BSWLUI019Nc>;ryUqxPj~L)=WH3;p!1-ZVT}Of< z@mCx5dfTv_@gvdGHhj2)`5Hr~ulk7`e9ah{*>I--o%RAdosya&ZPTrAoYRjAluvd& z5KI6WA=;|%_T21MPfZ+xpo7vetwB*&1#;!R(e#vIf_mP^xPuuv$>MsYl|mNZFPz3g zRZRr0i1&NgST5GSc8b)$BZ{7X7lH`cuiyTyk0kA@X!AE#W}A25#3YKO@40&@9cuV< zZ^ic$&RzG86=%Z=5_y3U&jnyqAjNXH%o0RDKlx5`s|jp}NnRChsM-wceSqiVz@~-i!{GBtka{`(+*3f=?qUE^A+nX4Hb zO$$?83zWlezW%+gyTYwoFU^?U)0eVl-D}KEbP1$rh4ioLBIt>P9Fe;8uAZG$8&N`hyE);y zhL5ItynJ?mpxw4t?!p2!z)iji>1$!gle&jcM=l(!0NS?9N{P8 zNx$n)bvYty=V+{ATRVS?<9Fqb!wz7FRw6y^Q1^xCO8(xm1;nip$tOCk{*f zIZCj0y~w*f6OW_G{S>2XT+|Z6Co?5_0pX;YyBF&_S$+9HfVfV|W!BKsDoZ`Iq{4mz z?7=CEZ|U{Zyi`^$xglykjK`WMbfBK`bF*$8?gR@K;-MFd*9|{hUYGYp1so+DSXy3^ z?@1GUc{%3^|oqd`bc0tdDnQifn-f<~4AWT6ISoN zt;M#Y)6tkQ6(;B@C@5Ie6iOE9+&b^j7NsJ;jauZsR&R*5w({{;)p;n|6BUXB8joZ` zBgzUs>?+?1M3fm7(vOrWpDVm(a^oetivC$Moh6(=Ls@@#T0|l+D(dJ7a}cz!hG1V8 z1pU7+Qp26`mOHq!(+LpQpu8F!gZ-#XdRaPr7iP-Qku>S|AC8-a>wBDS z6`$=XFBFhPyYA51CviB{J$p7jdq01E{r(L9ePi|+gY5qL_dq+gUCdCunZiP_1F>~x z={0v+7>4M|=bwd>&IL4pU~h8IE^lE-(EhUnnm6q3QM#*#xFI?c6WtZuPw!{ypdTvg zfWfKgO^3l%S@Iz1=u%4@lV{r2o=0xhz~ijg)y!mz;GFS<1)RbfX?4M_T2>q=$jQAs z+(&VuwFpfkV4Pahy=DtCP4H*K4;)RNRD$85< zpey8{WQ25s0AL)Ww=L0OjTL<81+K}wX#U7Tt$jxfys;F1{9i==0b>V59Isq|O9Q%v z(W=SkNUI3}3Q!b$-TC7nUR7Tuy?}^!ddDJtRFLbsTWM^0 zTtdJHLf`2quos*OZwB1f8*ap2(>>)3RPtKQ+Yb*UUq-1^vKvC-IWY}XUF(A5gf%5sZTD31 zEQDsWZ6Loq1AnxPj;_j>j0fOE0K^>v5xVi`fAb1z$2^@Gx2Ok8Nx6>O@RIZ%O7ph~ z{Xv8NZy{8FAoNuxtz$h_W2cq-&9F#g=|GY3nDp`Q&$)cGw)<| zH|Z@8F~vrE#LoLPygeXpPmuKlTahaniHiK;JaY{F#qq5X^g~>Nj<($+40$y-)GtTo{9K4 zNw#zevvhuU1xLzNO5z|W25*9;G~L?&k*EE)*+nVxW`5eqBM-3@4Zb=Li#tS0)r7=X zz_5z+zHN8*>voP^vXU0f-aPJqCxnvO`V%`lwmn{oEdwgW4kq? z&>E%*=IZ5Kb=)%I+xB#Jtn}|IE5O?Q+o;U$2jWK!tjK{0Y;P%Z+1%h3qTIg_)0Y$Y zFrD zXNwCeq!>2>->+a3lJVUmvFwfqJZT0l|n8C|=RMxRxEhN_ZKlxmVoAWZ|g)gNk;%Eb9~`4i=?8}3NJ@Ma7FWW zO5?Kcxl?dEwT>WAb^ZI?Q9_XFW>gIdz?!fX+5jnB>GffmGk;OfuTh<&YlVOMr0GUn zvwb$?{)ZOl?!mZ&d{V&^84S(h<@xLW?UX6+{eMx|*6Sp!r%3efprNC1bFXHeVWv15 z$<0I$$Dc6~G@6F=qR6f%eih7NT!}FAIrCg-N(*x>7Zj65G5xyHE)J^Xbw8=+l2Eo^ zhr>qP2@_)QstXuVT;Jvm zF|(36VhedF;6nZ(h&Fj`klO-?Yajiw&Ra!9ZG0;4SL%&oEaL|1X z{ep^Fs9Za`Q5}U#fT08*>gQ;hM@?>n1xb>mXtEcrDzb*X?U0H!zofjy*IC>A7%%A$ z85RdM+1f{W_jCM1;ndL(;o)pEW?^7k#p~kyfEdvcZK(rS94%<9zL!bI_X<@*CD)yL zq(lUd0tP-Oq4gDq<4^XI+0CQ`%dfpxgQcG;H_u*h-Ea@{MZ*#w%lbcbH@n-9n@`5Jn;!0dXzDX7Vzx2x?Re3RUd4{tNMX;FD z_T$UOW$yrv&M!F`QTWKOmZUhKsD&U3Tzq7eBp3kP&ySxnqC5cFDpXoVLyj2{chT$c z0=LpM{I;)WG;OfkOSK!L@Mn+LL!_U-ns_es|Gs)G`;pD##S8nVcv)K)X=EylWz6`f zqcNkVr~|H!Y0uzov5b5|3zay^Wp*|UJ3V9d7svHl|bx_2jl(!fjHy8 z*Y@aK82BQv^vXB)N~q2ve()FNC!Eto>S7#OCDUtaFu&Vlg~PkR;KK@&+4r?F_xqS2 z!d+Q!xT~@sb@wXK(uUP});gNfo_jn>tsa9U7BRZ9_SMdEB*nE=cTA}eaF>JR=$yu* zR49ooXK!vQ@d$8Wmz8rv-`{P&ZM^@n_b{C42Yig0(VIu<2_TZe6+_Q=j;z6WWi8UD zt#9j3@2{OcOFb3UzGh{(XJ|F>F>GuDf=zHlqHrm)Lxst0H8+M*JIvqU9tLE#9{ye8 zVIDaw!0YsIBmPC9hzv+r?4iRzjak-OB|x)!lz;O~a^}^;vg~rfX`zy|%1pHuGM&kG zUTWy*i2Fc3k|r!zFE@uY2dcI6QuOX8jzQ=mH{{iOd0rIq7ts=_cJ zhq&&fXJ*IH$UqkwMV4Wj99P`@*(v6AaDIRVjy7NTrMy_^wLewZx&uIOW8H7nzAyg^ z;ga+NUOvG<)@Vy@gD(v4ixqt}!Jc3F7FtNueHfVbonzR;%GjA-OhDs%t=JIy$i2VP z2PDP{c?Z4KBK5%hE~JZUe-~GcG6l&xL@}&bjMO-SZ^i#i1pj2mqyyWAyNEv!o4*#u zdodoL^^zkT>5KDAN^)lC8QFyAW^7{YKeXoiv6Xm6zF5l^U9P_z--plzL4&sFwcu#S zx!ettD|N~?D7z!&yNg}=r5ncc>gl>(0Lr838Txnd&Pe3mW~JdXlb{Fu>nb{1m+D8b zrzL+qM?vPl9g%$j*xm*){LlRLJ(0``7UVk!U+ncMD^_PfBls>>eq19T+mjHeaJgFn z^EdSB%9q0@$#@fL3Iq%(4R*rBk5CU!gcyA9AJDWx735IC)0{th>Ia>;>^8cI41bbd z+hP|f=W(Vv{GaCN>dPO+BH&XrDP;YOw+8nhm{gE5c-+xtSqT5}LAAErQYGKoW(fZG z??f0AHoih#!7o8D_NQmPu%92u%kA@h9*lyW@qn*yLj7clvs&qhv7So7U~WthhvHC&K8Rr9SOtUoaxJjuL+2#KJ|(swNk_lrAXe(3Q{3+CIPUr!@B3 z9}JbFza@w~cW$kZ1QIHS+{Qiv|7#l?$^-ejni;*<{SUPf=&sPF#sfyyxfh?Ba5$3+5)Kz5{l^x> zxs}OGM^WN3aoz@2ycJwb$x@HM_OD${!=j z2ifVhR^-yK4YWQ~orhNZRRlklIz)Dhu7%AA8ApnnD%>!ZF5d)k-Z*m00y1;V%Z6gO zw|(2j=y-Pugz+E`k27uB*tJ@piwH_<5<)^mo8Us2Sker$Wt=MEe6vb1nM}+i`XGvt zR~1ACj?Dg=scUcfJe|E(&1}(_smF6O2-&>)hYo%QWPIwml5=p1WxTMhQpwv|?{??# ze$JU6ijL}{JEo3p_=4n8#KRy~6i>%s{rFY=I1X7gCJNt@k))+XtC3=jj+K3KOg-q=T)n12E`dn#44M2|-pw za6wWOT=TrbbQL4Ai!9lgl9J|RLz-K*Gx;z5a@@~OoQ}~HiqVB#7QCc_)1>c509KI? zudOFr$n1v@t~olSAA@}3-Q3r5XXQHw>!+$5!O*rK9=efLoPQ`fn1~%tqbH890e;V* z8^U#MSj0yMXg=aMTf8n-(#1k?XG+=1*MU7{$@%RsV{J8XbtvFYLr&lpymf7ker*p9 zix4ClvLWKV=kg(02XKARA_yM&|4pZoo12;W67@!ST(^PC)7-8f0rxIPwM~Phwz`k| zH9GI<>+`T~5oAF`I=-tiO2O%}>lDdlxNywu$nY*qySZ?Ezo31+-!FYai~N4Kc9WoC z4in+RCfUL2$DnTXb>N)$x<~4)x%4pf6T0+3tfK=i*@s1(Peg zg5i5!U8b6nUp5bB$D*{s;i}M~&IFTTN>Z{DDiuUvVypMFyAqnoo@%*cHQQM>{xZH8 z@#I^#JLE3gU#@2UZl7)J=NZ(vL~lj}J~Vtg6NGRY8f)Z?Q;Cjvm@Jt?5@^>9w*Ex| z!IWIK*bbq%Uz3|u=b^L;`k2;_<7_s&GVXsCGOgX7761WlSUV37?GgPRfjtROr>eSC zCAe@YLHKqJFgyqa*cVFyHdKGPw!^a+OP)&|9%Fl7v3qZe44)N5++S{Tz?a16A2#$N z|6{#%W&E~i%JIqIsT;6gc^Ep$+eJS)pvW`nUX!)Z!VZr+ylXNf8P6?Gwa0BlmG3D2 z{*eQ0W$K~X>ZQYGk2J>lyjt=?H@xF$zIa1Jw~FPSgw>E%yn(PZJ`Tq3@Rsa1MxZl?IL?FV zbT)q$QIn+&iibIJbSfhwM4BbtF{Hb8z(NBPMa5_Ty)p z*VWGmN{dI}=vYjnni7i@{0_E*aPV)7tY||MQ_n2m4ewkUnR@wj_JRJSmE|K6cG`xx za~0?=;;+LOmqmokh>;a?*<&8@Dz`mMxm)@te8|x3&-hb9hBaffK(?Ezs zE%?I=d=q@OaCR|_!oLHwXZpJvgHC)v-BNNa2;yRnh^bM5FM?Fxk(VSOSPU4ba zz#ER(&`w7ynqN|!s&FDyRRC>LvT^>n!tbxas*2|As=xp&ch@N9ua!lK+8!*J*#|{{ zYnkUq<=lT)5Ok60SYVs%(h1TBNP4ysrhb$j-giQqw==R$5+jWxCB<2r6pR>zPEz#) zYfZs0sLjfq9jB(P2<7wUw#f=WBGm_6?e>WU)&Ng3lby!=5xU4;=9@0rUeOMFQ?+GR z`vkaJFWoq_tn}U`EYvQg@uIf&bQ*GL_$1N$7%h^b?DetsyV1>2@c$t2z#2-ndyJnc zSFS@&A+O`;J@})&l?ME;`>4DCGk4Qh)idPfPt+|rg~oA2d?f|uvn$ELnCp^E4{^O$ z1}gz&;Fj}|o9f5b-X2A$)i~k^8hXel#t^AiE)APHx518qx4&*RBDXL}UXy089EZ!H zg8UrYhiMa>_Ks}mdZ@}VZQR}c<5z$fkw;R9m;X}{={|S&*wQ=Ed3&ax#rZ_Gm0q?c zGE%Z|K6GJ>N|Y(C@+IL@DO6WwcCP~bbT`JO@!+pV+wgo7TdZSRa!k5o|J zY6hC~bP8X%;_$JcPY^{F_X23JI(qBz<5xSqLsLG@)cZVZhH3u5TR#uW$?AO=k_IV! zNC4maSl>qDl~{V|)u={cKOP}8XZZj8Er|YVR8X_RHxO{~g)OI#pNi%>j&lccGRYT^>*&3YN&esjGXBz z-SK$2)ioX|L%KWCXt=Ulp5j?y=Hq`mf|7CxITwLAzR%}9$CJH{J|}+wWW<5vq+%c6 z0oVT&awFs52dwb_<-SVSWR%;^%99LZNwN)+SJEo0M9-m0Kh0Zw$T%Dc69QOE zxokTZ%_n1tYU@F}oNsu53)Y)bNGu?f_i$y+WJ0rd8jn?Y*cZb(DuPwCTCTfgRYRAE zSgA?!L#`DP5JjHORhT|(D~4sCPb>&<64C6UJFEsqk-LiI`CsYv#%{4w+$=kyoj4xH zVHf)_xk9#sM-z%y(!l-~=Tk4H1)rT{T_e67+D}fOu)J~|hnY@Z-o7nIpSOLTfy@QJ zLHxnB*MR`AoP`6c^7Y5uaN+rtyUzIqM;EsV*?!JJ-Hu!`=rQ_!7C)H%FzyU!Z@VqD zyjY|w@Xn5R39Vq;$Qjg}7y>0X6d?VPcp4Hw^ttWVE}8(tZ9~k1rzp zm@wjFXN-ZJ!OJbZSttW(1A5eAN!$Lb5sLG8ebr1^EtXYW3Cj=2OwNdrMq=k+B5rZe z!BnW{JV;>$CdP_EfF_j!T@bi!}73(7Fs%@ z6bHX$X$?J%sfjA4%EWRVp#l$~3^%784v}CFNKE+IHgCuA?FbONl!ZO@HDu{S{^^yY z!{0vx8_%gvj?u_(16pD~dgeX**lU`H%rAH?N3kZTF1=WE^ra`!<#L zC8Kv~mgYbA)PIfM;<2GBY``9XBTidGfc?w7x9^Q%LJB8D%a+0Y;Sk>xKLwt=3GTl2 zJ+4(8%1SW%psjic6!GmwwXhEx1Je#~?5@x}ib@G8QYX29Z>sAsgx>Sn7AWan(pG&| z1@uJ9I65~R`FcnjRcfwl-$?3Fux;T4<^sGJzKC4!Lr(Md2=%6{+KYX<+z+r`PcyO& zKH09?IP4-rqoreXkSzzaefMP)w}S@I)k&EkPf@c-wtw#KxzmYtV#SLU7-WKZ5`cdh z7e24Xf^~d2@@cqatN(Sp?v{od;OY+}miS&8)h|sKq~gV9W5tv{{#z(n?}Eixw^0?9 z|7fIY1ll~Cty}n@Rp@QQ>W~v`2pKorc`0R4v%*E3@;tMe5$eCYgMD(L<$Ly1ZV1LI zEKpGp!z#57wsc5bGEj!<%4mM!3lfcQL32uHt1IE#`(v6K_eCX%&?&pb9S$>V%q1&g zVEKyO2;7jP|2kmZ0(pgiMc&kVpG_SppJxA$nEnY7DuA^D7jhut_iJAP&*3eO@!}Z$ z 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

l&m!M3Yn&;7nt^d< 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

p(AbJUp|=V zIdNNmkMl?t{w=r5KtI?!Bo~4I3aB=s-)>=X=%2)6gfVZb-W`MzE$FI6*_=s)EW{GG z#P@=gGHMt}2R(B;10T7}TcS}3Y>hY=dVj83hXHf{=t@ZHN)YcpW0TIofqiin#(P>d#lRPjRyP*$DB~WEtIfwBTXfN3=&hLM5OUW4H>)I z>jI>FVE1`TE&k;*Cwsv9H1UXflOzAy?iX-U;x(kS4jRye2qi&RUxD~hO#L?q*36mo zlf*Wti1PjW#goNl4srxSy_%uVwXzD4lZ!EMR*2K39 z8*2ZvjjNB7a0$w{i^Iysf0qj2YQ40l;2$o8wmW2NpO-?WKEg40~Y(Lb510D<%ujp+#EM>GYtzn6oAe!C)r&8W=zx5tvXo7xC33 zz%@AnpUeNt7b96PwLuZ=jGqVi)XE8rcL~~~fIog&_>Uvvt6eCPqxJhCjr+~aAdLG9 z{aa4FvyA>6&*oQ_clmaE(ug_H3n(KK1(dQw(?Y)G+M_p<0M;!IlJ`zaiu0oL@ih3c zYQQSbm`5H;Zc2rq@%uAw*TTZC(|mEp44l~>#sR}Hob}f} zZ^hiB#x?zBy|D*-7{(nc;rsX%{te@l=4D2N&PHk%nW>m8T{4k-dATm!c=djkYszsOWqX|Ro43UhE+#gk~5d zis~%Da5g!>P%VQ33SYrCy;|HA{Z#5PXqR)mo6U_)2f|cBukYDHmMkNcq%1Ap7**INW zcpD!p{VMEoBY)KcN(a5MYv$b`jX9(u+5U5Y&(LrX{3lvf4d(7T;T_s~_5j_bGJE#1 z3tcZ$06(Idf(=_8ckc6$!h;1<@w~c7U;{Pf`PS;w;kQ&_y{*f^L9l}Pcnt~=4l!R4{DIu-QluY|)Ufyd-98FCL z^WX`Oq19A2HcP_PK`JYZHA} z#J0LyvJT%|5#%udV@dt<@!v*4ISPJG?B3EOJ)!`x27BQ8cI%P{ z_4-B_D4X)zPG@j#FfJNuzSP%`(}~S0!;+#&>T%!|1snFPD(d;Y^8OvauH5octYNXe zUlzpjyJnXdo8Gk_9Bz!KslZ6nEY5J*-=!ang>y{_`qd&q7W8gn@{!VkBIrqEluX;o zNZ+@gYrPqPomcUA!q;%sRk0+Cii>mLtaS@65^gp* zVRX|+QJcS=M>O-OKqE-IF3ishm{s{Xq7ljRW)mh;o8xC+SZY5N?HB>AB++2Z#A3;9367w)Baz27zX3m-*u&R-hIIK~wPP>8^;H>JTCH z(I^TG2Z59)p||fO=E(Q8MTEw<*r>W85FS2QIgSmUo)%-KxV>;XMByI1ob8IuFby@; zC6E!`Tr{xc8(HzMd}IZzHjQ&ia4$N;+bP2lgD*(IBd~a=BaN#o)iZ4rhkbia-U4vB z>USrtk4bxn&nfp&C2tRdOOb*0++k1Z%{}gca8>Ttt(iAjU2#>vYF*A8ZydxU5vZVG z7kuGRZtQmiLh#>%UQG7TtLrye$HPC ztE^(e9}KeU&l>zslCg^Pr#C(%k`@VG_w=iTLhrV1!2uFw3B@Oa?CaRzR0yP$XMhr+mfc7CRa&IPivQqmtcd=|~eEDnESwIy5 z4ZMpTX3b?&9*RkITLel1nbsApM6Xp4MknIF!SawegwINJ>s`srQIW3h^6YRX(jT*1 zp@z&LM|PK)@JwZ8mrnvy@Ap&+-0FunMwq9Cjz!VS0^#zRrb*(Z`Lr3*4w86)2t2ut z2A>IG1Y!cdRdo}7L+q0AAHD)gc|A4(1Z*=5wJqC6Lpsi*#yZap-rEvSg z;6?c2Np9=u!hfAV`7QiCXXGQ=B^Y&ut!2+C&%GnI0m6VnbVSAW?7A6DSV~65f~W$k z(M{ot?`=-_ir2AynC2C^%bMA5#kCk~s$2<4nt*NOY5H3+IF$+G=S&gpgoON{Ll?=- z`pYE^b`!e?sf{&^YUndTE93WbYuR4t4vuB*)Ij}IaKZZqRQ6SPV=<`7?l6P~(dC<~ z28IM}Brg(X=Yv7~<5kWtv?-V=xGB*kPbF}W6z9!Yz=PM$r>t(0>AW2;ZAT@j-r{EpdW`s02xESd73&5$B^Y2VBEY_Oq>FW6qYPf8{8|waAp{d-uCm>^@1L> zUdzPjPl%|;RhWUqjtAsn)(iZ+OC->qJCh>MRa%zfdM#rPn$4Cfj3Bh&n_mGCKI&^&V^J*PtLma&3kd2+KjQ}oQJN+ct$S2m#_@dNGm3(9p%rRO<+6#33d^4gL62q0Xt~X3R zI=|c6jfZr95G?4(Ve>~f^1u4Qw&#EKL)G|>ORpy1Gu?s9El|Og&(VR6ZV|iX)q0C) zmzBRi$XbdoLemO6^f^VsiM0fR8oqTKKNcubX4}XYGoIbo^h)=tTb0M0;Kq{EU{)%( zgDqL%R8kSd82=a_ObN-@j2gHEW>Q!bB3cSXJFp1 zD@HRs8sQlfLoKEET3wRUsuQtS(du%4N+&iKy-El`(Y9Tqro_k$Nzis`zs+@|(s6G> zP?_NKmzJKxiMIR7tL^PmlnP2Xd;o^wJ%%KXc*NdkvCx$j({<>MXs+U4kACSO2s&(4VOa5p~ju0-kv>V#vl6?T@*XDNUuUEDTjxk~i15)N$U7 zR0CB{QZa0;!l}%U&d~iK!v=K86C&cZ)NCRM{h=Sm!fATnu#~!rqU?&bUj&ecpEIB| z@M7r3`!lZ7n971@SMPEHe!hvFGIK(eE3ci@u1=rgY4~bIAEZ)$F9{!Hlp?<4O#7m9 z>w$l<WiSkxhV* zC)4F7Qj;c0?X-lj;mqT_Z%s8HW4*Ri5ETvy0Q48XI8{*aW+F6t}; z#lVP(hc$CsiZ(TazO>?HWPuq#mG#=hvTw&K#h|oa^#8o{wT-vzdHmY?68TzJl$}#w zVyTkNY|0{B@01m1LmW)vtP3Xu(+XjlPC=i*2WKTk&LWFt+qlz@-@RW6B0`OKfsqtJ z9d0EnE*uT#t=51k5q3u*4ShMU{&ghFh}uh&H{$y#TnUg5flQ1*I?db4ZsAM$yoB$W#~o6)z9=zfrGDkXCl~ z(>Io0T5k7=-MPf^>r|B1{XJ+brVu1KSV(^(>uOGj$IOKdZVA6kw(CrFfbH`EpSYV2 zamnqsYxs8i7@f;(euw;@P>cI4 zH)bcZLmb+&3>HYsklMsIms`yM9zPsJn4H;r=oK_hY7R;SI$ab1ldv>-mrz_&#bcda z(evla6TX;vrlK|Wm4zkHWuuHd%6~1PZZY5WcMI6>%Cnt3>(QTtTl80TIHoBXAf;22a_^kx#3xTQ1 zpGHZ$7u@%~N~E5$_GhgFr6$JkC@5MrBO;H@r28h0BewHDbd@>O1#u!8)CZmCn~Wd25(8!1t_8*I=vtfynE zVvnmU|5^&@|0A5LwyJN#Y7Y26B44(cmXhbG_6^VMJa!3}42pC-k}(v&_N6mEjS zhj068f0z->Wyv@KDPj3k|3M>U<=s3M+**N8w#z{MAK^C*5UeCza*ekB^; z%wvww&PP)(UW`Xp19pTLFO5|~W!*Ga&7?qs^`%^`_oz7XA@U}@>32_q$9uj1{e0Q_ zW0o)|*KpHs2eE#t#o%9vzmE3{;U7!Y$6$wdCJ|!!CGD#1EV$>*&~L(>f96m1p2SIH`Ib0}xSd#8{uwosXxtyZTOLQwCd=_me=|COOzA z0#h6gp7y&E_-D&Kh8>Jk#`P0RHFws|qXhJe*6q24Y+fc5gw$sjlI^wQ4@k>izVL76 zv0QWOx|^2FUs4^OjU}Co>2J_kRnM2S8&#cP`0 zSp}j;D}4I2&C$v}gb)+CQ=Q>#1XlyNAZ&2ea9aOMO-ZcE{sugJpy&pnI0sElK?!`W z=_ZfnDZr!VM?9~Ngqn>Yx1!}z*y1nUL?t8$Cb$~e=gNFE({J-BWGoOl285b%c$W@; z$2lw)y|4}+ZG{oxur2w1Qj%V<e{58~ z_{iZZdBy7BN*4!T<{m z65XYBD}Q-ll({^1XX^wv+;^4p+|-FT1O@sd=RIK^2b^F1MIUIFfh!79!uc$!%OH#M zm#QuNM2{C^5jlje6oi$LDdfmW6l=E;I1Ov;dHMQOc=f@>RMNi;K~6lqBOf&fudwu55LO3ARe{d4_1< z8Drrwej423QdBY|RI%zg26Vdh)chJm@RmMjbmq_R-~3vzeWw?6Gmi~da=Il0v4{_>cv&-C#b)@<22aE|U<<@*#A+^H_>p*$V!|Q@J&RdC_*By{Dj&oh@RGLH*dLHI-6XZLj9H3E3%Y7wgZStEYX ztehxG;@G=7?BzC`hW2%#J!PhMnfRygE$f5>lp~JjVXHG#v&d^uOwfG%$!U@Xp31rY z&I_c6szEodV7G#t^D3aYL@?5GzN(}kd~=mA)MfuCFMiP{tOo|x8;X0=@NBe0ZQ1*5 z+7$I|ISceG?4SRvVe?O*)s_AW@gJd9HzESk{bPl>=Pcg;)DX1XyFJHP8 z@X(5B*Zw0@U43w4>Z*r08!Ahazp-l8*(079e{#=3m;+VqJiI5}`tmLBp+T{%$sM-( z5?bvS*=Ew-&Eg&@Y@Ay?hS5#^3ex=!(vvMU+h&K19?{T=1QtSM$>VVH1Dh1rw%UD8 zv_im+l8C-dF!Wg^PCx}=Km-~6De2_T$|X?{65UQIsVjl)G#o@b+h>uTK7HohJ2F6b za(WRWm|OT``bFGS=#@kjz2ks3xOOy3D>o0Sua)N#f>K6I87sgjDMpQsWOa2Xl(}QH zV#082TJfF8*kM#uc{WkR;tP2Ya9d#YjQ+2__t?_adE&NA&|&W(QB@0k-_88hlf@gc z%X4JE%0n0&Y5XATU zo)R46R&WT2A^j#v3_^#3y9g@S$Hk|AbZT9Ij|;-jv1>EcJ$K!}kHzqAQ0<$>UCH9V z*f1XQzi;;D-j(0p#Ou~)>t%^|p5w>`sJ;;vL^lJAnlf-B0zJFnk0Fl*T^TwZ@#Ep$ zSoD|u?2lSiEa)Le5er$bF}V8p`kn`67=F@$xXZo}#?|&>Q>Y77F{X$f&fJG$e2_Z$ zt*sFSd)K2Ibbv)^O4nZ)NGc(B3Pc(Jm{#Vyp;HV-8tD;vo}2%2tBr(- z*#yI11%RN^$)qvWAOcwLlgKHgH5^_@pHYAUW(;jg?CJZD>WOf7#N4;j^nyH~?I?$_ zC)lTgdy#O&`)L5TUln6DTw#6P#OOsC$!L_syDIE?IS_dMYB-F2aEuGRG3|x)0xe6C zLhbb@8o>&lRkvez-M^-Zv3hdw)zLQXb(!G_e$&SKRpEOluVG>ojxs_?FRxWo07q<^ z2Rqf9EWM~WuU|CRTGDU%8Z0nU=>nVYFLXR`l+hs@di&Mz)j6e-IV|AZhGimo*u2_QIZTAe?) zi*5C^AS}Df<8O64(9QF-1@dlzPD(_lL&gf4HsyF-5<~G4Upt$tGxvRm#h7s zS`0y|SKc#Ck8{!_uh~brT_4^a9!vtZ!}*w#rhqm&nxH5a-A1Gq7_1?tX2L7M~Q^5v_4_L+TqU^vTzaBYWq!zcOGRs$jH1TMQ8E zM{eUA#Dhn^+%0OjX0>1km#o%hhTab^UvD?uW_eRhyWiSgH>`_#4~ksEH#Q!$81-kD zyqiv;Jz7ozm!_SEy(Fi(Fz^gcTWsyKk3s?xV!$#8=FCF_GT8?AiqNy54;BM#8hqVn z@^&ymy=6BxOZKcCzVa!yF&R0}h_*bzPsOJlyobqW>P}Bv@=AcM4*h)7smLWw*t^3@z%Bfrsu89NzRA&=h_xA|6Adr zky_HcJNk;ivVbysgC~lUNE)G7u)S2f!!T{I{1VScw335+SD1sC`0XF)&-=WsUO4140!wFiY z=AE8S*9K=BgAV7Z**aUyA3{zRPh?g<$QF5Ikb;UDB?LRZcloD|RiD|Sh5%^4MN?R4 zG^q2fKL(UWM!@x@qY2nWQJOYwCehk8i1;Wa^4U8)+l1PDtaS>$EDB%03cYLgyl?rf z9MI&wExqGv{yY8m&;kFxo{n!fOTLe~-9Ybd;AvY!JgLc087x3CV@4XU2VTrTKP;%l4^*Ne_`yt8F^M#NyWjiALeJvhK+g8NWsN!$jj?7}*SBfOX z%g4v2RWJ*oO}gHjEmU-8okx;Rn**`~jt&r>H1T7TJlX=Ho(3_4ylJp4-XAHLhrxFn zdv;B=a&7@@>z&g)irMHsMt|nfASADiq~wYhn2tB`2cd&8k~p^o2fA31WqnO$7oc|0 zgie$Fgqh#=6)}Mhzw08xMDqLTu<$IL4h(nNO%gF8g%S}^*DRl&;@|Z<_W-7r-r-oO z$1!snL+j$U6Le7!2{u% z*(^qn$cH_5Q*y|PcdwasM(SVR&NV{A{_c@YEWww-HEOwUN!v9Is^9WXhh`mA!tspJ zgK;5xl^M{8?1Wp{Akof+vW+O);}SL2-3^Mb`&@kvR$p8G$lj@=+~xT_>e(q-WfOu7 zoscUJwekk~E*w>oXi6Ja`TXkE9=SbOT?lb>$7^NAf`=xiMOB0)DQasOq1r5+tGtuo zIePzSj#48yv%8J6rY>5J-OdU=$bO;5yNjUbEK>NLv6{|U$rvSR;ade#Se))_`f9qc z>tWjRZkB_-*GD+0AM{4!_fQGyOIPUr+>P2G$%P{6coJXm{LinTpXoIz=mB@*<&D{Y zuh#FV<-HfEwlM7~Ab_c!MU&JfD2((}SZJ?9x&PFe4B^%1HmvczavR&}vw>mTVs?Jg zf!IZ$P2uL4CaE7K5ZnM__iJMZnd0s@nO&|6s+6ajS=P?Ryi@(^Q4T?0n(4QGBwQuF62}HY8)>w}O zESlM$kUReRwI*}L&0Dd(Y4kZHm5Yc3TZPJ2*DP?aODO0YS+fLSdY>__ zAW9ajVkTG9v}oMNS<5keM0f72B8KKKT=WiRj}9y%a$;gqYIMwVir#NQw_e|wRT2_m z*&HXOBK*|r+Op`Vot;-|7LR`DpPuSbF!y~ilZX(Xgnc0S}3{$q#90Lz0g9z8J`8Yhi{p*wmRi#i$p)2Pn_+b|YVYK<2Q113)s z9z&0pDt3_aJg07N_>=i=?Gp!ZaDW@e5EtjdSot-DY(-GAqdrAT8lXJ1n5mCbe_-S# z4+m{dMf4qI&&Y%2=hNs25u)G01nA|6G~!!c&(i<5jc%$z`-s3m zQb)!A!__-~*8w)|!m({Nwr$&HV<&BF8#`uWJ88I+#*J+?wsvgu%k!>t-u0aCm-{c6 zbI?aMeXjuq$&_pfa~s$Q)InK z3pS_l^WKohxk%H3Lq`)5PcGR~8XChm&JuoV>g8eSL9P~hUh)8`kW2oF-zcveBUf1k zW55pUQ-^ZBF^)Y2c>g7AO%0%kZkLu;g}fj7QC?BXUqq{mT9;Ss!Bqgaz+{Oc@jBy$ z7$|E{oQ4HNsh0eQr&pSb!n0!O4*jxPpsOJFrI-6+7EmrFX9mEGE|TmVRwpkVF!KpR z3>KpWz7Zx+_Ts>E@p3FnF$UtJ!lFSCk@Wxa{C0a+>c4(Ikg3m8flBiv1*K-Tmb?Hy z=-)k%E4G7h63VCYKGxBowKPry42y4T|0Y=pJqdd3Yd4Tr19v|I6gGC5QfS)a_oGXbe<3%cXD1%JX5@&fioe|;EZZ&_>1t|9Tc82j@qtEj8c*5 z^YIqm+Y!Cax)q`B@y)@Ip(8AC(Ni{1!Cfmxd^`>SFO+;ch78O;mXi)>Lnw}}%C^97 zHu`;IN0HT?rv22)QxCP7^6-M^?e?mPXP|Dphs|-~$3HuVHQNq3PnjJ$T#6LBt z4TSJ`D3OZPGO9dwagmy(GK{!P5xuj-TiaJJ9Ey&HFu4svSTM!la}ycF8&=yw+0?mN zub&$Ptnmp6l@XyAw>8iT3-kC=R7+DANP)*Mf2_OCwMg!PpC727_n$s@D_=%M=UZpp zi2pr5!x9)@O9fsn^7!>n6TgoKKF00L9f0;$ViL5iE#7A5S9T zWr#V*C=Y)-fWCxa(!ZSC{G!%mmS{amtR80*EbDa&PBN#gvHC@U04gdIs7rY5UCtsp zv+#aNp0ee8ERj(t9m>EQUqrnd2jXjlGQF%0J?`b!p~VaFjkt(bc|g0zQMZ1f{5~c6 zO>aJ_WH~li_x&IfU&C~p+zLJibIR_zkMD}ApT(R71N0zpp`lGi&pyeRLW-h!GCIr~ z9*GVvoE6VY9#%ORdVoTAq2NnO`%nL@+X<7J>j_*v&MB6ZIwX$n9?up!D%pCL3YS|s zS$Tu^LqQ85LB1`M`n~>K`iz9wd10MW*}>(fW;_;0!?#te=eS8#-N&o4aXqG-!Y7X?%W^ zwEP#i!a;BWFD8b=d4q|!%`nvAEnTr#&w>r^ux4m-V+Ylw-B``|bYts1Qmv5L_Rq07 zTHAs=3IhDF(qTZzGcpU>@g~*K&KV$*Bp~D2tPf_hN(Y3Edrp>y4p9x?{Gu;ItN~QQX>hk?xB!BWyg^Ln6&I z0WEr3+rWtzAO%;2&<8OU#BP(zh7CIJA1#u>E};NmIa;}bf5a3mLx>dR(%2=_MnX!5 zGU42_eYv2JH`u~ka$8P>Bsv^^2>WhKVCoJAB`2ZaH6xnY&1Uu#1+z-#i|jswwx({w z)#4zmN%*HTWu|UNylu!UrYRueV3NHV_lx9yCVi*qQV0PU8X0Mi=j<>4zNMw9IoI%% zHYlId6~=MJJLb6~KX}In*TpAvnAojDhUB~dX!iuCr!jOg`ztD3`|pDL4-v-T6*(o6 zeH?r0K-o~6`&$X%YkYmrF=th|*m8MYz)E&+eD*GZ2C<>%co9X`BhWo7YYGG}vC=+%Pkm&X|g?K!D6zYwRcd z^tV3izSsEZxqMY|d^usMk=k4i1i$~2EGjCzfFtW z6@OsYy*&}aMoxYn#Ue7on=JkT#duv~_3j9^p5>&2?3k?ZYs5Nut$6X&;v$H+xl)S9 zn_}Rz4xbRYq?U)&Of^lQvydD5(G+Qy|#NiFWw^ks{$iK1{krh!v>zx5HgK_$-h8D2A3_-~| zi~R%Ml*CJ32+hBls(^i1bI$z*Rm=1dL^>1^z@BFuj^5{FandQ=$Ktv+t5m_&o;RC# zM<|+Lj(*n)X=)})5+^c+?sKoHxZ-gG?-K;sqR!UIsTFkVX=9U4Oj?+)lrp0pyDpQ? zzfzR5W>Bt@Y#7m5?u&>KZhIJ~f11=P`nV^^?Ww5PCL1E!lTfbpfdPt68$&EH;JIa# zuVQxbxH-$pk@{JrbAx8J(zd{*FgItfHQ%)bv6)qu?@$&TUA^3N-0Q!!oxfu{6_Hv? zv-k3Cg{vHT%^P%bLiNiPRfX~9YsgO0_Ut~v@YD%P_;ya$hlu@}@rnP(M371khXu}= zEy_=KtA~}2OkPTuew-}4+B~Gl=j+kIB=PcZjmPMzQcPCljppOX`hB7FJInHC+Ug6q9Tx*GhqDt7x*FrmzVl|HI+36 zz>%ZhzK7UVB}*_n7&`{r_Yf4$$%vAFaP?rx+;#G=Vr{4oCyI=XZd5*pkhC}S2elQ3 zsM(Z;C;Z6VN8fXNVx;n7-p1kN8A^*k7|qwKSXbWgf_M}0gK+Tz7uKr+HU*I5 z2EP?BaN=za*`|Zc7c7Doq8EZ0-WG$-u$MFzSX3}-4mHJxaH*#VYH~+pvf)$*b+O=$ zj#jcbdk)v|p)zk9O8vmL4qo8&!p_ngFCaoME;po=AK)v#B4(23#G5RIblBuK`^?mT z5*wY$G@KH;LGuM|uRq&Ub2UMcqtO9iX4N0$6NzqH1lybC6GysdwM0YLXO(f6+=2JK zIJ2Q%U?W2=!a`9_ksO~Jd2>YMgmZqA1BB&(Cj%KbR=k8Bfn*Izq?wG3*Id^yG+ljF%nfRr% ziuDZnL9BDLdsQf!Qh*`)b}~V5+V@=xXTjaK`nz#1(YKG~i;L~Zk9Csg&X&#{Nh4Oph7^1aCO&5P(P&Bi z2D^kK=zoqguP;-tE2>x!l7epJ&+R#}j%v;PStG!r*N{|twL8M`FL@Gw9PJ7*p_FKn zwpZhek%pSwaYkRjU>w?sS%l)pEE$}nsK%AUz{+OEQGNsYoe2(uU2J%-nj<0>C3(jk zFoEPMoyEQ0?w^}Iy&*0iiQSh%a;x@a+G~PXGD7V+>nA*fa1k+u`VzP4`ED2QZdcnKC*Y=m=y3nB>POP* zfXqCJyb-Z40`h#Cf+nkx>x5VPRlyhUEO>*apM2ZIMh4^SYdur3VSZQpnxX!^6pV2T zc6@I)2NIOY!}Jv7`Yd$9xT7jxr>VSx65LNXT_5=Tx8xXkh>V?>-smSMH|?|Fb{a_f zO1R)q6*C#DNvViH8_?tCp~!X>3j-bFBR$WZ!`FBp-+!vp<>`yHd2?WV9e(*16S#}o z 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 r{(A!XkH+i&i~j#T(feOxve Date: Fri, 30 May 2025 19:37:13 +0200 Subject: [PATCH 06/42] refactor(docs): Use even smaller logo image --- docs/logo.png | Bin 268509 -> 166611 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/logo.png b/docs/logo.png index be4a37902a34a32c40fbc3979eced744c05a135e..14546ce8aab087859ab2eeae5ca81b0bcdfe2a5d 100644 GIT binary patch literal 166611 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]wFH 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%0D$dB;woE*0iyApq)C#KO3S*M@| zeRT%pj($QZ+T}2R?}EsF?3s9_-^()?xVU_Q46F*-!b4#pS{gaE#6)54!g=FVDCzY3 zaq5j-RR`@>5x5?hC6e@Q54&HB((}x~C7s$29peFb26Sqs4$C-C@x_XK?Kzf$8+94k z*dN53YHOwHkvofse6vbW+}LCBUs^l&t5|vR3WIG7_DBc6s#VZ9QeIHjOAt&4?M5hQ zw77~=y`MEWMOa3vY~u34T_uGa!2`Q zwchfp$63eCry8&hDYr8k-f06T%z#5I>rA>PaJxk zyvIKZUwt*qpO$P@e5bCUCT9H;Yu3H=-O9Ar^AlQ`CzGq9+_}lpl$Ax*jX>k5lCi#E z=k0{o8U@rzk?RF7!XOWGPXFcz))z1JRDypjU3t!V_+%g@;Up>h#Rb&QKxw3E^-nvL zP0J1G{Ts*U!zOHGl1AE4aI0=|zd}o@tIdDMxlD$<0$oM8JS97qJ5zq7*MTkhw3Scz zn+8G*_&gs(mO=BrCI23$97cvxSW1#c%_}x)pJu_V@X=pgg{qF*piIr*3UJDa@|`S& z!V^(dC!M0Ty7+zgSZO4~Zoi;%Pwb2;6VcuU6_qOxJ znjy&z3Ts93B^>T)yYtwjNwUTz6Bd#T%4$J;{5ZxCH4>uuR!aDXS<>W2=b%@NjlPIe ztNb%5*r)EOPz$!s)PBBUJ3Q-Rze$Sh#$kv^*Xbgf=+$rT9jf+txj8SVP27gzpH1Xp zZsgkczwd$fGm}C-7rGf24h{bin@l6jZ`_VX`^C1wquf~!AeJ8RZ#d#(He?xE?rC|n ztEh1~GME910!10PkRg{elV2Ux9VsN7jHx`5wWWAC4kKhcg{=L~pu?O6T~8V6qm_Q? z=r4?@z1+d^QS9MzxOK*KP2CV>*rHbL1!V=I3YWpJCZfwxb2f!ytE0Wwisn>L#);-I&d2n+qr+QPbpS6-M2lo zH(Hub@Gr2Q0VxsQ?1$$cVP<%Oi3+_y9JfCh|E+~#uMpgPhFp?1mX%<<|BX0CXd`uV zH1#(54L=C!2KJ?S8=y#=Rbr^kAx@#Q(b{HsmV?aCNOC*%T)`*B@@UeFeOg2J$=_eo zYW0)Xo&Inj)COUX{hSfiT9SrrYIeB@JtGhf=q*GVY~*!UH>M?>^D#)8SAcDFHeeRq zs9~KNdsim^fqQublqN(gc*IZddvHgBOFNq9;_R%7Uax)96Eq~31HrFS)G=hzH_jJ- z=OP&m;2U+h&S5}ncH$VnpVoHpdP+%F@oiS%{3Q>;eO^ngEy?1vpL*1@z{1YVCMw#E^q$*~=;QhJGnV8xcO^BBy zyJezHI+!xBU%g+P9-L$YP(+b3-S>PM(5v~G%jWG>c-cXxfl|*l;ga#Y*aGgKP7w3A z*h4c&O%up5|DP_4eT*-P?njF8391hd5w#`InvONbquBtqGASo5D0C zPTRg0QOb~z8lrQ%FPJyecigPXrfM;`8Fj`Kc>08uNR~+9}toUbyM0EO&pwyDw7u(63HD};%&UO_-@uw5w zq|3O#Rq-mPe{cXC(mmi$^@{Ok7J^T&bQy1r!GImK1fiH(S^L9SBdS;JiM!S-%|zKA&F({P%1q90c6t9c5BHO_gm6XENpG z_*uM^+3g#glAy(<$ozqmNl8%UYIT7=`(}hR_)9W8V*vM)vKZX^8aqoglS$0X)8i}Z z%N6SV%S!Dq<5(z~>o8;yL5gBH1_vj96JSb4L0)Wfsc9SEXpl67!FrJG5@w{GKEK~D z`h!u$oi}vMhKOA516>!?)uV&qnfo#4c{8>-QLEbqUWpTSqSf9gF8&9CL%@^_S3{k6 zBh+m9dm)^6hn@@9D9YQZX(Fgo8Ny2(zdz_3AeCaxcwG2em4AgsyULOy)jKI z-GrlWIyfjwZn^a5mG&PQ;Hx8z{6$O?cW|FEh)^73P}z1{pQ*O@)H7vw{Kh%P~H)7E6GAeAG3_7?erv@P)|f=uMxlG2wgK{xMv;in`TX;b+{|@)Zp=z#U=g?WU{Ogw(S)okdCQ0%g6JAiC!$lL@#Bt- zJPSaHF1VH?v&WZ?md@#01wni9;PY>Q;XJ*1_g~P!MuP$rU3I<~tEi&dQRY5p#gD#Z z=1Mpq$rKpfMTf#ghzIdSR9z4Rx+TBL zpk@$HvQRCy(&%<6!KdEGZwOUn$E9&vo)~@rYecT`s6U-p%M~| z;S`Z%q9iBa>&(-4t=bd*#%E@r(L9cn+BI49VkCN!cY(z?j-xiB@bPsuTD6Uw|Ro=iq7 zd3Q?CJF_uGEHjdqm$rRJ)a!$0!Z0ZXFJO>tnRB2Pmno zbX4#|Qs81)nb(uZnig<3a@hkfm=^QAnns=u-5(wwTp9=e0ISeA4@px69ZS;2ksyu; zXmH6+<^XxAav5fS1}PW(sKFX9cj9vqt=eUw55O|8E<%&$*+OGy zZ=&WcqK9m7c3$6$^UoyuBl;^f&Sx&0HUMS%LBEshDlk{;4m`ShtE z3>{FUtE>L)E_j5^#(1gImKEI6?cEeuBZC@GuHO@j8&>`{lOi|l{> z{iE7vdpVsY`{014G+2;U@QVsf@AM$rxBu>MGVq_9Y8T}wi{J!vo5UZ{#J#8BAGVFRQv*iOTW-sh5s65>0CjARA2Sc9lo{ ziH9yiVnyZs3bjYmLbLU)ECtnZdH+$$zWk8qURa=fR}}{9^R= zim@(YMJp|S%q5X2#jZ~=WZJx4a&@L{Kl@H}r_m9<3c4z47~@PlUKIOjf<)wQURb!B zQ<)wt(9b&PmJWJu{$zJqIJCh#P-x>|2Y<7|O^e5k%9smFo3omfu-P^mY{tnaxq!F4 z!2BnJvd*Uc{zf>r>SG|5h|5Wv)RCUsrWwE#nTJo380JU{>DN_9$246gFG;St-q6Yo z;ckUFFDi}Q8&ov{71N@hVDiU>6Q0!Ov*(W1@Gm2psYkX)$shDIwvm>;KrtE{E9-W9 z{|1`uGTe2(ll5)UGfV9~mwFYcf@#j5d2lowif}sUJ5YNGQygbdH89m4P_Q+(=e}0V z=<1lmJR#zilG3a=r%cC{S#(d%_>u5&m0#fM{$0#n)f zfsi)gl$6kv6a&vuYrG~_wISN}!PmInSuT_Uwtty4d+OBb2O`Z{!CXXAGU;lKTs1Rt zl@l!*|5k*Bg{}UK%;7XlK-k2hZuK3CH#9jmd{t$^ncu~fH6@!q${}X9dbhVbFOMHq z<9;Zwi(Sd)F-eWybyolFb2%(7#+DPj> zqQe=wnw_TCnj&!XS551yEs^_}geH`hvjaDK&N>jU#VlQwh-kZeKJD^Unb!85{de_` z#xU_$`&Y41ihqh}t-tN8&dq$t$7mJ!&EL*{5(xVK)<%ksPE+9o>U^;{9Wf8Bt?GR` zBl`L+;y@&=u1dxQq}j2m<6Ujt7-Rd5#eM}PiIgHni)J#1C0W`A$4j#w08r9uqL6_~ zFJsGcmds>wXotI$UrLzN{3{2&rCejxe0LuI$Td9?CVW>%UL{d6=94==b^k3ND&M6Z z`xlAmUPx(qh64I|^WS9z74ng$5E+Rx!g_nFWHBh4D-hyTzlGSj=bAmx7fg&S=n4{Siw0`@Shd59?5dnR=&1P}^51nFI7Xn>H9G z=Q2BWj~A6fZtoOWHOBQ~jYoZi_Idh41cx;kv9X<7Cg%pe6}llM8U2}~xld4lM>yZu zaZ`kT_C2fnESvzyvum}b?C4cv)s?Q{J#BYYq^=tqx8-l@+4vu!Ss&AQBD&Tpr4%-| z|D4!XcL8>Kl#@i(q4Q``~oGRxUCqKZ6{}6Stj#(!+pqJeOMJy_n)22 z>;>8@l^g(3>sgx%$g7C~c*UX5c3e4|&tbJ!YS&|evw%d$8S>O>8TbHyQ=hj-GbK}<7b zdmwNi_m9qLL~?OB^=w;p%@o&0+EYxkTMko`N)BmHzOG`idH8rKp$85vG7U8~7&H%{ z;&Q;~{A;ooKh5%{7iSi@1>!s}Rpk#v)>Cs@vr4IS(d-IXPkGIUQSXE`+om(2$EO@L z8VuX%yEahNJ^oCZpVZ=}e2>E+sJGW(EL|6j7w5nN|jF@1jQU*-a~ zW7*!%@K_>YHE(DON=HlOp+npCj;4}Oh&D+QQ%-^^#Lj&M3O>7#bppo>RU&*s9Ck}7 zz&&+y?LH`X=eehl-7}QQU9dDhOJj$c6-Woc3fg5qJoOK&^6)3Z=OD`FQ!q3aBbeq( zp)V`&^#Vso%f25Kh*Tm(KVA{kQUA)-%(c6KV6Mu#Y$p`K9+PSQ{$xdgr2{EUgi&KI zl<#~z!r{rp)9E_QZ{QXAhlJJHk9DY86h$&-i1Y}4neq%f9eFLyX$*@w#e5Eb>LtZa zYoAs^J0^}nAj5X0g*LqUC2yrgFd<$>C*0~l?6*=ooD#IhKeVZ@l~!scQ8Eaz2(FG1 z+yX2%F_sUt?9Tl)%m4|2jR8rlXq!bP00r$dx_ScE(Or0C%DhJT>};EpwwdMEND@{Y zOjeaxdhDO3Vb?4C)Lq~e(hFVJ!t8wtZVA=w=#e<0`mya>er1~t&s?F|H6r=>Fx!QI z&$XTSGd`y-chgcy#^r0qH#u+4aySFJL3G++*GRLdl3&feWavyxJp6J_1lDiR#$6*% zPHx{H?;{TZK2soJu+he*^M696=Y=%PU`A*YhrmFj0xPYk3IXqlgXxV-%`$DTS;p$hU>Pe2Zl|Gb{ z6!r=J{;1TKuat=Ch%{qtgDWK!qg=`TO@&`=BD86RZXAQ5nN)xIAFR2;K8;9*piPSQ zYPvJ|*a_CJf5sRjO`OjPxMdrT?^m9dTriyWtuhz5Wj1Foy8T5d%gI;b&5-{d4XNO_ zC;Wk5e6%%mOvnt{7y%Bj!rF)21P7Pg@#Ur|I@sZ_>B!$Io=o6#s7?>Lxs{+lOhnjtU#ZIERdQ}uZ{?f#} z`mic{guFFobhZ|9%a7aL+b1UM+$pXL%X?8!Xx)DE?@VN@R;%NvjWbv_3-Qp=9*_|!c%V~wG zQN~1+vn4Gbn5He{G8MI5sR5NwU#@|5ZhZW0L)Xnd%!s;`b|>UUtrEq|31Mbhj1Gg{ z6emB%HPu{qZQzl3aW&ae$oLBW!31&~4*TX)EIRQSvhY<#R-A{6*YPb~StgngtjNHj zObm!5vN^GAaiM8%A+@t_Vg06TjYR7rm)tuy8SZox2m{Ze+};O%2t#lmPI-3!9#aB; znfWMCnip{Y-6%owT!?+BuOVj>TVIt~93^3ukY!hhO+FU0<}HpuZKklKW_RDF$0r0S z(X0**(hqhTQ-+V;{laXUpD%Ed;WI$Gn57`FK5G`3CHv>59H-ivmt|+zYmx@x*0Fm^ z_v3Ea3UJNl8TfiT3nla<{M?$c(bb3JFk^*mBb{qjXYK$m8?}^NrYSsP5$4~Dg4t>! z15hRK?BTVLn-&=94CUFJUuYeJKS{G-a^bt=!DUDdGmI38LwLUG4 zx&3fb(Nodm@&Xb$)+S8)0l;j6H^CPc^_6FLHTa#XlC|y% z*nQyPx&B{f)WlP6oj7nR`vUZ4=6IFx-qHUibgP!AI=JIBT|~ve7Z}!nmxM=!M*aiB zvd2sO``IB}VT%VOflvOstBPJo2+y1-{FbM`f*rvRGPOT{pTH7LC2_ zt(|FMq9oj4Rg%mnW%4M3HBt08BD{LJWZt&XT8`_FDNaMsis)bF?mljC-)9@g#Z1G~ zOY62B%pN50)>U|*v**M2d0b%jh3q1WkCS=I!rN@jnt4FIjS$@S=DEzWa-*fKZ&?*T zq0F!rku0#9k@Vf${AS~;@63jg&l?wG;;flOBUg)63BCAj0p@)vXWQH3Va4e|uvW{p zbPE9df@RaJ{F$MmS$*+3YNJQC{75~OCao)+jjDQ#67o$SuhZ0YVz;zYs4{5i*Pre` zKWasz&)FFMJur0g;i39-iMZLDDTU~uU+(fWw0Vx?0{GT9?f#nqcc8KSckqKJcn$}U zvcGSuAT4}I99ZZgHKQ8a=%UU+IT)saIcUxlOc5iU6rmk0^FsmEb(N-e4@PSVxjRLC z;!jatZ64|!Ay@>qQ4V`wXl6Y{n&wV2(BD%N>)%9D>p+B_Mx&0Bkp?xHkD<%sz+e=_ z>~^=BYd_J?$y`%N&Uk7iae384{l($qlCx1x9&M;ou^Bh6BBEFs+J?S^g7BaBL;JD@ z92~E{+e$%KEuF`hP}({I!{0yUc*PPVQLOHfW6i9@@2i2CTj1uI?N1>o2WrnX<1UWj zPX2qh;n6TPAU+;ENZe=O73SsN&sdi8)Qg*tl{R2x1s5?v71l=v>kQvni6xA-`3Z6g4;L~_?sELoWJv? z`2oMdR~9wyF1$+yvp;n7G@AbeZ{QiUUeTvXZWT(`M}y0Rt1>2lmnu5TEa5FY=ZjiG zxv5pS+RJY_3d_o|u3W0Nq~CJmpw5e9?xMdE=red{uEdWR!-3OmQ^GEMoi+xO2N~|Z zqYC;{=&u>LkhRg|pc$2-kgrYT-z{{`+oaOpis8TA2Mb4AVi#eB0CmClzDkA+ zHqYcGDM&I74x8gh1htvuKMf=PaRW3u>n2mhW8jf%2s$4_K=VL!;f9a?PbKjMld<;=%(m@f|V-M??DxC7S z7Op87bnMepvsth3=u>5&@Ncrx15x2m5aV+QsK>_sYL4{d72+t0_2%q_#&<;@1sqa) zp8?)J*zyPxfgKZbJ7^d5tvms*3vUB~Pw`%$aG@bN;dd#o5m?}it%%lB1o6M8XlQc7 zJxNacn8_?&$;;u9Dk{hLw!9XdP%Y<>t2FVk1;XL4QLUq7Eox8>YFMzU7+0GIm(2cQ zGaq|fB=6Jd+YH-9rdtB;7}o2!#teRM)PWI=Bh+1Z)AhFDs79Ctior_SdA#Lw9U-M5 zVdkv{K9cgVWO!72^ZC()Ge!AnNTn8R$K@#aheJXv_;gg{)?^6c>W8UCpnfnv(H`*V z3Vp^hV{tMY#?W_iH;RsE-}<;zW>i+D=>YF*rlAsbXYD9(nvHZ^X%w~bQjS17Y9g$ z=|Nn**Ok}sA{=lrE8)O3np`eJS?|bBM~uyA$E}=z>PenC=XSZT+S970P9gL(qv>w9 zF|z0uT^I-F{@7`pFZ3umWgL0ycoK~ zKf0;eba|xJazRQ>lH||S)SIY!GEU7HHp}`4#(upp)zUb~mF)bV-?i!00e$|n{r&|; zUO5V{K(6y&ZmKX|TIkDwi>j`-DnQ?xZXM_;_DtY*MZqRSHe{&F*oSqBAc1e7Ry0O# z{@MrK@-sxu`(a$M9sjH!qy|1v_{Es6;)0f!*$}Q4e4!@?+n4I%YD!{-geR zV(Gygkbi4tx$e5qE3Q6n=`md}ooI4Oc#w|T++0KW*2Dds(RMxrY&obb!^1jA21GYN zk`fmiFyJrqzSYg?0#j;r`8#@mngPx)6JBqL#mo)*WCm9kKl3+&oIJT2cL<-&^3z)%EJf{YT;gzvcO-Z}QRAPN`SQ49J#e;PiU+xnX?F2^-&l;&1Mh z-f3Wk@0^FZQ8Jj(Ef=%{7W&NWZMl8qigbyd8kx|%BoFr)_!on(l7yH)&D9%DQGxeN zhOc*)Jg=!+6kRXauL>rZWYdxnl55cz*u7?s->^o3q%+`pCD1U<_0$P2Xq&Hsb9mZP zD=&q4intMhp+Md?pMkQ|C96%3wyL2seLr)#xc7zAA$QOXNN+EzCmt%x<8wo`kZ#%5 z&WaZwG)1R60KZ2W04JAu^(r#9gmth~$4$SSE2RgBM{l{uqglZcLp}N9yhOejmC~sH ztT^HS7p9es%~bjrU$MQ7VHq!4&BE)eWzh3o@bfDC)EHsgqh$5`=Fcj{k3-k12*7nX z@S}eD(Lp%$xy+OM|9`4hwH7wgwmDDn3ttB<*uEwMz!!B}d_uY_-PT#}`)_aeme`h# z6@lm854FM*W;fr1;QNM))eg8drh27D{DxQaWs6udD& zctnbM3)Lf%>t8JXY=#ucmGZ?SrV-Ai>^4;CvdJ^n&d@=|$9Er~@NC47Fv<6x9CB;3=1i!H$BWY>7-Ze6{qu&wnR+_~hsB;y-Av-g>Tf>a(Tv zq_-!Hmij=0sa%dhL2vf%H#eK?VTY@OU@Y@fb z)5w_MUu#XOiEzlKxD5|&^Pd?@0S`0V&8BWyJXv_|^5?wa?SmXmS{I3HJdx~Kr3b6R zx0V@xyL(;O7T<|*8tMzuZn@Jg6b`ibLd>*z3Yn2#iWgkc-;<8}ewVk|lkI!XOrioP z5DpW}tR`RmIM7QiVz#^qd+zXXZqroI?SLjd*Oq49dG258NptKYZ&EH$9$#z-w=joX{pQ%nc5x$)fKo;<>4)@9+U7c~oN+FXFj+AU{_(Q+P64^9 zv9&Ajqm{e%6>tZ5a~1x`4g@Q`*U!hSWuksk81Z=ga7#?!(3q5ML;&n^9$<_*@KWk& zj`%~(=Xs(I;PUGB;)7qRq5hDuJfytuio(E&jBzqV49VcQCE2rh> zRPIGe@&Vbj7zwp>d*G4J)6aO_C5BWR3vJ2f4SNaBExp(p^j1P@)7(KO`dK>tp0fQx z9?T6l2-Kl*Q$9U_0( z{W%BXi63d6U)b2R!(EQEs?QFXljC3u1goVc#F5V8vQb4RUmL%rmg1qQ0AJ&dyx%4Dbr65vE0|<>cp?s?p>jX&UH#Vn0j1Q6_~Xhm7>vsH!qhFi`51STRCHGwkN9 zA;KDXTX5`xpezmF5G(`FaGqa2_UeoQ&%Ad_v_&K_CR9-FrHEs}u8!aOvZzd;e zaXR*;fU&CZ+1xyPIvJZWx^%Z)RZ&!)3aSl;Du?3X@D9fK>L%?)jVm&CT%uY?NtS0o z^R(X2FsI@h!=!WGei!?bZB{y2J5%klwRkwSlyOZ7p|0c*#dja9knato`6;`41-+8| z(isdA)GwTWzU?T`8=4tMU_FC+c;>YL zer%>2bUhHiJ%0@4KlcM(8--WRtWIy<*8ay!pZ!0{(e$~0F^z>F9ao0jdNRr$^7ST_ zw!CayF4Sv~!Bcy;(ednZ8}APj8iWKB$A_%oW7=Ub-}HmE^EUfHkwO|G3YrNhT?=oM zau8j+y7g{A!ME&uN7K`lsB)D@Gb+_gGiiz&i}J6mq<(m&TVV26+s1)hTb(`Vm4=1R zch6vlYrsm=CYM#94&60}wub;e^_@LA8LQK2CiCGrF@e*Dyi7PU6?v_ow|I(4j)m$& z=V6x`0z>W3HrfOpdhy&8J8P!Zg<-kM3KmT4Hh}SlPqZeRpHfGpnoJ9VC)H7N7;m5I zu4sY(F3i@eWnkXclkmG?hCajJTi4Iuq9=8;kIFP;)mx&TW%0uIJY#S9-CtcF*1a<+ zz4bnVHc$eXuY9gLy?!3$B*Edy08L?k~C|Y>J15Qy4wUD9wwWjpQsQ!)9eb z_a%li<4N0Z$f>czRj7Tpe|nuBkcyU7r|sZ0p9|T?%H>fcC+NP#y#!C6J&g%Xt&(NS zWz|Syqtu~gkAWSA#GT%|&Y|&PWCBMVb&f*ZuJ*dGBDaUS&=Cx^KNU$vgnR4e(@ zvFfat;^{H5o518suE3P*OC_$qlxt_5bQsFP*C_O0;|qe1HzZ`oIG)SAtS?Ozs`EH} z>GDwKV*Rx1t%2EJcTOR>PtZR-a4hol;dQiQ$|=dKF`8D-*C_rmI04{z&=lZFbA?}k zq($!NY2$#gJ^nDnl_0bm#JNW0^tNue_ixKLl{@f6)B9YiC*JdYEPwlWsLY$3Ap;%m z2NrsGML(}nCBXuh+42)8LN;`<_}fJPjb_6QI7p(CBo4DCb=71%51yHb05s^mzn%|f z40ucrNNr^HL%PgWCAISZ5yP$~u(zLJlp%o!-$cq%K~HxpKQj%#{20M{y%YFucvOKB zn8Cc6nMf8RiB&l6Hb+9I2E;Ida$ogRZqUvtS^W1>rGMW8r!7d89=`y|5Zt^x7h6`H zM1gK1N?#J&kD(OX_V|Q`vxQu~*6?ANv`JIR>RC%q35jl*XpX%wnTeu%M|H(yU5}_huHPNmp{DrC}r?$#SO0KDvmAtqFZi zlyV}u%IjQpy6ALe@4ho2!*8U=UqeUv`8QS3t&k`-qs7&%SS#mt@b+q$j&gq$qoQm> zcMJOB=k!2`sZKWrV| z_Y5sR!U6F(AIB)JcRikLRdTUQaM7p{Nk8;2Lzv01kPUx0Qi(c6YNu|cd-MVG#D>a~ zK>J|Mjc(k=z$gB> z{7`Rj6!;jJgrEOHVVG~g;`tgQY06?c*TfqnFDqMsgL;hB{VisNx^xp1`s!_N5cWl1 z=;}Pmr1(cYY7%XIPS785L~+O~TOE{T;5&n-;WPRkNC>Z?qO_(x$ulS6(taK<-=NLp zr5SCL(JH`JH~LvV|#sWPE|u%RqiSB_0dJMx6-{o)bO z{0jLDH7}u7q!FH~t!XYU^D=g|f%dX+Tv$}=BwG0SzC(fmzV)WIld9TsrybL$^PWLL z-)tUByDjgd>P9QY{HRw?y4aerD`MUDy)Iu3|7=!e@LT1gby$a6gUL+iU8~wBNnzN^ zbp3H)8oINS3Tp(lb-7YBTMU@ey;Tt?MG`;FmR2?+*8F1Kd+j)eyZ46g3jJiC;uBeo zykvh#fy{xqY5a$WN8F2eo@|2zlYJ+F(i^O3t4RRZ9r#iP5PMx>jO#iA;m7}v=phDa z;RJJVC0o8chknqjPmP6|LntsaoR6u^VR%8u_13O$s4g(B4*U-=#tmKo{Pw+I0=rF% z1Vb9CRFTr6nnDny(h7u3hT<>fNr z%uSzUx@n$rGNzniS;Z|?-sijKqPuj!2X(-;@W*~Uc;mU;!|zm(+xxVh+yF9febeB7 zPMP%oLLXOl@|KKJ%aYM7ME;;y=AH)tzToX3jt$`lezP1g3!)S@w#gKsq9IstnA>oB z&2;8Rb9e@e5>}}xOS;V8F*M4(qmm3A;|jN37$M@{#WL(aTL6?&6Q?pSU-T!yoMGsB z_|ydJow;O5&gLlikR_zrHa18d^(U&ZKAEUvFb47ubgO9|5p=h+WWI4mN9Tg~6!%lC zWoEofDQrvGO?O_$p(^%B0rORXB_iY=j0+oI+}m9`7T;&WoT|V$2UimRRT8B zYa@)OTkk6V(Z;h9n8`EXdAS`VNW8rk?-%76c#|yPy65aLHJ!=jimeEzPIaIovrRC} z(?=to_lWVJ$kcYbx^-A%_@WN{Ec9dNbL1J^8Pg}j1xl+0>f+=J(kMgY=yCvOPPb4oE4ZUmRV- zZ6CsrPl(7}d_8@OIrt4r^YnNMwfY`j+M&0&Br1jRUvHYk;0=4}*VRrpsd3wN*{{j( zK~WD{L3-}{*OR>3ig=wz)lOY@7_j0@SQID_15qVLmnpOIqe4W4GxU_wtT@CF(<1ha zJLegV{GantslYxnCPWZkS|ED-S(Dxq{K30X6#yFkd7mD*O)X4h2;Sheo)EYCsEKfv90XkgZ*g1wpkvmx86alJ9$@LD>%r$yXolkc+NO1rQ~wAe;bqQ zBp}!q8~4NZn^cVCXGd+gD||YhylSGYUf`D7sv)HJpUbV!cJZFXGs&kRE|;h6uew`B zaturL`?*yv>3R%;{ARpUtNYyv-^lM-N~ZOyZ`F>C+QIOPTTJ9x(%5c6^iP4Ud zE{TyOx5D1|2A}@0{g2&Y6KsF^K8@3Mzgyo_)_KY$JgA_!F3hbml z8mb&RS0kX_^}Z4q4R}u-5WJl0G>=fpSuc-X@RNO9J*OmNF0$m>sn8sc)O_Z&(o>3- zPnA00N5^7l`u*y=Wn7g2yZE8q6@?$Mg0w^n3wmy4qxZxBY0$e_q-Lsqpb;ldE1iB>i{d zdVF_s$hF!8zVc0&>`XJR0mz2+4!``h!hR#a^A&_XLlg*@aSn$6QFQg!#|0!$hxzL zp}M4SUy|m!`HJ;;UoJ_p11;H00WZXNs?qLgIlWplbNi1uAh+&gT=?*r`1LE%N4*kj zPy5UyZYpo>j@Mi94zLc44R~G-2)gYYablHN8{Gktst2=VFH~COW79`9o6)F|I2ziI z4?W{HsuTn3TvX;IC)Gn*MDqQUJm0E>CpK+;{cg`=d_EEuEof5jp>5O3FQ=KQvto}K zs)DuI$TV1-)?z1GQD8x973puDl@b)E&LZE{DA3H# z`kvlG843i#$#^wzuwjF~1IcU{i_>`WU0tr2qSqZ=tXwPcEH0o;4Ka3yw4=%bua?Hf z0zrefQUJff{fl>eUx9~n_c%k|_08}9t%7f0aWegL!*l?Sr6u)3a(AOj_ak-o;S=;+ z`(^y26@#8}kcA|oACP1Q2?<6kcOONvbz2amJPUWd)Z8bCQvRPkG8cPm7(hY%EG z#lZ05ryrRA2+JJW4Pk$#Zxkp%wv4S&$Trk(CL9%_h>8Qm&#(SaXI?Ye?(1O;EIQE^ zP~Lt^;_>X)oxk2e0Sx+nyapyQe{6vfrdD%o|G^EB+eg>=Il8R^Y|#bgePfJpH8^WT zs{HB$?GnAJf#l;M+`^9vJM4~d(|#ouSa00 zB9xx&Iu3RV#T8o&Aq!+BCfr+4Q;PmYiJA`=OTzi1xv1xgzv8$y!B<9WjNat8uT2e(rd*gq$`hE>_n z(wl$mKOv0C+ zjHl|(!o1YL5=hau%B|ex>a!l%dn4#or}Ut15+a8PO+?5)Wlubq6-px_!*` zJIP6_@wsJ&gJg&Rz6^C-6<3hy9uGy8T>Ikm-Z=C~!clGgmlmv>MXC!;aZZeT8bV?v zV*B8zfDE)Ev3XX`R`*H!+gqfV9*3-8%2R|4tfK~d;|P0A^zi*@w(&GHXmK3gRoI$U)?g#nNQw<4J9n8o zQ`wp0NN)K@iPilzjQA%Ny6P`47rPjD$T)ff0VCONl~i=LS%kVEF5N(`?3g{4CMBZ`{vHL`N+|*^$hu96D#FJo*I^c z7$;w^`n~c@IFoLMg94%o?#@V<)fNt%G7uZ^W!c)`O0X2_zliN_po!#mfZBdh(Wqus?ay*t+cq*2#+x7I|BLlum% z1+yS|3b+G~Bn>B{zFL%i*asjzM_!jLhWL5`n1 z?Y*E4p;rMpkVQ8F^Uss4P)?Cr9=90)!ZZuKrUGfRJtJYsNGl*sQ(?p-_1X%krc%T6 zxPY3nKtV{cvs~$!DOh6#Y;)y{8=Zw%BJ3GX7elv?E5gz(-xU3u*VJ2vFvv77z2LB^sxG# zmOg#H5Dl*vv-p3PBqvC6B>`lDRL!zyTGubs?aOF9tL$gsA{&XL2^OOHZ4{1##`pga z$;ES7F0)QUVA`0J#X?~STcwK@vxqbat7;DR_D8wwbG!b_JW>5!My8`JWM|i$s@vfG zRW-n*ofvTAPxL{i^U~}2IQE+4RGaFK5B3|5pf_sbHt)KlBF#qCz67gO~4^B{-`cLH9J}4naR8(T1I~ zF##}o#BXO*Yh{E@IkmR!ltxM&(-_Gz`>&H{R;rCVe`({EU+VXvF+4Y*(bVDIHh7(Y zEV|G1G-5n=l^3%s$l|DlYC{J5ueXeoqYV8k{8>=<_m&oxW~HX{78=DsIfmRiz_WsP z=i`g8M}_-`Ory1dDMRc3?Eu;Z+LeDk0(_FWr-jzUFi0`@f1a2;)eU2vG9c0SZt&w9g!|9s^`G8}e-Yh;=0On^J1$0PJ?P})r-%LIRP(bpsd=q+ zM^%wHM9-gO?fd+a+eOVAsSn>UC-&!%7@I_K!QDX^Lvj3tg%%3J6b&i8wwlKv1^uEk zzgmL)Pj}jmSb0_CS!8?Ea1ybH;3)*rvp;i8j_2;ZsmoKCX9fEnvdfL$%^bMn^na zOuh#5PJ9L}ncHbgEu*#_{Z^jC`~1{Oi`@6`CMsGhBG-ru0>14}1{Ike_NyauH$xc( zEykc{01uziqPtIFo;w0LrFVE&R$;PtFD4=+MoEhP8Z=*<9(@Q!RcWAboi7HN3U(6X~u1iIe4=_z8Q*F^oloIIrih~EFv{KNck$&rf8a6^Pwq@* zX61DI3%vWB7M7L)Ut}{!D&ZADdyR}<@H6vyG{OjyV|HY!`ZjA?QSgpHfxq1F_riQn{gi{-Xj9nmZYhMcw9p)$t5Ice3fuhZ zNS?*pz1tf3=ve_iTCC(CWQ+ytV9B1`3kR%a8n3E=uA(e(?(>}qGHQte>#U~(^VEjdt(1bMe`i`JhI%oVY({C7ECQO) zpaDKFQDUxqmK3}mcwzq}d)a&i_S<8Eg_3R!dgOp z$`O2X^zyFRHAZ5q4k?QI>BTlk=U?m4sW!P+wHKcH%)@NeQ8RK_?O0$WQ<9@*N>(8k zb}#_;1K>;eO9qE2V#0IKxy~6G_vAkcOV(te)7!gjo8^M-s)RwJI*PY{2JJ0y;KPQz zkf|Fp-h#8ZvsTcD)m2*R8|}vMtD2@4uJc88QG6S_apIRkH2b-?6zq7a)uWg*mFf?1 z=5^x{ur1uCWN7Xu{|Rks#n1P38@y|)mua8l{UD6ZK<71m+9gesEUWN^}jwyaHOs^jn#LW*GwXIa-Zp=YD>v`H`?gZ_q)O4l1NhNSy!QjK(2v#pWD6z zlZ(&PN`<}wXcQ6IhHvuy@zeG_w;b>zI&5G*UX)o-xR52*nuD;RIW9Gl(O0QRz3B86 zmYj~NoDL;nyqgpSs+bzytxukhC$YWW-Pb|E5mxq4eoo|54B`hD`SBU7r9AdB#%|zhHm(_*LtThwc%N zR1VXf9#&@iVc@9Z2lY6#rr^F%c(LQ>W^fG;5v$QPU2=O%zAAXCCT8`$4c}W7$2~I8 zQSu2*yI@H~Kx9a4o1U5TK{nHc0*$yYht+theL^ixf}3y5EUh3gBp6nvZ3Fp`BJkNn zmyZNVWiF(n(X(yw&&?zI)C1LWw(^83|5n}z^KGy@s63HHl)36x6;EMLmE}5Pg$&Y; zRwl80g-O8p414I!U&Fs*2l63AUA-*Iz`s&otN%H?M5mRiQ<8s|s?Pm^5P>D49_bhq z=GZ@o=;G3Ua$#%l@k*;V&7Br?eQ17!hPn4_)ZaDN5rusBIWV-9NdH(EU4C2K=_|`- z!s07lTix+LKXw+>RlOT-`P^ zdQ5FZb6_Q<#v*}u@w3t1$km>Q`Bo3&^5M6mh07MC+~*b#Pd9O_{r{LfUye2_G0J3grB> zEE^^Zp#Q7q9jaBtl`ZJiTJ%_?=|sHLE_Q+tAnX*rAC(tg6u!-JK9OE(k7){aw0Yl_ zaKAS74Y183B6F78_>nmLqbYCJL%GL|2zq(_nrYPiXz*=h2zUr?IlFQdccm%*#!n{* zbn}J1lhOZ?9~?+#?A2wEFYF*L>&~*>_~XgBlj7`=h(-a4Gb*%>gZYfBv zrelh#%>qd=15RXlw|Hp+D_MrwFKK*_I9%=PFH$L}!v0;jaKoZ&Nk3Y#d#^vcrIIg{ z*bm+PYc+2et0ygKD*jrxAHdCA;CQpKbM~?81ox5_T3zPcy65yoeaDJv0;LoRB{6X{ z`9B#;c!~h6VpXMMpod!WVukt!IN5lCPCq~8hQ3%7Q4v*Cj0Q6(iLjn*^zi;6$8&W9 z6sBQ0$u0iId=wGp-QM{SAl)$hbUwWzHyE=pOxVNigVc`dv@$b-)fZ$_2OK2w6;;T6 z64Yzs|2PAA98taA0DGRIy?K?YhjGt1Wur0T>V`eyC#7_un@b_=rx@Aa2h>YmBzDLz z88T2UdrdA5&Npd;VT&Nn}#o2PUO!0e1Xho|AA`RG^O z!m=dIbk)tHT5~%()V?DGgs_J!#sG4q%uXk|&3u{_ub4`uUJwk<`K9n;2Zy(@FNXu? zRAqmS+M%YKo`oZuMZ^8QxUkX#;hexpO`GidcUx|*|7RZ{j4 ztXV>hL2Kpdd~QTk@Mb{*G>K!swB{ON`r`xf2V2maQ!Wze57pJ*Q|tdl?`QQwc;(wE z&}wqupGgkLQn2nBPhv+f4%fx>VnL_YyGWc0>Chja()hIOmOo|1`re6cUd!nZD7**v zMDa!qE~WvtPkYTzyS{Jo+LLL5WpAc;g3`KY*d z+jR9Fj6tMbCkuSDgofD7n$6+q&q#8?-{1UpR9dyqhw^@H%YQLS!%eG$=f5?S`(-ma z@uLLkyHL};LkEGv7Q*T!zy0W1ldRq44kG@gUP&ysAB#qBLGqkn8uK^Y${imq1!In1 z{au$A+N|y&K-)vxb@Kv;Gf!X@{IP3BZ`g83MKYbsn#K2(bz~ku4N^fUljYIZM3xT!hGFAVNb4;y}RY}6=yCzmOg%?N1i$0 zPdUX<7+I~VX930Pdsw2SjC^b?*T;zEvcLywvNAc(>gG<_Ps{p8EeGX_ZYMs#5GL@` zy2L&j;kSBS|M~_y^K(Y#a=!DmJ%Ectt(6n*LC*-&;IHw1A;K(8J!`RmzCGEUqG}PN z$wS`vaxZ5fceJvE=(KFGijWcnQ8S6WS}#(*f6I{77KD_&8r#pS8-4Le!~G=Js0D;p z4H$$bx5hJYq$(yb61tB&2ZUk!c-sPQXL=GQ{LmH*3t{;Aca!0|0bYhQ-|Fa#ke|Nt zR}(j-e5oqvUGAYSEp9GjL|x^4l$<>?o!8cuD*)5uhL;($=_Zs)G$mV+N|6k*!AiX} zh^2VdZ07>&p;QjY-ljTjkL_B)dzrhMZ4*;zqyhaM`4V1@GTs&T)|D2NEa1+_hYH6V z^FCOs;h=0l9&zA!0CvZ)hdTN>%g4O@*fjvz3mk57S@Y&!2f2E_#}U>4%TGF5f9{z2 zf??BAGxZ=kdi!J8D6rc&tzLtJPY7f4yDk5w2kptqCI?oidYk>8=B`0(`I((u;Bmsd zi%NF^yfPcNiuT}LX};F06=uvwMDb9M*PgbXqqir=j+S6aMlAaxoX}_CZ;^IJb3Z3p z?E!z7ggYO79DVGSZfw0CjgQ`o0KI)*he%pEMemfjfev*=$e5e3wAS{>?L%_+Ihz0Q z^-Nv1$g;KsBvNS;coht!*Ic)w6y4|jOC;+x%6I|>u405o{$Y9_EUJ!8iW@kJIl5(G z^gxnqzF=M$p@fQ4$KbHnU&^7d-Vs`nzm{S~dL+K`rueu@xq)MzUO z0eG3ZzW&%;a>TW`$97zI!n;5xjXAEz&~#ZfeES&JH&d6|Qe*Paaz=D(l3* zuqp5C2XY$s|@tj7N-x3gu-QeM2 z+Wi#ic}ak*F}nT3jK96U+PTkADvV~WwjIF6lWh9j%jkX8*cCScH$}o6nJR`qx?)jL z1UCx}w}}QGuDm7Tk>yxlT``{%laaOSAM18%$aZkWxI>8mbr3{qt1WRVsVdH5+Bt~i zY{o$dH=C$Bt`ulG_{c_Hdv$ET1`}-FB=@BHzRf%0=QgU81B*ou77bq6*cJl4FE$zp zAJTGYR>bH6<94wq#W9m%LYoK@0VQIZF8n*M+%`Gq;Eb9llN6No>HPl2m3#$4bkv)9 zs&*@kbHoc-7=%<`oQHyLKA(@7ur*X%+99aE7uLQYt=3eYA6RxBWb<7y#p(ITaiZGX z{cpUA#;(-*3H?=LM>5Q`H{YT19E{Nn3_k-&pg8m$AN5s1=0`53xC=6f6uN>wBb1uT zwKo_n`&9rx@XBrrh<73iwRhq-Z|opcyOGkvL`KTNELg|=vQRQZc@9JLnzA)1RZo;w zShqnVXS-s=D6(r{Z`{|`Q=;b~I+pM8bHJ(nzC5~j1Es3^cl_Yd^?x@Yn65*Ffk`*Z zYkU8;wZrAHfAl_B-OD=lHW6QqbB$F#Hs+et%G!F)g#m-9@h!m3ZK9ydKYR0!$LJmv zhI~qC7+AJ~=Jc!X*a~DA-C{C#&~*A?@+ zM;cp8m^m|k%vgu8dmCoh#+ydC^kVG#$-nI>x!sH@kvfuORkN&QnE7;lD({X?Us_@H z1-=fQn|lRs)|wao+yROAK0!(zUsKJTXBByst*uo_Dz2!L>c==UDn5aXHT`aZueyO%z(Na9~j ztECqEGMVbsN)3$t)vOFgRmg#MuTzlfPW))Ci8AxnLOM`#+@I5AgPZN3LUZU)n%vBD zj?7ZxV1tv00d*yc^c+ppACyU!+Inqre2kktVJ!@v@F^RvFj|2$f$5bHPs4YK30Q?~ z=Sc^h+k9p!R_=rH{C`>e_X;g_d=BlOhg50J;cuZL-7vFYM9+5pQdk3%M(zg%OYvW4 zo*Z9ox1OoGFTbLIHi?cRkuzUc*H4E3*DYiyu`*sR@;RL31BoK8L7${?BHm$z;4;;l z5!+ovEjmSpU4Lb7HWxCSdoTjhhdMlMW;I_^lH4LfA<}D^Uc?q>>*{ zCdoz%&)sYFbBm?|Xb}=9JV#x_p;#;JYXrb6cy#!Vh*^B*)D-P7jB~~qU1wFowSLw* z0?!d=8Hl&4)d)zNdN=)9Mdl5N%e7zCL|6|c=jCxh$#PBcMq5*nyWvC*c?EPOqZ z$r|O39CL~|wvHh4*{<;2nM$z;T~D7S!ETMtD-mp>%J+wl6Ty%BSf78Uz80r`6+{|Q zzEPh?4@GPkeYaNle%|uo(|r)@V=a$~UE1Stw?e1?Q2PE4Mc;S&ee&~&ZmmnH#$6fK zmSsz|>9G+h#k=w<0PF@HH>#P09$(ul`xn3|F=6+`>^EeY-^#A*L=G1uDtqL z{*5z66Zu#;UeZmKAvJ^B7HNt2RwkN7Rt}MS@ri3L6KY(vb#C2ry1mDO{9Y*)`Bqd!I0jAnA>R5 zkY1p?3RW$v+u|+X8wZFm<3Ix(C*c;2g=*sWytY-CThOeKccgzlsiTqp~k3x7)z|0bTSW5&Clmf!0Q8d3dH`_Q5=9N(%qN z-0}fmo8V5a+~unUhpsn~Iz>&IwlQxTixIe=bksu7B3i>-+u7tlxA-15_1tLYK9qcb zdp=mV>ObIXd}MRKtX>+!gvYQiYLI=w`CQ8$)x?@@_m@Z0`@)uI^EG7&-*@-kwDU#c zeL+xi+g+iVHrR zch3)+qE(^S-Pg1J2?*z_4WAF1@7}Juu_p!1v`CxU-M$I30`{VIHmlg-wrUnB&4E_iW_P}PU7(FW@Z{w30dKt3L1aS z3^8};zjEumX6FuKY$OD)4Tl>te@b{)F&vb36h|bus-c!rkT`{61DgTAEreW(Ghw`4%0&$_YiVDWY^=oC3Fb zpD^V+)$pLZbsL@gp}ckLtnUKG(0fx9P}s}O|L>TS73QD*=Ri$71|4EOHnk#4yI7v;Ni9IV^_z$ zw^&Com2ddPXp#WqRidA>j4(?99P=Zls?{=jDaH5el(Q9lauj~<^sY;?0Q7(XcanR4 z)v^_Z_|IIGDUchP?el5Pw)>XdF?rygz${u_J~Jl;*JywvMgrG_afWiT5qohv|Cyni z4_2iqYG!<8S;x`$Q@uF>b+tu?*c>&dF1cvM%`C6$Z`?mqIGU*TxBSP``IFASg&@{) zS%gzr^e@C^&B_z(#N&iYyvV*q@jQ1jiB(pkgT42>r^J~@H3c78>S(lE#^!%yb==G| zfnqD;42S96XUQl;vaI(@k53;clQET*tO#b|>So|Vp+n@_|8Vd~7JXax>K2#GYcH}N zGeTfw(AaF97=1@-f5T2&m7}a4UyJXR306t;vep>nCVLb$@PD|7ZqEHUzMpu@7JLDI zeB#;vYTS-?OEQ zc@v=TWU$~-U&5r4KxDkMGGwklKU8Uep~}(v-oiyrK+k0zn|7{a;Ii}k^Rn3`MTrKg zuH8}~tyd_vMQmFnn5b4f5E|8D+aSQZYC!T{ z1bmBZLY}k_*K2tB&4w>r9FQ^ajSs>!-o}0GvmW$VZ- zW%SgAk2##`=m1FKoyy7e$>HPERiJQJsN^%D^wrD z=BzN*=5scqFqmrDcY;d}k@Vj%&wU?RBsgtm+!RhzCI-t0oM;MySfSE?WicjzsTZ@k z2RrX&ono}`|Iu~FauKU&BIc1gC2V(t`JP|8Bj#jMTXWE$dVdi@cpJYS0+~xLX8PT)oYr39UX7A8B;^t#Q*$o#8ZsTgFTZ|gr5j!&;bkN zWn3=S5f4ZSn+794HI%UNY-!fYpqG=)MmvS-B5oxsM&$ZFYral)y+_U;E>MJV1IeZx z;q&~zppkhhFV6{w4`{5VTwpe#V;@xj{k6cSM@}~qCiHMaR ze77sFtk3Is&#~j(^LsQVrcLGlm8xiGL^$qjCbxq%A}P@0;F50JZP-Ue&$DCbbld28 zY-5#IJIdU}iqY7pxdZ`Z!ziQo>G2CPlYjYxyK0@_{-eqG$!prmn_~%YDk|rJQ(f;9 z{N}}O_JF!{Qp)D!?=$#n+T9tL-=V4N1(BA0I!`NIc$P2DBdD7rnF5C1yhbu+DyLF% z*km^7LXZoMlq$8V_aE3~EChOrg21{cR@UZ-i2e(gKNKfqsNy=ITD#uVIPiOf^8 zT+puX0M`PAA8o+m#vA&MaT1-`sPo0b`er?!r-P8J&bxR$9K*JbuRj`M;T(7l>ahAnGJ9bqSj%VUXPX>pKcT*0E%dip_WNo95l^>hDXrRrDH_pgnfa5-5|J0kwChdI*V4uQW){yg)P+TTFm~JUzolZM>-y(_%9DT3RjK=H zc}3*bQ}M@WtX{``|NGkm|7&PMbMC>w{?1QxFCq-5ww)`fGTKmAVf3L&ppt>T+t}s_ znl=jG*rIo=5~p$j%_!qPbFowTpDjXOv0&*00d0*EKh{`fu_9o30Cjm~@Fo)9hGniE zA>7}vc#Sh5C>63kIn8@(pN;l{Re%FeYmReaz4jeW=I%>VeY#zVa~a*PJNWLD(9oh;3Xb++`z|gghFesl41r|8iN!Kwt!5o67S#) z!=3Hk2R2(zjGC9->77#td8Qbbl{)7wtmgStWCW`L>S*?C^U3TEM4^U`fqiV}s#X3k zkLlkX+6u>3E=yjPq3ZVOv73HTLI36PH~;#=++l?DAOeA;CX8}{_4C!>ew6zo;c^Zi z3&0iG;&*aa;Lj{8Jw?c;zw{#}@c@3xqZvS$CuA(SIK7>3F zKY{6~>j(F5k6cSvbBifv~e58=Sal@I{?*KuIEVl4by>y8&ruE;>DGjrGI3(P#)Ac?>kxl#}LFrmFat^#{ztt>E0 z=Sg+fAy)>a2eK-q@trT++T#LZXbA#DK%?2*nN->D0;5OZ?le*7F6fKyv-Kx zAUdMI>50ZAjNaRVe*Y&mnG;xA&1lG4;~vKYc* zSLCCw(z|Rpum*M?j5Ut{re8$vest=#1$S5r_Z;4&YY;tIh+>??b=H)b;|w??m*-7) z4no&?&MX}Sb4h@HDbQ$kj`sqhvvyj7N4u~mAC&uz=(h_Q<|GvRP^X)P|C%?t3-SdMy^axmboHDy z*z$RtbRItVU4uRqsOT?7pQYPFYT=H*#(f{N6KtJ3>+@XdeL4LR4SLh>C}q?a;PKs= zWda#K*n2xEv&Omxq><8cF$3ay`|1S?T-X}HuWb^5(T@CfX2 zjzy1^3oL|7IPxUX=b?&sDkJWfP|VhwR_Hu5^hF0nTteZYh>n%Wp%f|zIRuHs_8L90CzULoR-*zs#5D;5Lt)Cw9Ze%;IDw}-iS*FZj7MxUOooeK1Gb_7`eu}$ZB-}N)$AD$x} ztx!=8FU8s`-B)Lk*y;8`q`giU3?Kzp8Kr z5PoPyTMtIS`&IXPSfH5G0^fL%F0PrEa(x19K+rmT##Ew|w{GEFkws}Y;4WCA+I<=E zsNG|XiCp{U*B%$w3(xLPYCJyIcVmr^=RHfo2mA>9U)k{RNX0BnX-u*M*ifev~Akh@p`(WS&ci4@4>73NeA zynn0t`?6kfnTqY#1Po>#y>TlM)GUkcmzF3_%) zEdCVlpaL8TYHIwLCwbK!0~4W-kx;3NCnwW4xrUCn$IK09$Qy@b$O`b^n8~Z>9oDt| z{gn%$oe9>ldnvRXyok^4R-uc3^lxU@E`8-8k7><`r`5hL^!4~I8@g^-ut#XX^&nY3wktW9|z^sxdr4?C;+H2!tzjY!K?_U}WR5?1s!Ud?J zR-FwR=y&AHj`dx#MvFL3h>-G>kaL%@ad9e7&%`uWXppyiWs0hqdW<>`_*OM29M`}` ztXPSukuj8;hY6~Js}WiP+th2F(+fxNVQ`KW)ojypfc zfbPzt?MY!~D&IbLGWc;bx^wbz&ntK>CwOz=yEBdNb19NK7Ivn(M-){z{LG+X!w2;_ zH3U^F1q?ZqSDV}T{44D~$aTKB>sc->1eKqDe`yp7FvKamLy#7$|8f|K>YiurENeX4 zW&saW0QXDyCJfN^jEL6MQ&6#$Q{5685Cg~}{ykAJ(>tUL*FcDFtS)a@S}2(yaE)U` zL{G3QMQFZ|!ptGGwaUaM=$My`4fb;cm5LB&Ny^I-_U|i6w>_y2G#YUveX!#IjByKw z#^$9P%59<%LN+xy1z<9F|FRQu5By0?y=G<$&h)chj`bvP1wrcZP2rD5Ngn2{6a*R< z5FB+2;qurt@^y z16?hT*OwqvH_g#2$k8CP0Dwp*l?o=QRupL-d`JrgJ0k93)+PVI=3I}kFWB6~K9)iL z`i-QTwggu2H+^arVH*TdW%>0uNmXvF6PTA&-;}vR>{ifDOWgvWw1Yo%PB+M`ibD3; zs@y!`f=eH&ciD=R(;VMLM*1!(<1@sGp2Q%oW#nfRIc`G;)7{I>|K zplbF0Xt_T3{J3u%otbN`ILL01amxtK#v0s2$Bd)_t+l5dwuo4r41Ha2D|cAlw~>{T z$!DHb^SKw14ac0OPX})!evHh0dj<76&1KXo{!2b%K>L%2u1L#|eWLj9rzVlc6_4Ln zEO5hMu;2you$INA0u$7C7Y6LhxWWh?y1;Yp=ZJ!_RFd?QzsvR8$jVb>!_dV!&9s~b z-jib?C@z=n))@W_P6ePmY20_?t>7=O-pbAjhdZ=%rX-bzWx>a;*CJ{d`&7!p!1&R( zym}2Rs?Xc?VIby(!_1uojau~!qo9>SUE_cTtwjIIOFn!l%_h z=huxlcP{Qx>?jo#7@MA&E1%jmeUX=|Hp z&AA|o&PatRSoAzhm-KmWwfM||d>=MPXKz;TGS9nrRHG<645Qg0KhZJVvn|E+zM}HS z))tfa1IH!#XTh6K!RW~MmFTSj+h?a#(9YWQA-^V#Z4yY~oSY`nR3sQlvbZ;?7&vN_ zx;KYWX}hv{6I8qJbWOgL!)@~L&iRx7;|Fg8?3GEDmcpqr43t4SC&%WLnSm56gaA z;dd0`pw`ViIgF@MEbN5ql7EM`xeEv!-nHq!T4NQa??_25U$9#V)=INc(5MZy5EX;t z#>*OWFIXq=vpVIpDb?z_#nPL%ODOls@TiRBl466P)JtCoxh3Gt^-0(}oBl18%F174 zx(oIO(2z{+c?D<8Q2g?OmM0e1IklTA&ejCKCk|K&zFvLB_Mr|snf6{S^}+4A+SDf< z=6e78u59NU9PfxW76kk{D|u+;Y{*Zb(jXUALJ0Mw0iXB$vUK%Rv8 z;1gCfK$bP-2{_~vA2;v`K&)kjr++@;?(`9oT}r$8v2aV$+>eZyJ=+rTcva7HG+AQW z0N;-3^(_}=g@8_J0(FSwjAKp{dC^f@IU&wm%W`wkffayUm$MB~m2bxHsu#yV5e=3h_1}w$Rb0kS1k8U_G*=2$D7=Vn?n_35IMp z|I9-r34rhjC(w%)a4GM;pzb4}pzjm&*ayt#me_+Vg_W29z^FrG;X0yjh)a@F9(N91 zEG`FUNPKLo8`ydTjW5Nfaqt%HD+V`@0ols7BR3j0Niz3_xVT0M1UlBp^wu8`t=7JW zWMXB1PjlY(#m`JwKD6}!vm|_MYoZYxjJ_HdT zwyMu}0IDwsCgF9(gfs|?Eezx@Rqo`@9BAZrWg?|F0L0z5@Jo2 zNGOW|w8CYI!2s4^3tkC+n1lv{xq+kiAM`a&1TX%`|^fr6%f%w_Xcn%jo~XFc=FCC5^gr_-?# z{3*dHl&yI7HzEvqA;&EC=wH6|eyjGXJX1YSO`X>bf{5I{@$zJ|=(`O~@zfL)8yqF@ z?s&BJb%0$HUxY74X7G z^O%LE@3+`-TufV!NXfT*6kbPxU@Og9_e+?nuz}>IVV#_2B(sV2u{2jMzJjb34x5!t z*4$LX09>pp#i}U6foVutOQCoJbYi%S+Z;dZjfyo~w{_$`+YWKt?5S@=$b~K;gV48* zxJ#^HG|fwUw*x5~ffuy%M_g7ctI!-8M}Sj+86x`C9aeWv!eHWHyiRWx&|M-TTUCIe zW3J0LMIPn-O#1xgV^8oFnCrXU&2R=R2YwQs%@y{L|KW9ho$Wu8Pd1ypLE3Z1Nc0jgY1iv2p+mDX~T<)BJuWh5@jvilDb;nz_I9J8+1U`18*%zf>epDGzaOTCFyiM5cHQJgrZJjB+|c^{N!Ne0(Knp zMzz%IrT>K-<6BEhUWElplA1$Gg+0s@4w9Y*|GXyuo=G*a(0M)QgPpqd<`W}u6*)1P zHeqqtwOvJh5sHsG`Lq@vCf}FM3#9TnOud=yg6Z4Ij4QX%J5EG1RERq-!lVl&54{Z# z7t?8$Q&S!6?WmXYQ|A6k??~zyyNysK_cOIBbW$kPklwV)X6etpV>Mt@2zs`1+6`PYr3F!PlB3;t%EryJ{f)h zI_xjf10%5spIK^w^JV~#;R0P!dUz5OPU^`?T|4#?@$+a@T3YNfCwV@f2=@obEWU3j zT?DV(wB^j|`KRgu*Gb=W^C)L0@M!ckqS3Ipqw}z#Ol?9Z7JQMDRES%xC zm4(Z|U`f%^FlAaIJHQN{Dkf^+7__j%y1M)G*MqiCN8rzL7GrwAc0be^R+?DI`gbWp z`|$7Rp&RHY)3XFzl$3m(+6}UCw8u$VBbSg(LbW+!QG>`XujC5US-|i=qms<|1(tM2 zEVCNFPqMJ0-I4Jd|NT50WAN*M?C4O4K4;X&n|ihG9b7qdt}WNEYpRxHvoyvx?MJsY zl^KOmgw7hk#xs_SfuiT1aDntW{aLC)3B^9>o-(x)YKm?Dns+%ul7iPxt_f=x5$7La zy-qk)(^=ACJiU0&L38h9Eu*U*2B*twx=5|v6nGhimnztQy+E~c6Ax=5tVM6{4e>rT zmu|3Y2UD$H!iy0AE#D5Us*5oPd8E9)a!16|`m`?Se$|%^dWiu&4uGDTw>}PJezn?v zt=7ZYIs^Xyc*rM5%!T@|iJ&1{TK{cK{dGdV%h&F0c=I|KjN-lUzB#&<0IER6H(yAj zv8cH75)EpDlfN8^Zg6ptkn^P#Acg!pu2!Fp}GL6HR| zB*%83U@0hdlEQZsA_`KYH$7(|dxZAGqv<2iQdK}X0BnimFXfU?JBF?T*+qRA1OO1b zDMdNeQMot+qUhb@s1liJNAzG z<@wHe-t*pnVXbwqx#k>WTS)j@EJ&jAKl=?o-F^q>)0e0EaP z7f52rO99^yGrX+R$!48gl8zqhsbnWc62;oEg#8*<98l*Ipe5O7&! z0}i`R5d>pjTBe1bYTtpq@AyJD#vh+~H3%qvg9X~Y6>XmAU6Q%bc=5LO&$Ka`(z3eH zFaR_D*YFfv+X}BruZpz?E^#{NnhhfT0T>$lRnj|7G z+%*YuDHAhBC5iJ_hmHNy46mF?$**?+M4aHFVjhkO?7?DjMl|@|`V8#Effbb2Vy9Z{ zX_@U{qqaq>HbW+bh^&bYe3vRbSsZ1-(0{-yoxvqt@7>Dk+-!lk$?kaN>D>L|M|OB& zFkxArsBS)uya`Bd3iui*MTI@QE`>DSz^BCup=HJlYvam{`XfS#)Q5WezH6*B*(9?G zwzXKzm3pt;F2MWNDW){8MEUX5VK(XqpDJ^9JR4wvciAN! znf?^&t7aVyGxPKhJHo84@T+SqRH}J!n8lE_(dKpzy%9@Jm7R6&?9 z^$PCkUpDAQouF){&mN#riOY;P$-%$dV9k|(%^KaXH7hw7kYyhyU@e|BUbVwpz6E z^e1gDRU3X0)t=M&J6ZeZnM6LMWGfA)6wr)xF`K@=iAx+Nnz&CID^^|y6v}2giD||? zubX%rmRXS8f94_~`vNv4EB|OSn9@b!lBuG3@(u9}y~6v$2I7oWUMN{eB4?cAVv|%< zt2vso-Dxpeqe$cLjW-S1S&GQ%{h=_1!aJ)u+^$jRJjYNovS9#ygh8uk*WD{ghs%_; zyTn9H7)IkZj*kD~ZtJT;W+$rHw*XfG=MCG-EY<1s48E8;j^$ki^ed5i!eX>gX}8lx zGcqKx$38|J(AaI%!Ds_>EzpMf=9JLP&N>)_3FRN|(_3LCZToxJS=M&0)`M4=Y;xDk zJa@<%b>m9N;GG4USe5IoE{~<0X?F?A*w9VU)u^PBVOz?JnR2bc>;fnJi%Aq4NpTK= zTiTh_O8drZ@Kye;ylU!sSg1-vVveU+M&M9+Q9!8z&72krh}LE(y`uu9ou6`${1p*Q zcl7Xj$Xk<%MSLNCW-wJYZ6y9rvM6lVlBfic#W7Mq`@f3wvjzl&dVAY9d!xq)vLXs> z%7IL?K+BRPRlzMkAS0EwHiB8k<>jAQW*iHh&vD58+9yHu7eMY~Joi0D=tlXM=b|X6 z2j0!45&z!}VEhKn#8!Cf*>io`v+w!l{|5TFp8iNyJ}ezs+#g6S%8^NRltOcjyL4qMU{=;!7vDujv24JgpTLw)S3i3rn{aQ72+>E1rex-{Nc1E;3{d{lfdgzX+dfL!o)D# z{2#UzD?w+_Dx1*#s>J0-fW+qQA2+}C%&oe|Dg}4)kCPR1O=ikZx6uE{1nYg3lPl&P zt}2%JJs-!7FFm%@3Jl)7rs4xJzheUQ5`rs&I%v!?0C zS$}kCJKAZ@j?+$bqd0gvr)P)hjX0lv_jZbBg4e~g$0Fkx3YQ)qn<7#{uMmhdgQ12# znOwz{p5<7|ty27oJ~efSh$v(ozAw07Tjh%~wODPrSILmT_a!bB;$mO?r^iK8WF`Zj zZv_1ls=Qjt@JhQ(RAoxddVBC0dU9%6h<_nk{(vIsne69K8qad2oQLm86IhY^^Q#7!U=s8HK zAPMhda$@ZXC#EdsST_6(KB~BXlZbi>Og$rV>^=#UN@Q^8JAJ|4X;Qo`DL8}gTp>20 zlE0~xUW0@emt|RBP&%f?*08!jfv+}wYQ1Ni9NuLC{};FOK${>R24Uy5eM9kqsf~ar zf>hL~OPd-n-GZlcRd&ic(NW#yzpeOznxZwds)Y(j7wo3MzNT_eg-) z=fm$o-|yk$_9FLX2Kd6H^IGDHODt}xrXHP@1ArqXsreH{zhi0$uDmdz7UON zU2MOkN`M|bc7Z(tuhM>b))|AE5lwY!1L7`HY@7|QCP;1*-3+ktPeID~E9^9mFz2Yg z?KT3fh6tAt@eai{#>+@nn}jgjT)`eMMT)zoDyJO0AMGT|F9~xV;PAXIhwU}Kb4Q-y zH8OLj*()k)y9NpJ;Lzzm6&6p(vj64LISHgyL5IS*(lSI7gTmpbyr@~GIy^OG0|hQZ zl36=X671qB5nECiXempLVwQ;{&+i{$1=^B2ea=R1>;WM4oWSjPsqI~eL%2h{7JwY= zd`5?(ik#VR!i)(FB!}CSBV&dUHOZ1TvQhaA7s|42teM@GW(Po6-bknB^}(rxAk}u; zxf`PEd|Lu-qigC3!RjQooD3aa9?{;JhK zcNw);Q>Shi!Qm)Pbf&v0x^w*T zwF()k@h zB0&K{8f8~ABzp3l!MF;Y62!=o(0KBoamhn(z*va-`~pt64-_j)IUV}Uoq*KupG!P8 zQ)D9GO7)X+D@K2Tp{+|QX`1PUO#Oug%T4{@WkGt$ahR6?^c${Yb+b@V`^to!Mn^$O z_yp2PH=ayq2ntzi8{EcI+eG?DHuu$!UjkNW)PYH%{1O{**fNhU{dP;iRyshhYIj^)Y4U==MFb~cjoa{3^5i-Lb9d>`7TJBYkaYvlMc#tPz5 zeUWW&oxNIbG0>;dNP#-#DHHFLkvDJ)KjTr-5?&xW6BYch^$&}f8{0JU;t@O<|5hg+qT&dyPB(QqhE*2EVu>g`5?5i^dK zEh91u@Z1|8$4Vwzm|x?z?1PKV)e&Z#ruF>d#uIAfHmW~(+sDTG5eW1>60{VS?`6zd zicYkea-;k13dkJ^kBZefyca?wHHD z&+YKPwnI8pF!_LAPk**dx1M2ow#WV6g>pWq{T^z4w#Z~%d&cIyHv#PR>94rBlk{d_|OUZT~llMNgCf2S(-JC`8aj0p3=Z)4fk6y%- zSg6~-yGAm9LUsgJ$5psm2h!y+IQnhy5z?EIWu2U8H6JVJ*R~otyUf-6xd;Q##Mx6R zMfVxX>ei^JTLWXJmgGX1_O1G5pdkxLX5%o?IOv>YD?+Pr*~yAvCydD%lTX#}HHn&= zUL=mL2?~G^2B_YmqS$sYhmM<}@F@fpr_c^xl&L(L^~aD^R^Di^2qVcqr|6Qj`R@2r z_0`~5y7v~}a@kBrGY~}vJPOGZMsInrG$_L`XPfifLE$2%g68d*IGnYnSVAf)HDrt! z=LM91Ftk`>D)G1!E6&AywxE~iLDVAk`NhYy9Bd&HeHUNN3D3`r{^ zgL9ml)uDT%69iR%nDFQm30(K%)Re1U4lLFxf{LxjRMJ)XwPp>!A{Q}LX7hNoD@1g# z{-!LQpdY%AOMEij4^Xf7IlgnA#YikYKcK+7LfTw{)IU_QKV>})&!`hLr^HDVc&(&9Deb!2W`7eJDV(+C#-t{JCH_=AIs97!z(%m$M%3iUPLGHx% zR7Fha4AfJpQ1{=eSrTLy2BE?+N!tESChdE;K;_{Y z32+a34C|2bNZd$s?Wbp&vXnph=?88uIzw^#IGA8K{5&)0#^~~ty}=yT&M{&5#l+L@ z8Z?ix!i=)?d`LW z$J8ma&ld4Z(AML`FTV}#&UxjXeS5(k+E2^z|N2Ronz=1Kj-qS#d!IIYQ9qvb-?fDt zo+5~s`SggUPOV;a4>Fb_+i5GQIHCpz>3;O1m7~E?G#IOuHB89Ih6 z?Cn*a@xN-dxH9i}?m3rhIDWcBFfAQ45u+y}faUDA?NZ}Y%XJhs@)j*67Pw!k1{M

nbtBLF3=BRNr`?t)TGiw68 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 From 98e425f32a6ab22d4b04f18d7a3f0b14b15b8479 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Sun, 1 Jun 2025 21:51:32 +0200 Subject: [PATCH 07/42] perf(rule-engine): More efficient filter indexing Move away from FNV hash-based filter indexing to two separate buckets - one for event types filters and one for category filters. --- pkg/event/category.go | 51 +++++++++++++++++ pkg/event/category_test.go | 34 ++++++++++++ pkg/filter/ql/literal.go | 23 ++++---- pkg/rules/engine.go | 110 +++++++++++++------------------------ pkg/rules/engine_test.go | 13 +---- 5 files changed, 139 insertions(+), 92 deletions(-) create mode 100644 pkg/event/category_test.go diff --git a/pkg/event/category.go b/pkg/event/category.go index d9d45e966..119dd55bd 100644 --- a/pkg/event/category.go +++ b/pkg/event/category.go @@ -19,6 +19,7 @@ package event import ( + "github.com/bits-and-blooms/bitset" "github.com/rabbitstack/fibratus/pkg/util/hashers" ) @@ -69,6 +70,56 @@ func (c Category) Hash() uint32 { return hashers.FnvUint32([]byte(c)) } +// CategoryMasks allows setting and checking the category bit mask. +type CategoryMasks struct { + bs bitset.BitSet +} + +// Set sets the category bit in the bit mask. +func (m *CategoryMasks) Set(c Category) { + m.bs.Set(uint(c.Index())) +} + +// Test checks if the given category bit is set. +func (m *CategoryMasks) Test(c Category) bool { + return m.bs.Test(uint(c.Index())) +} + +// MaxCategoryIndex designates the maximum category index. +const MaxCategoryIndex = 13 + +// Index returns a numerical category index. +func (c Category) Index() uint8 { + switch c { + case Registry: + return 1 + case File: + return 2 + case Net: + return 3 + case Process: + return 4 + case Thread: + return 5 + case Image: + return 6 + case Handle: + return 7 + case Driver: + return 8 + case Mem: + return 9 + case Object: + return 10 + case Threadpool: + return 11 + case Other: + return 12 + default: + return MaxCategoryIndex + } +} + // Categories returns all available categories. func Categories() []string { return []string{ diff --git a/pkg/event/category_test.go b/pkg/event/category_test.go new file mode 100644 index 000000000..6b6a6eedb --- /dev/null +++ b/pkg/event/category_test.go @@ -0,0 +1,34 @@ +/* + * Copyright 2021-present by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package event + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestCategoryMasks(t *testing.T) { + var masks CategoryMasks + masks.Set(File) + masks.Set(Process) + + assert.True(t, masks.Test(File)) + assert.True(t, masks.Test(Process)) + assert.False(t, masks.Test(Registry)) +} diff --git a/pkg/filter/ql/literal.go b/pkg/filter/ql/literal.go index 208d9268a..5e68e0346 100644 --- a/pkg/filter/ql/literal.go +++ b/pkg/filter/ql/literal.go @@ -21,7 +21,6 @@ package ql import ( "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/filter/fields" - "github.com/rabbitstack/fibratus/pkg/util/hashers" "golang.org/x/sys/windows" "net" "reflect" @@ -280,12 +279,13 @@ type SequenceExpr struct { // Alias represents the sequence expression alias. Alias string - buckets map[uint32]bool - types []event.Type + emasks event.EventsetMasks + cmasks event.CategoryMasks + + types []event.Type } func (e *SequenceExpr) init() { - e.buckets = make(map[uint32]bool) e.types = make([]event.Type, 0) e.BoundFields = make([]*BoundFieldLiteral, 0) } @@ -338,12 +338,15 @@ func (e *SequenceExpr) walk() { // initialize event type/category buckets for every such field for name, values := range stringFields { - if name == fields.EvtName || name == fields.EvtCategory { - for _, v := range values { - e.buckets[hashers.FnvUint32([]byte(v))] = true - if etype := event.NameToType(v); etype.Exists() { - e.types = append(e.types, etype) + for _, v := range values { + switch name { + case fields.EvtName: + for _, typ := range event.NameToTypes(v) { + e.emasks.Set(typ) + e.types = append(e.types, typ) } + case fields.EvtCategory: + e.cmasks.Set(event.Category(v)) } } } @@ -354,7 +357,7 @@ func (e *SequenceExpr) walk() { // to be evaluated when the incoming event type or category pertains to the one // defined in the field literal. func (e *SequenceExpr) IsEvaluable(evt *event.Event) bool { - return e.buckets[evt.Type.Hash()] || e.buckets[evt.Category.Hash()] + return e.emasks.Test(evt.Type.GUID(), evt.Type.HookID()) || e.cmasks.Test(evt.Category) } // HasBoundFields determines if this sequence expression references any bound field. diff --git a/pkg/rules/engine.go b/pkg/rules/engine.go index e9b6cc294..a3803d301 100644 --- a/pkg/rules/engine.go +++ b/pkg/rules/engine.go @@ -27,7 +27,6 @@ import ( "github.com/rabbitstack/fibratus/pkg/filter/fields" "github.com/rabbitstack/fibratus/pkg/ps" "github.com/rabbitstack/fibratus/pkg/rules/action" - "github.com/rabbitstack/fibratus/pkg/util/hashers" log "github.com/sirupsen/logrus" "sync" "time" @@ -53,7 +52,7 @@ var ( // the collection of compiled filters that are derived // from the loaded ruleset. type Engine struct { - filters compiledFilters + filters *filterset config *config.Config psnap ps.Snapshotter @@ -65,8 +64,6 @@ type Engine struct { compiler *compiler - hashCache *hashCache - matchFunc RuleMatchFunc } @@ -74,73 +71,35 @@ type ruleMatch struct { ctx *config.ActionContext } -// hashCache caches the event type/category FNV hashes -type hashCache struct { - mu sync.RWMutex - types map[event.Type]uint32 - cats map[event.Category]uint32 - lookupCategory bool -} - -func newHashCache() *hashCache { - return &hashCache{types: make(map[event.Type]uint32), cats: make(map[event.Category]uint32)} -} - -func (c *hashCache) typeHash(e *event.Event) uint32 { - c.mu.RLock() - defer c.mu.RUnlock() - return c.types[e.Type] -} - -func (c *hashCache) categoryHash(e *event.Event) uint32 { - c.mu.RLock() - defer c.mu.RUnlock() - return c.cats[e.Category] -} - -func (c *hashCache) addTypeHash(e *event.Event) uint32 { - c.mu.Lock() - defer c.mu.Unlock() - h := e.Type.Hash() - c.types[e.Type] = h - return h -} - -func (c *hashCache) addCategoryHash(e *event.Event) uint32 { - c.mu.Lock() - defer c.mu.Unlock() - h := e.Category.Hash() - c.cats[e.Category] = h - return h -} - type compiledFilter struct { filter filter.Filter config *config.FilterConfig ss *sequenceState } -type compiledFilters map[uint32][]*compiledFilter +// filterset contains compiled filters indexed by event type and category. +type filterset struct { + types map[event.Type][]*compiledFilter + categories map[uint8][]*compiledFilter +} -// collect collects all compiled filters for a -// particular event type or category. If no filters -// are found, the event is not asserted against the -// ruleset. -func (filters compiledFilters) collect(hashCache *hashCache, e *event.Event) []*compiledFilter { - h := hashCache.typeHash(e) - if h == 0 { - h = hashCache.addTypeHash(e) +func newFilterset() *filterset { + fs := &filterset{ + types: make(map[event.Type][]*compiledFilter), + categories: make(map[uint8][]*compiledFilter), } + return fs +} - if !hashCache.lookupCategory { - return filters[h] - } +func (f *filterset) empty() bool { + return len(f.types) == 0 && len(f.categories) == 0 +} - c := hashCache.categoryHash(e) - if c == 0 { - c = hashCache.addCategoryHash(e) +func (f *filterset) collect(e *event.Event) []*compiledFilter { + if len(f.categories) == 0 { + return f.types[e.Type] } - return append(filters[h], filters[c]...) + return append(f.types[e.Type], f.categories[e.Category.Index()]...) } func newCompiledFilter(f filter.Filter, c *config.FilterConfig, ss *sequenceState) *compiledFilter { @@ -172,14 +131,13 @@ func (f *compiledFilter) run(e *event.Event) bool { // NewEngine builds a fresh rules engine instance. func NewEngine(psnap ps.Snapshotter, config *config.Config) *Engine { e := &Engine{ - filters: make(map[uint32][]*compiledFilter), + filters: newFilterset(), matches: make([]*ruleMatch, 0), sequences: make([]*sequenceState, 0), psnap: psnap, config: config, scavenger: time.NewTicker(sequenceGcInterval), compiler: newCompiler(psnap, config), - hashCache: newHashCache(), } go e.gcSequences() @@ -217,6 +175,7 @@ func (e *Engine) Compile() (*config.RulesCompileResult, error) { // for more convenient tracking e.sequences = append(e.sequences, ss) } + if !fltr.isScoped() { log.Warnf("%q rule doesn't have "+ "event type or event category condition! "+ @@ -227,18 +186,21 @@ func (e *Engine) Compile() (*config.RulesCompileResult, error) { c.Name) continue } + // traverse all event name or category fields and determine // the event type from the filter field name expression. - // We end up with a map of rules indexed by event name - // or event category hash + // We end up with a map of rules indexed by event type + // or event category for name, values := range f.GetStringFields() { for _, v := range values { - if name == fields.EvtName || name == fields.EvtCategory { - if name == fields.EvtCategory { - e.hashCache.lookupCategory = true + switch name { + case fields.EvtName: + for _, typ := range event.NameToTypes(v) { + e.filters.types[typ] = append(e.filters.types[typ], fltr) } - hash := hashers.FnvUint32([]byte(v)) - e.filters[hash] = append(e.filters[hash], fltr) + case fields.EvtCategory: + category := event.Category(v) + e.filters.categories[category.Index()] = append(e.filters.categories[category.Index()], fltr) } } } @@ -258,10 +220,10 @@ func (*Engine) CanEnqueue() bool { return true } // Filters can be simple direct-event matchers or sequence states that // track an ordered series of events over a short period of time. func (e *Engine) ProcessEvent(evt *event.Event) (bool, error) { - if len(e.filters) == 0 { + if e.filters.empty() { return true, nil } - var matches bool + if evt.IsTerminateProcess() { // expire all sequences if the // process referenced in any @@ -270,7 +232,10 @@ func (e *Engine) ProcessEvent(evt *event.Event) (bool, error) { seq.expire(evt) } } - filters := e.filters.collect(e.hashCache, evt) + + filters := e.filters.collect(evt) + + var matches bool for _, f := range filters { match := f.run(evt) if !match { @@ -293,6 +258,7 @@ func (e *Engine) ProcessEvent(evt *event.Event) (bool, error) { return true, nil } } + return matches, nil } diff --git a/pkg/rules/engine_test.go b/pkg/rules/engine_test.go index 6484fb19a..80c269d9b 100644 --- a/pkg/rules/engine_test.go +++ b/pkg/rules/engine_test.go @@ -141,7 +141,8 @@ func TestCompileIndexableFilters(t *testing.T) { compileRules(t, e) - assert.Len(t, e.filters, 3) + assert.Len(t, e.filters.types, 5) + assert.Len(t, e.filters.categories, 1) var tests = []struct { evt *event.Event @@ -156,17 +157,9 @@ func TestCompileIndexableFilters(t *testing.T) { for _, tt := range tests { t.Run(tt.evt.Type.String(), func(t *testing.T) { - assert.Len(t, e.filters.collect(e.hashCache, tt.evt), tt.wants) + assert.Len(t, e.filters.collect(tt.evt), tt.wants) }) } - - assert.Len(t, e.hashCache.types, 4) - - evt := &event.Event{Type: event.RecvTCPv4} - - h1, h2 := e.hashCache.typeHash(evt), e.hashCache.categoryHash(evt) - assert.Equal(t, uint32(0xfa4dab59), h1) - assert.Equal(t, uint32(0x811c9dc5), h2) } func TestRunSimpleRules(t *testing.T) { From 907e3c1e3b9a5ab1f3e1a1d4af138e7df107616e Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Sat, 7 Jun 2025 22:32:03 +0200 Subject: [PATCH 08/42] refactor(eventsource): Unified security telemetry logger session Unified security telemetry session serves as a container for the events published by all ETW providers except the core NT Kernel Logger provider. By enabling all providers inside the same session, we can preserve event ordering and save extra resources allocated for the ETW session buffers. --- internal/etw/source.go | 31 +++++-- internal/etw/source_test.go | 2 +- internal/etw/stackext.go | 9 +- internal/etw/trace.go | 155 +++++++++++++++++++++++++------- internal/etw/trace_test.go | 3 +- pkg/event/types_windows.go | 30 ++----- pkg/event/types_windows_test.go | 6 -- pkg/filter/ql/literal.go | 22 ++--- pkg/filter/ql/parser_test.go | 6 ++ pkg/sys/etw/types.go | 8 +- 10 files changed, 179 insertions(+), 93 deletions(-) diff --git a/internal/etw/source.go b/internal/etw/source.go index 5bcc62456..7aa84f3cf 100644 --- a/internal/etw/source.go +++ b/internal/etw/source.go @@ -32,7 +32,6 @@ import ( "github.com/rabbitstack/fibratus/pkg/sys/etw" "github.com/rabbitstack/fibratus/pkg/util/multierror" log "github.com/sirupsen/logrus" - "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" "time" ) @@ -162,16 +161,34 @@ func (e *EventSource) Open(config *config.Config) error { } } - e.addTrace(etw.KernelLoggerSession, etw.KernelTraceControlGUID) + // add the core NT Kernel Logger trace + e.addTrace(NewKernelTrace(config)) + + // security telemetry trace hosts remaining ETW providers + trace := NewTrace(etw.SecurityTelemetrySession, config) if config.EventSource.EnableDNSEvents { - e.addTrace(etw.DNSClientSession, etw.DNSClientGUID) + trace.AddProvider(etw.DNSClientGUID, false) } + if config.EventSource.EnableAuditAPIEvents { - e.addTrace(etw.KernelAuditAPICallsSession, etw.KernelAuditAPICallsGUID) + trace.AddProvider(etw.KernelAuditAPICallsGUID, config.EventSource.StackEnrichment) } + if config.EventSource.EnableThreadpoolEvents { - e.addTrace(etw.ThreadpoolSession, etw.ThreadpoolGUID) + // thread pool provider must be configured with + // stack extensions to activate stack walks events + var stackexts *StackExtensions + if e.config.EventSource.StackEnrichment { + stackexts = NewStackExtensions(config.EventSource) + stackexts.EnableThreadpoolCallstack() + } + trace.AddProvider(etw.ThreadpoolGUID, config.EventSource.StackEnrichment, WithStackExts(stackexts)) + } + + if trace.HasProviders() { + // add security telemetry trace + e.addTrace(trace) } for _, trace := range e.traces { @@ -305,6 +322,6 @@ func (e *EventSource) RegisterEventListener(lis event.Listener) { e.listeners = append(e.listeners, lis) } -func (e *EventSource) addTrace(name string, guid windows.GUID) { - e.traces = append(e.traces, NewTrace(name, guid, 0x0, e.config)) +func (e *EventSource) addTrace(trace *Trace) { + e.traces = append(e.traces, trace) } diff --git a/internal/etw/source_test.go b/internal/etw/source_test.go index ee4222e37..3065f7a39 100644 --- a/internal/etw/source_test.go +++ b/internal/etw/source_test.go @@ -101,7 +101,7 @@ func TestEventSourceStartTraces(t *testing.T) { 1, []etw.EventTraceFlags{0x6018203, 0}, }, - {"start kernel logger and audit api sessions", + {"start kernel and security telemetry logger sessions", &config.Config{ EventSource: config.EventSourceConfig{ EnableThreadEvents: true, diff --git a/internal/etw/stackext.go b/internal/etw/stackext.go index b2b2f9379..140cef601 100644 --- a/internal/etw/stackext.go +++ b/internal/etw/stackext.go @@ -38,9 +38,9 @@ func NewStackExtensions(config config.EventSourceConfig) *StackExtensions { } // AddStackTracing enables stack tracing for the specified event type. -func (s *StackExtensions) AddStackTracing(Type event.Type) { - if !s.config.TestDropMask(Type) { - s.ids = append(s.ids, etw.NewClassicEventID(Type.GUID(), Type.HookID())) +func (s *StackExtensions) AddStackTracing(typ event.Type) { + if !s.config.TestDropMask(typ) { + s.ids = append(s.ids, etw.NewClassicEventID(typ.GUID(), typ.HookID())) } } @@ -54,6 +54,9 @@ func (s *StackExtensions) AddStackTracingWith(guid windows.GUID, hookID uint16) // EventIds returns all event types eligible for stack tracing. func (s *StackExtensions) EventIds() []etw.ClassicEventID { return s.ids } +// Empty determines if this stack extensions has registered event identifiers. +func (s *StackExtensions) Empty() bool { return len(s.ids) == 0 } + // EnableProcessCallstack populates the stack identifiers // with event types eligible for emitting stack walk events // related to process telemetry, such as creating a process, diff --git a/internal/etw/trace.go b/internal/etw/trace.go index 6a1d78ac3..c64a6c420 100644 --- a/internal/etw/trace.go +++ b/internal/etw/trace.go @@ -72,16 +72,10 @@ func initEventTraceProps(c config.EventSourceConfig) etw.EventTraceProperties { } } -// Trace is the essential building block for controlling -// trace sessions and configuring event consumers. Such -// operations include starting, stopping, and flushing -// trace sessions, and opening the trace for processing -// and event consumption. -type Trace struct { - // Name represents the unique tracing session name. - Name string +// ProviderInfo describes ETW provider metadata. +type ProviderInfo struct { // GUID is the globally unique identifier for the - // ETW provider. + // ETW provider for which the session is started. GUID windows.GUID // Keywords is the bitmask of keywords that determine // the categories of events for the provider to emit. @@ -91,11 +85,42 @@ type Trace struct { // for providers that are enabled via etw.EnableProvider // API. Keywords uint64 - + // EnableStacks indicates if callstacks are enabled for + // this provider. + EnableStacks bool // stackExtensions manager stack tracing enablement. // For each event present in the stack identifiers, // the StackWalk event is published by the provider. stackExtensions *StackExtensions +} + +func (p *ProviderInfo) HasStackExtensions() bool { + return p.stackExtensions != nil && !p.stackExtensions.Empty() +} + +// Trace is the essential building block for controlling +// trace sessions and configuring event consumers. Such +// operations include starting, stopping, and flushing +// trace sessions, and opening the trace for processing +// and event consumption. Trace can be configured to +// operate a single ETW provider, or it can act as a +// container for multiple provider sessions. +type Trace struct { + // Name represents the unique tracing session name. + Name string + // GUID is the globally unique identifier for the + // ETW provider for which the session is started. + GUID windows.GUID + + // Providers is the list of providers to be run inside + // the tracing session. For each provider, the GUID, + // keywords and other parameters can be specified. + Providers []ProviderInfo + + // stackExtensions manages stack tracing enablement. + // For each event present in the stack identifiers, + // the StackWalk event is published by the provider. + stackExtensions *StackExtensions // startHandle is the session handle returned by the // etw.StartTrace function. This handle is @@ -120,13 +145,68 @@ type Trace struct { errs chan error } -// NewTrace creates a new trace with specified name, provider GUID, and keywords. -func NewTrace(name string, guid windows.GUID, keywords uint64, config *config.Config) *Trace { - t := &Trace{Name: name, GUID: guid, Keywords: keywords, stackExtensions: NewStackExtensions(config.EventSource), config: config} +type opts struct { + stackexts *StackExtensions + keywords uint64 +} + +// Option represents the option for the trace. +type Option func(o *opts) + +// WithStackExts sets the stack extensions. +func WithStackExts(stackexts *StackExtensions) Option { + return func(o *opts) { + o.stackexts = stackexts + } +} + +// WithKeywords sets the bitmask of keywords that determine +// the categories of events for the provider to emit. +func WithKeywords(keywords uint64) Option { + return func(o *opts) { + o.keywords = keywords + } +} + +// NewKernelTrace creates a new NT Kernel Logger trace. +func NewKernelTrace(config *config.Config) *Trace { + t := &Trace{Name: etw.KernelLoggerSession, GUID: etw.KernelTraceControlGUID, stackExtensions: NewStackExtensions(config.EventSource), config: config} t.enableCallstacks() return t } +// NewTrace creates a new trace that can host various ETW provider sessions. +// The providers to be run inside the session can be given in the last argument +// or added by the AddProvider method. +func NewTrace(name string, config *config.Config, providers ...ProviderInfo) *Trace { + t := &Trace{Name: name, config: config, Providers: make([]ProviderInfo, 0)} + t.Providers = providers + return t +} + +// AddProvider adds a new provider to the multi trace session +// with optional parameters that influence the provider. +func (t *Trace) AddProvider(guid windows.GUID, enableStacks bool, options ...Option) { + var opts opts + + for _, opt := range options { + opt(&opts) + } + + t.Providers = append(t.Providers, ProviderInfo{GUID: guid, Keywords: opts.keywords, EnableStacks: enableStacks, stackExtensions: opts.stackexts}) +} + +// HasProviders determines if this trace contains providers. +func (t *Trace) HasProviders() bool { return len(t.Providers) > 0 } + +// IsGUIDEmpty determines if the provider GUID is empty. +func (t *Trace) IsGUIDEmpty() bool { + return t.GUID.Data1 == 0 && + t.GUID.Data2 == 0 && + t.GUID.Data3 == 0 && + t.GUID.Data4 == [8]byte{} +} + func (t *Trace) enableCallstacks() { if t.IsKernelTrace() { t.stackExtensions.EnableProcessCallstack() @@ -137,10 +217,6 @@ func (t *Trace) enableCallstacks() { t.stackExtensions.EnableMemoryCallstack() } - - if t.IsThreadpoolTrace() { - t.stackExtensions.EnableThreadpoolCallstack() - } } // Start registers and starts an event tracing session. @@ -151,6 +227,11 @@ func (t *Trace) Start() error { if len(t.Name) > maxLoggerNameSize { return fmt.Errorf("trace name [%s] is too long", t.Name) } + + if !t.IsGUIDEmpty() && t.HasProviders() { + return fmt.Errorf("%s trace has the root GUID set but providers are not empty", t.Name) + } + cfg := t.config.EventSource props := initEventTraceProps(cfg) flags := t.enableFlagsDynamically(cfg) @@ -212,21 +293,34 @@ func (t *Trace) Start() error { return etw.SetTraceSystemFlags(handle, sysTraceFlags) } - // if we're starting a trace for non-system logger, the call - // to etw.EnableTrace is needed to configure how an ETW provider - // publishes events to the trace session. For instance, if stack - // enrichment is enabled, it is necessary to instruct the provider - // to emit stack addresses in the extended data item section when - // writing events to the session buffers - if cfg.StackEnrichment && !t.IsThreadpoolTrace() { - return etw.EnableTraceWithOpts(t.GUID, t.startHandle, t.Keywords, etw.EnableTraceOpts{WithStacktrace: true}) - } else if cfg.StackEnrichment && len(t.stackExtensions.EventIds()) > 0 { - if err := etw.EnableStackTracing(t.startHandle, t.stackExtensions.EventIds()); err != nil { - return fmt.Errorf("fail to enable system events callstack tracing: %v", err) + // For each provider in multi trace, the call to etw.EnableTrace is + // needed to configure how an ETW provider publishes events to the + // trace session. + // For instance, if stack enrichment is enabled, it is necessary to + // instruct the provider to emit stack addresses in the extended + // data item section when writing events to the session buffers + for _, provider := range t.Providers { + switch { + case provider.EnableStacks && provider.HasStackExtensions(): + if err := etw.EnableStackTracing(t.startHandle, provider.stackExtensions.EventIds()); err != nil { + return fmt.Errorf("fail to enable provider callstack tracing: %v", err) + } + if err := etw.EnableTrace(provider.GUID, t.startHandle, provider.Keywords); err != nil { + return err + } + case provider.EnableStacks: + opts := etw.EnableTraceOpts{WithStacktrace: true} + if err := etw.EnableTraceWithOpts(provider.GUID, t.startHandle, provider.Keywords, opts); err != nil { + return err + } + default: + if err := etw.EnableTrace(provider.GUID, t.startHandle, provider.Keywords); err != nil { + return err + } } } - return etw.EnableTrace(t.GUID, t.startHandle, t.Keywords) + return nil } // IsStarted indicates if the trace is started successfully. @@ -317,9 +411,6 @@ func (t *Trace) Close() error { // IsKernelTrace determines if this is the system logger trace. func (t *Trace) IsKernelTrace() bool { return t.GUID == etw.KernelTraceControlGUID } -// IsThreadpoolTrace determines if this is the thread pool logger trace. -func (t *Trace) IsThreadpoolTrace() bool { return t.GUID == etw.ThreadpoolGUID } - // enableFlagsDynamically crafts the system logger event mask // depending on the compiled rules result or the config state. // System logger flags is a bitmask that indicates which kernel events diff --git a/internal/etw/trace_test.go b/internal/etw/trace_test.go index 23075a2ad..4b0fc4560 100644 --- a/internal/etw/trace_test.go +++ b/internal/etw/trace_test.go @@ -20,7 +20,6 @@ package etw import ( "github.com/rabbitstack/fibratus/pkg/config" - "github.com/rabbitstack/fibratus/pkg/sys/etw" "github.com/stretchr/testify/require" "testing" "time" @@ -37,7 +36,7 @@ func TestStartTrace(t *testing.T) { }, } - trace := NewTrace(etw.KernelLoggerSession, etw.KernelTraceControlGUID, 0, cfg) + trace := NewKernelTrace(cfg) require.NoError(t, trace.Start()) require.True(t, trace.IsStarted()) defer trace.Stop() diff --git a/pkg/event/types_windows.go b/pkg/event/types_windows.go index 8a2c67bf3..f9658aa2d 100644 --- a/pkg/event/types_windows.go +++ b/pkg/event/types_windows.go @@ -35,12 +35,10 @@ type Source uint8 const ( // SystemLogger event is emitted by the system provider SystemLogger Source = iota - // AuditAPICallsLogger event is emitted by Audit API calls provider - AuditAPICallsLogger - // DNSLogger event is emitted by DNS provider - DNSLogger - // ThreadpoolLogger event is emitted by thread pool provider - ThreadpoolLogger + // SecurityTelemetryLogger event is emitted by the combination of multiple providers. + // Most notably, DNS, thread pool, and kernel audit API providers are in charge of + // publishing the events. + SecurityTelemetryLogger ) // Type identifies an event type. It comprises the event GUID + hook ID to uniquely identify the event @@ -578,27 +576,15 @@ func (t *Type) HookID() uint16 { // Source designates the provenance of this event type. func (t Type) Source() Source { switch t { - case OpenProcess, OpenThread, SetThreadContext, CreateSymbolicLinkObject: - return AuditAPICallsLogger - case QueryDNS, ReplyDNS: - return DNSLogger - case SubmitThreadpoolWork, SubmitThreadpoolCallback, SetThreadpoolTimer: - return ThreadpoolLogger + case OpenProcess, OpenThread, SetThreadContext, CreateSymbolicLinkObject, + QueryDNS, ReplyDNS, SubmitThreadpoolWork, SubmitThreadpoolCallback, + SetThreadpoolTimer: + return SecurityTelemetryLogger default: return SystemLogger } } -// CanArriveOutOfOrder indicates if the event can be -// emitted by the provider in out-of-order fashion, i.e. -// its timestamp is perfectly aligned in relation to other -// events, but it appears first on the consumer callback -// before other events published before it. -func (t Type) CanArriveOutOfOrder() bool { - return t.Category() == Threadpool || t.Subcategory() == DNS || - t == OpenProcess || t == OpenThread || t == SetThreadContext || t == CreateSymbolicLinkObject -} - // TypeFromParts builds the event type from provider GUID and hook ID. func TypeFromParts(g windows.GUID, id uint16) Type { return pack(g, id) } diff --git a/pkg/event/types_windows_test.go b/pkg/event/types_windows_test.go index a93c15b5a..588b580ae 100644 --- a/pkg/event/types_windows_test.go +++ b/pkg/event/types_windows_test.go @@ -128,9 +128,3 @@ func TestGUIDAndHookIDFromEventType(t *testing.T) { }) } } - -func TestCanArriveOutOfOrder(t *testing.T) { - assert.False(t, RegSetValue.CanArriveOutOfOrder()) - assert.False(t, VirtualAlloc.CanArriveOutOfOrder()) - assert.True(t, OpenProcess.CanArriveOutOfOrder()) -} diff --git a/pkg/filter/ql/literal.go b/pkg/filter/ql/literal.go index 5e68e0346..8da866f36 100644 --- a/pkg/filter/ql/literal.go +++ b/pkg/filter/ql/literal.go @@ -21,7 +21,6 @@ package ql import ( "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/filter/fields" - "golang.org/x/sys/windows" "net" "reflect" "strconv" @@ -379,24 +378,19 @@ func (s Sequence) IsConstrained() bool { } func (s *Sequence) init() { - // determine if the sequence references - // an event type that can arrive out-of-order. - // The edge case is for unordered events emitted - // by the same provider where the temporal order - // is guaranteed - guids := make(map[windows.GUID]bool) + // determine if the sequence references an event type + // that can arrive out-of-order. This happens if the + // expressions in the sequence reference event types + // from different event sources + sources := make(map[event.Source]bool) + for _, expr := range s.Expressions { for _, etype := range expr.types { - if etype.CanArriveOutOfOrder() { - s.IsUnordered = true - } - guids[etype.GUID()] = true + sources[etype.Source()] = true } } - if s.IsUnordered && len(guids) == 1 { - s.IsUnordered = false - } + s.IsUnordered = len(sources) > 1 } func (s Sequence) impairBy() bool { diff --git a/pkg/filter/ql/parser_test.go b/pkg/filter/ql/parser_test.go index 7199df993..5e5055c9a 100644 --- a/pkg/filter/ql/parser_test.go +++ b/pkg/filter/ql/parser_test.go @@ -436,6 +436,12 @@ func TestIsSequenceUnordered(t *testing.T) { `, false, }, + { + `|evt.name = 'OpenProcess'| by ps.uuid + |evt.name = 'QueryDns'| by ps.uuid + `, + false, + }, } for i, tt := range tests { diff --git a/pkg/sys/etw/types.go b/pkg/sys/etw/types.go index c1a5b92c3..27c1a37e0 100644 --- a/pkg/sys/etw/types.go +++ b/pkg/sys/etw/types.go @@ -59,12 +59,8 @@ const ( const ( // KernelLoggerSession represents the default session name for NT kernel logger KernelLoggerSession = "NT Kernel Logger" - // KernelAuditAPICallsSession represents the session name for the kernel audit API logger - KernelAuditAPICallsSession = "Kernel Audit API Calls Logger" - // DNSClientSession represents the session name for the DNS client logger - DNSClientSession = "DNS Client Logger" - // ThreadpoolSession represents the session name for the thread pool logger - ThreadpoolSession = "Threadpool Logger" + // SecurityTelemetrySession represents the session name for all security telemetry + SecurityTelemetrySession = "Security Telemetry Logger" // WnodeTraceFlagGUID indicates that the structure contains event tracing information WnodeTraceFlagGUID = 0x00020000 From 71aa832e20f015d82cdb3b77c9a2ff29038056a4 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Sat, 7 Jun 2025 22:49:20 +0200 Subject: [PATCH 09/42] perf(rule-engine,eventsource): Event bitmask and bitsets The new bitmask implementation is introduced to deal with event id bits testing. --- pkg/event/bitset.go | 94 +++++++++++++ pkg/event/bitset_test.go | 128 ++++++++++++++++++ pkg/event/eventset.go | 94 ------------- pkg/event/eventset_test.go | 80 ----------- pkg/event/types_windows.go | 22 ++- pkg/event/types_windows_test.go | 24 +++- .../bitmask/bitmask.go} | 47 +++++-- 7 files changed, 295 insertions(+), 194 deletions(-) create mode 100644 pkg/event/bitset.go create mode 100644 pkg/event/bitset_test.go delete mode 100644 pkg/event/eventset.go delete mode 100644 pkg/event/eventset_test.go rename pkg/{event/category_test.go => util/bitmask/bitmask.go} (50%) diff --git a/pkg/event/bitset.go b/pkg/event/bitset.go new file mode 100644 index 000000000..2a87e3a17 --- /dev/null +++ b/pkg/event/bitset.go @@ -0,0 +1,94 @@ +/* + * Copyright 2021-2022 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package event + +import ( + "github.com/bits-and-blooms/bitset" + "github.com/rabbitstack/fibratus/pkg/util/bitmask" +) + +// BitSetType defines the bitset type +type BitSetType uint8 + +const ( + // BitmaskBitSet designates the mask-based event id bitset + BitmaskBitSet BitSetType = iota + 1 + // TypeBitSet designates the uint16 number space event type bitset + TypeBitSet + // CategoryBitSet designates the event category bitset + CategoryBitSet +) + +// BitSets handles the group of category/event type bitsets +// and the bitmask for evaluating event ids bits. +type BitSets struct { + bitmask *bitmask.Bitmask + cats *bitset.BitSet + types *bitset.BitSet +} + +// SetBit sets the bit dictated by the bitset type. +func (b *BitSets) SetBit(bs BitSetType, typ Type) { + switch bs { + case BitmaskBitSet: + if b.bitmask == nil { + b.bitmask = bitmask.New() + } + b.bitmask.Set(typ.ID()) + + case TypeBitSet: + if b.types == nil { + b.types = bitset.New(uint(MaxTypeID() + 1)) + } + b.types.Set(uint(typ.HookID())) + + case CategoryBitSet: + if b.cats == nil { + b.cats = bitset.New(MaxCategoryIndex + 1) + } + b.cats.Set(uint(typ.Category().Index())) + } +} + +// SetCategoryBit toggles the category bit in the bitset. +func (b *BitSets) SetCategoryBit(c Category) { + if b.cats == nil { + b.cats = bitset.New(MaxCategoryIndex + 1) + } + b.cats.Set(uint(c.Index())) +} + +// IsBitSet checks if any of the populated bitsets +// contain the type, event ID, or category bit. +// This method evaluates first the event type bitset. +// The event type bitset should only be initialized +// if all event types pertain to the same category. +// Otherwise, event id bitset and last category bitset +// are tested for respective bits. +func (b *BitSets) IsBitSet(evt *Event) bool { + if b.types != nil && b.types.Test(uint(evt.Type.HookID())) { + return true + } + return (b.bitmask != nil && b.bitmask.IsSet(evt.Type.ID())) || + (b.cats != nil && b.cats.Test(uint(evt.Category.Index()))) +} + +func (b *BitSets) IsBitmaskInitialized() bool { return b.bitmask != nil } +func (b *BitSets) IsTypesInitialized() bool { return b.types != nil } +func (b *BitSets) IsCategoryInitialized() bool { return b.cats != nil } diff --git a/pkg/event/bitset_test.go b/pkg/event/bitset_test.go new file mode 100644 index 000000000..8aed8ef43 --- /dev/null +++ b/pkg/event/bitset_test.go @@ -0,0 +1,128 @@ +/* + * Copyright 2021-2022 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package event + +import ( + "github.com/rabbitstack/fibratus/pkg/util/bitmask" + "testing" + + "github.com/rabbitstack/fibratus/pkg/sys/etw" + "github.com/stretchr/testify/assert" +) + +func TestBitmask(t *testing.T) { + var tests = []struct { + typ Type + expected bool + }{ + {TerminateThread, true}, + {TerminateProcess, true}, + {CreateThread, true}, + {CreateFile, false}, + {WriteFile, false}, + {LoadImage, false}, + {MapFileRundown, true}, + {ProcessRundown, true}, + } + + b := bitmask.New() + for _, typ := range AllWithState() { + if typ == WriteFile || typ == LoadImage || typ == CreateFile { + continue + } + b.Set(typ.ID()) + } + + for _, tt := range tests { + t.Run(tt.typ.String(), func(t *testing.T) { + assert.Equal(t, tt.expected, b.IsSet(tt.typ.ID())) + }) + } +} + +func TestBitSets(t *testing.T) { + var tests = []struct { + evt *Event + expected bool + }{ + {&Event{Type: TerminateThread}, true}, + {&Event{Type: TerminateProcess}, true}, + {&Event{Type: CreateThread, Category: Thread}, true}, + {&Event{Type: CreateFile}, false}, + {&Event{Type: WriteFile}, false}, + {&Event{Type: LoadImage}, false}, + {&Event{Type: MapFileRundown}, true}, + {&Event{Type: ProcessRundown}, true}, + } + + var bitsets BitSets + + bitsets.SetBit(BitmaskBitSet, TerminateThread) + bitsets.SetBit(TypeBitSet, TerminateProcess) + bitsets.SetBit(CategoryBitSet, CreateThread) + bitsets.SetBit(TypeBitSet, MapFileRundown) + bitsets.SetBit(BitmaskBitSet, ProcessRundown) + + for _, tt := range tests { + t.Run(tt.evt.Type.String(), func(t *testing.T) { + assert.Equal(t, tt.expected, bitsets.IsBitSet(tt.evt)) + }) + } +} + +func BenchmarkBitmask(b *testing.B) { + b.ReportAllocs() + + bm := bitmask.New() + bm.Set(TerminateThread.ID()) + bm.Set(CreateThread.ID()) + bm.Set(TerminateProcess.ID()) + bm.Set(CreateFile.ID()) + + evt := &etw.EventRecord{Header: etw.EventHeader{ProviderID: ThreadEventGUID, EventDescriptor: etw.EventDescriptor{Opcode: 2}}} + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + if !bm.IsSet(evt.ID()) { + panic("mask should be present") + } + } +} + +func BenchmarkStdlibMap(b *testing.B) { + b.ReportAllocs() + + evts := make(map[Type]bool) + evts[TerminateThread] = true + evts[CreateThread] = true + evts[TerminateProcess] = true + evts[CreateFile] = true + + evt := etw.EventRecord{Header: etw.EventHeader{ProviderID: ThreadEventGUID, EventDescriptor: etw.EventDescriptor{Opcode: 2}}} + etype := NewTypeFromEventRecord(&evt) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + if !evts[etype] { + panic("event should be present") + } + } +} diff --git a/pkg/event/eventset.go b/pkg/event/eventset.go deleted file mode 100644 index 614029027..000000000 --- a/pkg/event/eventset.go +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2021-2022 by Nedim Sabic Sabic - * https://www.fibratus.io - * All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package event - -import ( - "fmt" - "github.com/bits-and-blooms/bitset" - "golang.org/x/sys/windows" -) - -// EventsetMasks allows efficient testing -// of a group of bitsets containing event -// hook identifiers. For each provider -// represented by a GUID, a dedicated -// bitset is defined. -type EventsetMasks struct { - masks [ProvidersCount]bitset.BitSet -} - -// Set puts a new event type into the bitset. -func (e *EventsetMasks) Set(typ Type) { - g := typ.GUID() - i := e.bitsetIndex(g) - if i < 0 { - panic(fmt.Sprintf("invalid event bitset index: %s", g.String())) - } - e.masks[e.bitsetIndex(typ.GUID())].Set(uint(typ.HookID())) -} - -// Test checks if the given provider GUID and -// hook identifier are present in the bitset. -func (e *EventsetMasks) Test(guid windows.GUID, hookID uint16) bool { - i := e.bitsetIndex(guid) - if i < 0 { - return false - } - return e.masks[e.bitsetIndex(guid)].Test(uint(hookID)) -} - -// Clear clears the bitset for a given provider GUID. -func (e *EventsetMasks) Clear(guid windows.GUID) { - i := e.bitsetIndex(guid) - if i < 0 { - panic(fmt.Sprintf("invalid event bitset index: %s", guid.String())) - } - e.masks[e.bitsetIndex(guid)].ClearAll() -} - -func (e *EventsetMasks) bitsetIndex(guid windows.GUID) int { - switch guid { - case ProcessEventGUID: - return 0 - case ThreadEventGUID: - return 1 - case ImageEventGUID: - return 2 - case FileEventGUID: - return 3 - case RegistryEventGUID: - return 4 - case NetworkTCPEventGUID: - return 5 - case NetworkUDPEventGUID: - return 6 - case HandleEventGUID: - return 7 - case MemEventGUID: - return 8 - case AuditAPIEventGUID: - return 9 - case DNSEventGUID: - return 10 - case ThreadpoolGUID: - return 11 - default: - return -1 - } -} diff --git a/pkg/event/eventset_test.go b/pkg/event/eventset_test.go deleted file mode 100644 index 89d483523..000000000 --- a/pkg/event/eventset_test.go +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2021-2022 by Nedim Sabic Sabic - * https://www.fibratus.io - * All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package event - -import ( - "github.com/rabbitstack/fibratus/pkg/sys/etw" - "github.com/stretchr/testify/require" - "testing" -) - -func TestEventsetMasks(t *testing.T) { - var masks EventsetMasks - masks.Set(TerminateThread) - masks.Set(CreateThread) - masks.Set(TerminateProcess) - - require.True(t, masks.Test(ThreadEventGUID, TerminateThread.HookID())) - require.True(t, masks.Test(ThreadEventGUID, CreateThread.HookID())) - require.False(t, masks.Test(ThreadEventGUID, ThreadRundown.HookID())) - require.True(t, masks.Test(ProcessEventGUID, TerminateProcess.HookID())) - require.False(t, masks.Test(ProcessEventGUID, CreateProcess.HookID())) - - masks.Clear(ThreadEventGUID) - - require.False(t, masks.Test(ThreadEventGUID, TerminateThread.HookID())) - require.False(t, masks.Test(ThreadEventGUID, CreateThread.HookID())) -} - -func BenchmarkEventsetMasks(b *testing.B) { - b.ReportAllocs() - - var masks EventsetMasks - masks.Set(TerminateThread) - masks.Set(CreateThread) - masks.Set(TerminateProcess) - masks.Set(CreateFile) - - evt := etw.EventRecord{Header: etw.EventHeader{ProviderID: ThreadEventGUID, EventDescriptor: etw.EventDescriptor{Opcode: 2}}} - - for i := 0; i < b.N; i++ { - if !masks.Test(evt.Header.ProviderID, uint16(evt.Header.EventDescriptor.Opcode)) { - panic("mask should be present") - } - } -} - -func BenchmarkStdlibMap(b *testing.B) { - b.ReportAllocs() - - evts := make(map[Type]bool) - evts[TerminateThread] = true - evts[CreateThread] = true - evts[TerminateProcess] = true - evts[CreateFile] = true - - evt := etw.EventRecord{Header: etw.EventHeader{ProviderID: ThreadEventGUID, EventDescriptor: etw.EventDescriptor{Opcode: 2}}} - kt := NewFromEventRecord(&evt) - - for i := 0; i < b.N; i++ { - if !evts[kt] { - panic("event should be present") - } - } -} diff --git a/pkg/event/types_windows.go b/pkg/event/types_windows.go index f9658aa2d..125f19d3f 100644 --- a/pkg/event/types_windows.go +++ b/pkg/event/types_windows.go @@ -25,10 +25,6 @@ import ( "golang.org/x/sys/windows" ) -// ProvidersCount designates the number of interesting providers. -// Remember to increment if a new event source is introduced. -const ProvidersCount = 12 - // Source is the type that designates the provenance of the event type Source uint8 @@ -223,8 +219,8 @@ var ( UnknownType = pack(windows.GUID{}, 0) ) -// NewFromEventRecord creates a new event type from ETW event record. -func NewFromEventRecord(ev *etw.EventRecord) Type { +// NewTypeFromEventRecord creates a new event type from ETW event record. +func NewTypeFromEventRecord(ev *etw.EventRecord) Type { return pack(ev.Header.ProviderID, ev.HookID()) } @@ -573,6 +569,20 @@ func (t *Type) HookID() uint16 { return binary.BigEndian.Uint16(t[16:]) } +// ID is an unsigned integer that uniquely +// identifies the event. Handy for bitmask +// operations. +func (t Type) ID() uint { + id := uint(t[0])<<56 | + uint(t[1])<<48 | + uint(t[2])<<40 | + uint(t[3])<<32 | + uint(t[4])<<24 | + uint(t[5])<<16 | + uint(t.HookID()) + return id +} + // Source designates the provenance of this event type. func (t Type) Source() Source { switch t { diff --git a/pkg/event/types_windows_test.go b/pkg/event/types_windows_test.go index 588b580ae..371feca01 100644 --- a/pkg/event/types_windows_test.go +++ b/pkg/event/types_windows_test.go @@ -80,7 +80,7 @@ func TestEventTypeComparison(t *testing.T) { } func TestNewEventTypeFromEventRecord(t *testing.T) { - assert.Equal(t, CreateProcess, NewFromEventRecord(&etw.EventRecord{ + assert.Equal(t, CreateProcess, NewTypeFromEventRecord(&etw.EventRecord{ Header: etw.EventHeader{ ProviderID: windows.GUID{Data1: 0x3d6fa8d0, Data2: 0xfe05, Data3: 0x11d0, Data4: [8]byte{0x9d, 0xda, 0x0, 0xc0, 0x4f, 0xd7, 0xba, 0x7c}}, EventDescriptor: etw.EventDescriptor{ @@ -88,7 +88,7 @@ func TestNewEventTypeFromEventRecord(t *testing.T) { }, }, })) - assert.Equal(t, OpenProcess, NewFromEventRecord(&etw.EventRecord{ + assert.Equal(t, OpenProcess, NewTypeFromEventRecord(&etw.EventRecord{ Header: etw.EventHeader{ ProviderID: windows.GUID{Data1: 0xe02a841c, Data2: 0x75a3, Data3: 0x4fa7, Data4: [8]byte{0xaf, 0xc8, 0xae, 0x09, 0xcf, 0x9b, 0x7f, 0x23}}, EventDescriptor: etw.EventDescriptor{ @@ -103,6 +103,10 @@ func TestEventTypeExists(t *testing.T) { require.True(t, AcceptTCPv6.Exists()) } +func TestTypeID(t *testing.T) { + assert.Equal(t, uint(14439051552138264620), SetThreadpoolTimer.ID()) +} + func TestGUIDAndHookIDFromEventType(t *testing.T) { var tests = []struct { Type Type @@ -128,3 +132,19 @@ func TestGUIDAndHookIDFromEventType(t *testing.T) { }) } } + +func TestIDEquality(t *testing.T) { + evt := etw.EventRecord{Header: etw.EventHeader{ProviderID: ThreadEventGUID, EventDescriptor: etw.EventDescriptor{Opcode: 1}}} + typ := CreateThread + require.Equal(t, typ.ID(), evt.ID()) +} + +func TestEventTypeIDCollision(t *testing.T) { + ids := make(map[uint]Type) + for _, typ := range AllWithState() { + if etype, ok := ids[typ.ID()]; ok { + t.Fatalf("id collision for %s event type. Mapped event type: %s", typ.String(), etype.String()) + } + ids[typ.ID()] = typ + } +} diff --git a/pkg/event/category_test.go b/pkg/util/bitmask/bitmask.go similarity index 50% rename from pkg/event/category_test.go rename to pkg/util/bitmask/bitmask.go index 6b6a6eedb..20cc2a1f1 100644 --- a/pkg/event/category_test.go +++ b/pkg/util/bitmask/bitmask.go @@ -16,19 +16,42 @@ * limitations under the License. */ -package event +package bitmask -import ( - "github.com/stretchr/testify/assert" - "testing" -) +// Bitmask is the map-backed bitmask. +// Each bit index i is split into: +// +// wordIdx = i / 64 +// bitPos = i % 64 +// +// This allows a virtually unbounded sparse bitset over uint64. +type Bitmask struct { + words map[uint]uint +} + +func New() *Bitmask { + return &Bitmask{ + words: make(map[uint]uint), + } +} -func TestCategoryMasks(t *testing.T) { - var masks CategoryMasks - masks.Set(File) - masks.Set(Process) +func (b *Bitmask) Set(i uint) { + word := i / 64 + bit := i % 64 + b.words[word] |= 1 << bit +} + +func (b *Bitmask) Clear(i uint) { + word := i / 64 + bit := i % 64 + b.words[word] &^= 1 << bit + if b.words[word] == 0 { + delete(b.words, word) + } +} - assert.True(t, masks.Test(File)) - assert.True(t, masks.Test(Process)) - assert.False(t, masks.Test(Registry)) +func (b *Bitmask) IsSet(i uint) bool { + word := i / 64 + bit := i % 64 + return b.words[word]&(1< Date: Sat, 7 Jun 2025 22:50:06 +0200 Subject: [PATCH 10/42] perf(rule-engine, eventsource): Event existence checks against bitmask --- internal/etw/consumer.go | 15 +++++----- internal/etw/source_test.go | 7 +++++ internal/etw/stackext_test.go | 3 ++ internal/etw/trace_test.go | 2 ++ pkg/config/eventsource.go | 52 ++++++++++++++++++++++++++-------- pkg/config/eventsource_test.go | 4 +-- pkg/event/event_windows.go | 6 +++- pkg/event/metainfo_windows.go | 34 ++++++++++++++++++++-- pkg/sys/etw/types.go | 18 ++++++++++++ pkg/sys/etw/types_test.go | 5 ++++ 10 files changed, 121 insertions(+), 25 deletions(-) diff --git a/internal/etw/consumer.go b/internal/etw/consumer.go index 78fed5590..f2ce29b4a 100644 --- a/internal/etw/consumer.go +++ b/internal/etw/consumer.go @@ -74,22 +74,21 @@ func (c *Consumer) ProcessEvent(ev *etw.EventRecord) error { if c.isClosing { return nil } - if event.IsCurrentProcDropped(ev.Header.ProcessID) { + + if !c.config.EventSource.EventExists(ev.ID()) { + eventsUnknown.Add(1) return nil } - if c.config.EventSource.ExcludeEvent(ev.Header.ProviderID, ev.HookID()) { - eventsExcluded.Add(1) + if event.IsCurrentProcDropped(ev.Header.ProcessID) { return nil } - - etype := event.NewFromEventRecord(ev) - if !etype.Exists() { - eventsUnknown.Add(1) + if c.config.EventSource.ExcludeEvent(ev.ID()) { + eventsExcluded.Add(1) return nil } eventsProcessed.Add(1) - evt := event.New(c.sequencer.Get(), etype, ev) + evt := event.New(c.sequencer.Get(), ev) // Dispatch each event to the processor chain. // Processors may further augment the event with diff --git a/internal/etw/source_test.go b/internal/etw/source_test.go index 3065f7a39..830dda6ad 100644 --- a/internal/etw/source_test.go +++ b/internal/etw/source_test.go @@ -123,6 +123,7 @@ func TestEventSourceStartTraces(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + tt.cfg.EventSource.Init() evs := NewEventSource(psnap, hsnap, tt.cfg, nil) require.NoError(t, evs.Open(tt.cfg)) defer evs.Close() @@ -193,6 +194,7 @@ func TestEventSourceEnableFlagsDynamically(t *testing.T) { Filters: &config.Filters{}, } + cfg.EventSource.Init() evs := NewEventSource(psnap, hsnap, cfg, r) require.NoError(t, evs.Open(cfg)) defer evs.Close() @@ -277,6 +279,7 @@ func TestEventSourceEnableFlagsDynamicallyWithYaraEnabled(t *testing.T) { }, } + cfg.EventSource.Init() evs := NewEventSource(psnap, hsnap, cfg, r) require.NoError(t, evs.Open(cfg)) defer evs.Close() @@ -328,6 +331,7 @@ func TestEventSourceRundownEvents(t *testing.T) { Filters: &config.Filters{}, } + cfg.EventSource.Init() evs := NewEventSource(psnap, hsnap, cfg, nil) l := &MockListener{} @@ -741,6 +745,7 @@ func TestEventSourceAllEvents(t *testing.T) { StackEnrichment: false, } + evsConfig.Init() cfg := &config.Config{EventSource: evsConfig, Filters: &config.Filters{}} evs := NewEventSource(psnap, hsnap, cfg, nil) @@ -1226,6 +1231,8 @@ func testCallstackEnrichment(t *testing.T, hsnap handle.Snapshotter, psnap ps.Sn FlushTimer: 1, } + evsConfig.Init() + cfg := &config.Config{ EventSource: evsConfig, Filters: &config.Filters{}, diff --git a/internal/etw/stackext_test.go b/internal/etw/stackext_test.go index 480761b73..48b92b2b1 100644 --- a/internal/etw/stackext_test.go +++ b/internal/etw/stackext_test.go @@ -38,6 +38,9 @@ func TestStackExtensions(t *testing.T) { FlushTimer: time.Millisecond * 2300, }, } + + cfg.EventSource.Init() + exts := NewStackExtensions(cfg.EventSource) assert.Len(t, exts.EventIds(), 0) diff --git a/internal/etw/trace_test.go b/internal/etw/trace_test.go index 4b0fc4560..64b6a1929 100644 --- a/internal/etw/trace_test.go +++ b/internal/etw/trace_test.go @@ -36,6 +36,8 @@ func TestStartTrace(t *testing.T) { }, } + cfg.EventSource.Init() + trace := NewKernelTrace(cfg) require.NoError(t, trace.Start()) require.True(t, trace.IsStarted()) diff --git a/pkg/config/eventsource.go b/pkg/config/eventsource.go index 20fddfde2..60a316deb 100644 --- a/pkg/config/eventsource.go +++ b/pkg/config/eventsource.go @@ -23,7 +23,7 @@ package config import ( "github.com/rabbitstack/fibratus/pkg/event" - "golang.org/x/sys/windows" + "github.com/rabbitstack/fibratus/pkg/util/bitmask" "runtime" "time" @@ -102,7 +102,8 @@ type EventSourceConfig struct { // ExcludedImages are process image names that will be rejected if they generate a kernel event. ExcludedImages []string `json:"blacklist.images" yaml:"blacklist.images"` - dropMasks event.EventsetMasks + dropMasks *bitmask.Bitmask + allMasks *bitmask.Bitmask excludedImages map[string]bool } @@ -127,13 +128,21 @@ func (c *EventSourceConfig) initFromViper(v *viper.Viper) { c.ExcludedEvents = v.GetStringSlice(excludedEvents) c.ExcludedImages = v.GetStringSlice(excludedImages) + c.dropMasks = bitmask.New() + c.allMasks = bitmask.New() + c.excludedImages = make(map[string]bool) for _, name := range c.ExcludedEvents { if typ := event.NameToType(name); typ != event.UnknownType { - c.dropMasks.Set(typ) + c.dropMasks.Set(typ.ID()) } } + + for _, typ := range event.AllWithState() { + c.allMasks.Set(typ.ID()) + } + for _, name := range c.ExcludedImages { c.excludedImages[name] = true } @@ -142,35 +151,54 @@ func (c *EventSourceConfig) initFromViper(v *viper.Viper) { // Init is an exported method to allow initializing exclusion maps from external modules. func (c *EventSourceConfig) Init() { c.excludedImages = make(map[string]bool) + + if c.dropMasks == nil { + c.dropMasks = bitmask.New() + } for _, name := range c.ExcludedEvents { for _, typ := range event.NameToTypes(name) { if typ != event.UnknownType { - c.dropMasks.Set(typ) + c.dropMasks.Set(typ.ID()) } } } + for _, name := range c.ExcludedImages { c.excludedImages[name] = true } + + if c.allMasks == nil { + c.allMasks = bitmask.New() + } + for _, typ := range event.AllWithState() { + c.allMasks.Set(typ.ID()) + } } // SetDropMask inserts the event mask in the bitset to // instruct the given event type should be dropped from // the event stream. -func (c *EventSourceConfig) SetDropMask(Type event.Type) { - c.dropMasks.Set(Type) +func (c *EventSourceConfig) SetDropMask(typ event.Type) { + c.dropMasks.Set(typ.ID()) } // TestDropMask checks if the specified event type has // the drop mask in the bitset. -func (c *EventSourceConfig) TestDropMask(Type event.Type) bool { - return c.dropMasks.Test(Type.GUID(), Type.HookID()) +func (c *EventSourceConfig) TestDropMask(typ event.Type) bool { + return c.dropMasks.IsSet(typ.ID()) +} + +// ExcludeEvent determines whether the supplied short +// event ID exists in the bitset of excluded events. +func (c *EventSourceConfig) ExcludeEvent(id uint) bool { + return c.dropMasks.IsSet(id) } -// ExcludeEvent determines whether the supplied provider GUID -// and the hook identifier are in the bitset of excluded events. -func (c *EventSourceConfig) ExcludeEvent(guid windows.GUID, hookID uint16) bool { - return c.dropMasks.Test(guid, hookID) +// EventExists determines if the provided event ID exists +// in the internal event catalog by checking the event ID +// bitmask. +func (c *EventSourceConfig) EventExists(id uint) bool { + return c.allMasks.IsSet(id) } // ExcludeImage determines whether the process generating event is present in the diff --git a/pkg/config/eventsource_test.go b/pkg/config/eventsource_test.go index cd43354fb..614df5ef7 100644 --- a/pkg/config/eventsource_test.go +++ b/pkg/config/eventsource_test.go @@ -55,8 +55,8 @@ func TestEventSourceConfig(t *testing.T) { assert.False(t, c.EventSource.EnableImageEvents) assert.False(t, c.EventSource.EnableFileIOEvents) - assert.True(t, c.EventSource.ExcludeEvent(event.CloseHandle.GUID(), event.CloseHandle.HookID())) - assert.False(t, c.EventSource.ExcludeEvent(event.CreateProcess.GUID(), event.CreateProcess.HookID())) + assert.True(t, c.EventSource.ExcludeEvent(event.CloseHandle.ID())) + assert.False(t, c.EventSource.ExcludeEvent(event.CreateProcess.ID())) assert.True(t, c.EventSource.ExcludeImage(&pstypes.PS{Name: "svchost.exe"})) assert.False(t, c.EventSource.ExcludeImage(&pstypes.PS{Name: "explorer.exe"})) diff --git a/pkg/event/event_windows.go b/pkg/event/event_windows.go index 6269605ea..32e103863 100644 --- a/pkg/event/event_windows.go +++ b/pkg/event/event_windows.go @@ -47,13 +47,15 @@ var ( // New constructs a fresh event instance with basic fields and parameters // from the raw ETW event record. -func New(seq uint64, typ Type, evt *etw.EventRecord) *Event { +func New(seq uint64, evt *etw.EventRecord) *Event { var ( pid = evt.Header.ProcessID tid = evt.Header.ThreadID cpu = *(*uint8)(unsafe.Pointer(&evt.BufferContext.ProcessorIndex[0])) ts = filetime.ToEpoch(evt.Header.Timestamp) + typ = NewTypeFromEventRecord(evt) ) + e := &Event{ Seq: seq, PID: pid, @@ -68,8 +70,10 @@ func New(seq uint64, typ Type, evt *etw.EventRecord) *Event { Metadata: make(map[MetadataKey]any), Host: hostname.Get(), } + e.produceParams(evt) e.adjustPID() + return e } diff --git a/pkg/event/metainfo_windows.go b/pkg/event/metainfo_windows.go index a052be00c..d400563eb 100644 --- a/pkg/event/metainfo_windows.go +++ b/pkg/event/metainfo_windows.go @@ -216,12 +216,42 @@ var indexedEvents = []Info{ // All returns all event types. func All() []Type { s := make([]Type, 0, len(types)) - for _, Type := range types { - s = append(s, Type) + for _, typ := range types { + s = append(s, typ) } return s } +// AllWithState returns all event types + +// event types used for state management. +func AllWithState() []Type { + s := All() + + s = append(s, ProcessRundown) + s = append(s, ThreadRundown) + s = append(s, ImageRundown) + s = append(s, FileRundown) + s = append(s, RegKCBRundown) + s = append(s, RegCreateKCB) + s = append(s, RegDeleteKCB) + s = append(s, FileOpEnd) + s = append(s, ReleaseFile) + s = append(s, MapFileRundown) + s = append(s, StackWalk) + + return s +} + +// MaxTypeID returns the maximum event type (hook id) value. +func MaxTypeID() uint16 { + types := AllWithState() + ids := make([]uint16, len(types)) + for i, t := range types { + ids[i] = t.HookID() + } + return slices.Max(ids) +} + // TypeToEventInfo maps the event type to the structure storing detailed information about the event. func TypeToEventInfo(typ Type) Info { if info, ok := events[typ]; ok { diff --git a/pkg/sys/etw/types.go b/pkg/sys/etw/types.go index 27c1a37e0..ffc6c881b 100644 --- a/pkg/sys/etw/types.go +++ b/pkg/sys/etw/types.go @@ -564,6 +564,24 @@ func (e *EventRecord) HookID() uint16 { return e.Header.EventDescriptor.ID } +// ID is an unsigned integer that uniquely +// identifies the event. Handy for bitmask +// operations. +func (e *EventRecord) ID() uint { + d1 := e.Header.ProviderID.Data1 + d2 := e.Header.ProviderID.Data2 + + id := uint(byte(d1>>24))<<56 | + uint(byte(d1>>16))<<48 | + uint(byte(d1>>8))<<40 | + uint(byte(d1))<<32 | + uint(byte(d2>>8))<<24 | + uint(byte(d2))<<16 | + uint(e.HookID()) + + return id +} + // ReadByte reads the byte from the buffer at the specified offset. func (e *EventRecord) ReadByte(offset uint16) byte { if offset > e.BufferLen { diff --git a/pkg/sys/etw/types_test.go b/pkg/sys/etw/types_test.go index f1b4bc087..da44ba3dd 100644 --- a/pkg/sys/etw/types_test.go +++ b/pkg/sys/etw/types_test.go @@ -98,3 +98,8 @@ func TestReadBuffer(t *testing.T) { tt.assertions(t, ev) } } + +func TestID(t *testing.T) { + ev := &EventRecord{Header: EventHeader{ProviderID: ThreadpoolGUID, EventDescriptor: EventDescriptor{ID: 44}}} + assert.Equal(t, uint(14439051552138264620), ev.ID()) +} From e079b38f813352606908b72a8b3b581d51c5d3b0 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Sat, 7 Jun 2025 22:50:27 +0200 Subject: [PATCH 11/42] perf(rule-engine): Use bitsets for determining if the event is evaluable by the expression --- pkg/filter/ql/literal.go | 33 +++++++++--- pkg/filter/ql/literal_test.go | 98 +++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 8 deletions(-) create mode 100644 pkg/filter/ql/literal_test.go diff --git a/pkg/filter/ql/literal.go b/pkg/filter/ql/literal.go index 8da866f36..d102d851e 100644 --- a/pkg/filter/ql/literal.go +++ b/pkg/filter/ql/literal.go @@ -278,10 +278,8 @@ type SequenceExpr struct { // Alias represents the sequence expression alias. Alias string - emasks event.EventsetMasks - cmasks event.CategoryMasks - - types []event.Type + bitsets event.BitSets + types []event.Type } func (e *SequenceExpr) init() { @@ -335,28 +333,47 @@ func (e *SequenceExpr) walk() { WalkFunc(e.Expr, walk) + uniqCats := make(map[event.Category]bool) + // initialize event type/category buckets for every such field for name, values := range stringFields { for _, v := range values { switch name { case fields.EvtName: for _, typ := range event.NameToTypes(v) { - e.emasks.Set(typ) + if typ == event.UnknownType { + continue + } e.types = append(e.types, typ) + uniqCats[event.TypeToEventInfo(typ).Category] = true } case fields.EvtCategory: - e.cmasks.Set(event.Category(v)) + e.bitsets.SetCategoryBit(event.Category(v)) } } } + + for _, t := range e.types { + switch len(uniqCats) { + case 0: + continue + case 1: + // happy path can use a single bitmask for all + // event types pertaining to the same category + e.bitsets.SetBit(event.TypeBitSet, t) + default: + // use map-backed bitmask for event identifiers + e.bitsets.SetBit(event.BitmaskBitSet, t) + } + } } // IsEvaluable determines if the expression should be evaluated by inspecting // the event type filter fields defined in the expression. We permit the expression -// to be evaluated when the incoming event type or category pertains to the one +// to be evaluated when the incoming event type, ID, or category pertains to the one // defined in the field literal. func (e *SequenceExpr) IsEvaluable(evt *event.Event) bool { - return e.emasks.Test(evt.Type.GUID(), evt.Type.HookID()) || e.cmasks.Test(evt.Category) + return e.bitsets.IsBitSet(evt) } // HasBoundFields determines if this sequence expression references any bound field. diff --git a/pkg/filter/ql/literal_test.go b/pkg/filter/ql/literal_test.go new file mode 100644 index 000000000..92269d2e9 --- /dev/null +++ b/pkg/filter/ql/literal_test.go @@ -0,0 +1,98 @@ +/* + * Copyright 2021-present by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ql + +import ( + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestSequenceExprIsEvaluable(t *testing.T) { + var tests = []struct { + expr string + evt *event.Event + isEval bool + assertions func(t *testing.T, sexpr *SequenceExpr) + }{ + {"evt.name = 'CreateProcess'", &event.Event{Type: event.CreateProcess, Category: event.Process}, true, + func(t *testing.T, sexpr *SequenceExpr) { + assert.True(t, sexpr.bitsets.IsTypesInitialized()) + assert.False(t, sexpr.bitsets.IsBitmaskInitialized()) + assert.False(t, sexpr.bitsets.IsCategoryInitialized()) + }, + }, + {"evt.name = 'CreateProcess'", &event.Event{Type: event.TerminateProcess, Category: event.Process}, false, nil}, + {"evt.name = 'CreateProcess' or evt.name = 'TerminateThread'", &event.Event{Type: event.TerminateProcess, Category: event.Process}, false, nil}, + {"evt.name = 'CreateProcess' or evt.category = 'object'", &event.Event{Type: event.TerminateProcess, Category: event.Process}, false, nil}, + {"evt.name = 'CreateProcess' or evt.name = 'OpenProcess'", &event.Event{Type: event.OpenProcess, Category: event.Process}, true, + func(t *testing.T, sexpr *SequenceExpr) { + assert.True(t, sexpr.bitsets.IsTypesInitialized()) + assert.False(t, sexpr.bitsets.IsBitmaskInitialized()) + assert.False(t, sexpr.bitsets.IsCategoryInitialized()) + }, + }, + {"evt.name = 'CreateProcess' or evt.name = 'CreateThread'", &event.Event{Type: event.CreateThread, Category: event.Thread}, true, + func(t *testing.T, sexpr *SequenceExpr) { + assert.False(t, sexpr.bitsets.IsTypesInitialized()) + assert.True(t, sexpr.bitsets.IsBitmaskInitialized()) + assert.False(t, sexpr.bitsets.IsCategoryInitialized()) + }, + }, + {"evt.name = 'CreateProcess' or evt.category = 'registry'", &event.Event{Type: event.RegSetValue, Category: event.Registry}, true, + func(t *testing.T, sexpr *SequenceExpr) { + assert.True(t, sexpr.bitsets.IsTypesInitialized()) + assert.False(t, sexpr.bitsets.IsBitmaskInitialized()) + assert.True(t, sexpr.bitsets.IsCategoryInitialized()) + }, + }, + {"evt.name = 'CreateProcess' or evt.name = 'OpenProcess' or evt.category = 'registry'", &event.Event{Type: event.OpenProcess, Category: event.Process}, true, + func(t *testing.T, sexpr *SequenceExpr) { + assert.True(t, sexpr.bitsets.IsTypesInitialized()) + assert.False(t, sexpr.bitsets.IsBitmaskInitialized()) + assert.True(t, sexpr.bitsets.IsCategoryInitialized()) + }, + }, + {"evt.name = 'CreateProcess' or evt.name = 'SetThreadContext' or evt.category = 'registry'", &event.Event{Type: event.CreateProcess, Category: event.Process}, true, + func(t *testing.T, sexpr *SequenceExpr) { + assert.False(t, sexpr.bitsets.IsTypesInitialized()) + assert.True(t, sexpr.bitsets.IsBitmaskInitialized()) + assert.True(t, sexpr.bitsets.IsCategoryInitialized()) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.expr, func(t *testing.T) { + p := NewParser(tt.expr) + expr, err := p.ParseExpr() + require.NoError(t, err) + + sexpr := &SequenceExpr{Expr: expr} + sexpr.init() + sexpr.walk() + + assert.Equal(t, tt.isEval, sexpr.IsEvaluable(tt.evt)) + if tt.assertions != nil { + tt.assertions(t, sexpr) + } + }) + } +} From 613e417ddfd187e69703c8ac48d271874e1aa340 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Sat, 7 Jun 2025 22:50:45 +0200 Subject: [PATCH 12/42] chore(event): Remove CategoryMasks structure --- pkg/event/category.go | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/pkg/event/category.go b/pkg/event/category.go index 119dd55bd..0f86043c7 100644 --- a/pkg/event/category.go +++ b/pkg/event/category.go @@ -19,7 +19,6 @@ package event import ( - "github.com/bits-and-blooms/bitset" "github.com/rabbitstack/fibratus/pkg/util/hashers" ) @@ -70,21 +69,6 @@ func (c Category) Hash() uint32 { return hashers.FnvUint32([]byte(c)) } -// CategoryMasks allows setting and checking the category bit mask. -type CategoryMasks struct { - bs bitset.BitSet -} - -// Set sets the category bit in the bit mask. -func (m *CategoryMasks) Set(c Category) { - m.bs.Set(uint(c.Index())) -} - -// Test checks if the given category bit is set. -func (m *CategoryMasks) Test(c Category) bool { - return m.bs.Test(uint(c.Index())) -} - // MaxCategoryIndex designates the maximum category index. const MaxCategoryIndex = 13 From b291a0520f14e46f8a8b2d8943b06885718ebb99 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Wed, 28 May 2025 19:34:57 +0200 Subject: [PATCH 13/42] perf: Speed up the `wildcard` function Avoid unnecessary conversion to rune slice to alleviate the pressure on the heap allocator. --- pkg/util/wildcard/wildcard.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/util/wildcard/wildcard.go b/pkg/util/wildcard/wildcard.go index a8d092eff..86d88db51 100644 --- a/pkg/util/wildcard/wildcard.go +++ b/pkg/util/wildcard/wildcard.go @@ -27,27 +27,27 @@ func Match(pattern, name string) (matched bool) { if pattern == "*" { return true } - // Does extended wildcard '*' and '?' match. - return deepMatchRune([]rune(name), []rune(pattern), false) + // Does extended wildcard '*' and '?' match? + return deepMatchRune(name, pattern, false) } -func deepMatchRune(str, pattern []rune, simple bool) bool { +func deepMatchRune(s, pattern string, simple bool) bool { for len(pattern) > 0 { switch pattern[0] { default: - if len(str) == 0 || str[0] != pattern[0] { + if len(s) == 0 || s[0] != pattern[0] { return false } case '?': - if len(str) == 0 && !simple { + if len(s) == 0 && !simple { return false } case '*': - return deepMatchRune(str, pattern[1:], simple) || - (len(str) > 0 && deepMatchRune(str[1:], pattern, simple)) + return deepMatchRune(s, pattern[1:], simple) || + (len(s) > 0 && deepMatchRune(s[1:], pattern, simple)) } - str = str[1:] + s = s[1:] pattern = pattern[1:] } - return len(str) == 0 && len(pattern) == 0 + return len(s) == 0 && len(pattern) == 0 } From 620d63c3f9b206396c1decd3f343c0c8b4b42741 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Fri, 30 May 2025 18:49:19 +0200 Subject: [PATCH 14/42] chore(build): Default to dev version if unset --- CONTRIBUTING.md | 1 - make.bat | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0cce23494..12b2ebbed 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -163,7 +163,6 @@ $ dotnet tool install --global wix --version 5.0.0 Now use the `pkg` target from the `./make.bat` script to build the MSI package. ``` -$ $env:VERSION="0.0.0" $ ./make.bat pkg ``` diff --git a/make.bat b/make.bat index b17ae597d..f224e3e65 100644 --- a/make.bat +++ b/make.bat @@ -22,6 +22,10 @@ set GOTEST=go test -timeout=10m -v -gcflags=all=-d=checkptr=0 set GOFMT=gofmt -e -s -l -w set GOLINT=%GOBIN%\golangci-lint +if NOT DEFINED VERSION ( + set VERSION="0.0.0" +) + FOR /F "tokens=* USEBACKQ" %%F IN (`powershell -Command get-date -format "{dd-MM-yyyy.HH:mm:ss}"`) DO ( SET BUILD_DATE=%%F ) From 158d0d0311497dc7fa35976146fae95f2a8fb863 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Mon, 9 Jun 2025 20:56:42 +0200 Subject: [PATCH 15/42] chore: Use standard library atomic bool --- pkg/rules/sequence.go | 3 +-- pkg/util/atomic/atomic.go | 44 --------------------------------------- 2 files changed, 1 insertion(+), 46 deletions(-) delete mode 100644 pkg/util/atomic/atomic.go diff --git a/pkg/rules/sequence.go b/pkg/rules/sequence.go index e1031fbad..d5bcf2ff4 100644 --- a/pkg/rules/sequence.go +++ b/pkg/rules/sequence.go @@ -28,10 +28,10 @@ import ( "github.com/rabbitstack/fibratus/pkg/filter" "github.com/rabbitstack/fibratus/pkg/filter/ql" "github.com/rabbitstack/fibratus/pkg/ps" - "github.com/rabbitstack/fibratus/pkg/util/atomic" log "github.com/sirupsen/logrus" "sort" "sync" + "sync/atomic" "time" ) @@ -130,7 +130,6 @@ func newSequenceState(f filter.Filter, c *config.FilterConfig, psnap ps.Snapshot exprs: make(map[int]string), spanDeadlines: make(map[fsm.State]*time.Timer), initialState: sequenceInitialState, - inDeadline: atomic.MakeBool(false), psnap: psnap, } diff --git a/pkg/util/atomic/atomic.go b/pkg/util/atomic/atomic.go deleted file mode 100644 index d0345f863..000000000 --- a/pkg/util/atomic/atomic.go +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2021-2022 by Nedim Sabic Sabic - * https://www.fibratus.io - * All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package atomic - -import atom "sync/atomic" - -// Bool provides an atomic boolean type. -type Bool struct{ u Uint32 } - -// Uint32 provides an atomic uint32 type. -type Uint32 struct{ value uint32 } - -func MakeBool(v bool) Bool { return Bool{MakeUint32(btoi(v))} } -func NewBool(v bool) *Bool { return &Bool{MakeUint32(btoi(v))} } -func (b *Bool) Load() bool { return b.u.Load() == 1 } -func (b *Bool) Store(v bool) { b.u.Store(btoi(v)) } - -func MakeUint32(v uint32) Uint32 { return Uint32{v} } -func NewUint32(v uint32) *Uint32 { return &Uint32{v} } -func (u *Uint32) Load() uint32 { return atom.LoadUint32(&u.value) } -func (u *Uint32) Store(v uint32) { atom.StoreUint32(&u.value, v) } - -func btoi(b bool) uint32 { - if b { - return 1 - } - return 0 -} From 77889f1f0461618c3e46618f8d26ea407551dbc1 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Fri, 20 Jun 2025 11:44:42 +0200 Subject: [PATCH 16/42] feat(telemetry): Introduce Windows Kernel Process Provider This provider is fundamentally employed to enrich the process state with additional attributes such as integrity level and token elevation info. --- internal/etw/consumer.go | 2 +- internal/etw/processors/ps_windows.go | 6 ++- internal/etw/source.go | 46 ++++++++++------- internal/etw/source_test.go | 10 ++-- internal/etw/trace.go | 37 ++++++++++++-- pkg/event/event_windows.go | 46 +++++++++-------- pkg/event/metainfo_windows.go | 2 + pkg/event/param_windows.go | 72 +++++++++++++++++++++------ pkg/event/types_windows.go | 23 ++++++--- pkg/sys/etw/etw.go | 20 ++++++-- pkg/sys/etw/types.go | 50 ++++++++++++++++++- pkg/sys/etw/types_test.go | 2 +- 12 files changed, 239 insertions(+), 77 deletions(-) diff --git a/internal/etw/consumer.go b/internal/etw/consumer.go index f2ce29b4a..6f5d2d979 100644 --- a/internal/etw/consumer.go +++ b/internal/etw/consumer.go @@ -79,7 +79,7 @@ func (c *Consumer) ProcessEvent(ev *etw.EventRecord) error { eventsUnknown.Add(1) return nil } - if event.IsCurrentProcDropped(ev.Header.ProcessID) { + if event.IsCurrentProcDropped(ev.Header.ProcessID) && ev.Header.ProviderID != etw.WindowsKernelProcessGUID { return nil } if c.config.EventSource.ExcludeEvent(ev.ID()) { diff --git a/internal/etw/processors/ps_windows.go b/internal/etw/processors/ps_windows.go index a52fe2e83..0e104e07c 100644 --- a/internal/etw/processors/ps_windows.go +++ b/internal/etw/processors/ps_windows.go @@ -41,7 +41,7 @@ func newPsProcessor(psnap ps.Snapshotter, regionProber *va.RegionProber) Process func (p psProcessor) ProcessEvent(e *event.Event) (*event.Event, bool, error) { switch e.Type { - case event.CreateProcess, event.TerminateProcess, event.ProcessRundown: + case event.CreateProcess, event.CreateProcessInternal, event.TerminateProcess, event.ProcessRundown, event.ProcessRundownInternal: evt, err := p.processEvent(e) if evt.IsTerminateProcess() { p.regionProber.Remove(evt.Params.MustGetPid()) @@ -86,6 +86,10 @@ func (p psProcessor) ProcessEvent(e *event.Event) (*event.Event, bool, error) { //nolint:unparam func (p psProcessor) processEvent(e *event.Event) (*event.Event, error) { + if e.IsCreateProcessInternal() || e.IsProcessRundownInternal() { + return e, nil + } + cmndline := cmdline.New(e.GetParamAsString(params.Cmdline)). // get rid of leading/trailing quotes in the executable path CleanExe(). diff --git a/internal/etw/source.go b/internal/etw/source.go index 7aa84f3cf..a8b9d299c 100644 --- a/internal/etw/source.go +++ b/internal/etw/source.go @@ -161,12 +161,15 @@ func (e *EventSource) Open(config *config.Config) error { } } - // add the core NT Kernel Logger trace - e.addTrace(NewKernelTrace(config)) - - // security telemetry trace hosts remaining ETW providers + // security telemetry trace hosts all ETW providers but NT Kernel Logger trace := NewTrace(etw.SecurityTelemetrySession, config) + // Windows Kernel Process session permits enriching event state with + // additional attributes and guaranteeing that any event published by + // the security telemetry session doesn't miss its respective process + // from the snapshotter + trace.AddProvider(etw.WindowsKernelProcessGUID, false, WithKeywords(etw.ProcessKeyword), WithCaptureState()) + if config.EventSource.EnableDNSEvents { trace.AddProvider(etw.DNSClientGUID, false) } @@ -186,21 +189,21 @@ func (e *EventSource) Open(config *config.Config) error { trace.AddProvider(etw.ThreadpoolGUID, config.EventSource.StackEnrichment, WithStackExts(stackexts)) } - if trace.HasProviders() { - // add security telemetry trace - e.addTrace(trace) - } + // add security telemetry trace + e.addTrace(trace) + // add the core NT Kernel Logger trace + e.addTrace(NewKernelTrace(config)) - for _, trace := range e.traces { - err := trace.Start() + for _, t := range e.traces { + err := t.Start() switch err { case errs.ErrTraceAlreadyRunning: - log.Debugf("%s trace is already running. Trying to restart...", trace.Name) - if err := trace.Stop(); err != nil { + log.Debugf("%s trace is already running. Trying to restart...", t.Name) + if err := t.Stop(); err != nil { return err } time.Sleep(time.Millisecond * 100) - if err := trace.Start(); err != nil { + if err := t.Start(); err != nil { return multierror.Wrap(errs.ErrRestartTrace, err) } case errs.ErrTraceNoSysResources: @@ -234,15 +237,22 @@ func (e *EventSource) Open(config *config.Config) error { } e.consumers = append(e.consumers, consumer) - err = trace.Open(consumer, e.errs) + // Open the trace and assign a consumer + err = t.Open(consumer, e.errs) + if err != nil { + return fmt.Errorf("unable to open %s trace: %v", t.Name, err) + } + log.Infof("starting [%s] trace processing", t.Name) + + // Instruct the provider to emit state information + err = t.CaptureState() if err != nil { - return fmt.Errorf("unable to open %s trace: %v", trace.Name, err) + log.Warn(err) } - log.Infof("starting [%s] trace processing", trace.Name) // Start event processing loop errch := make(chan error) - go trace.Process(errch) + go t.Process(errch) go func(trace *Trace) { select { @@ -254,7 +264,7 @@ func (e *EventSource) Open(config *config.Config) error { e.errs <- fmt.Errorf("unable to process %s trace: %v", trace.Name, err) } } - }(trace) + }(t) } return nil diff --git a/internal/etw/source_test.go b/internal/etw/source_test.go index 830dda6ad..adf7959d6 100644 --- a/internal/etw/source_test.go +++ b/internal/etw/source_test.go @@ -98,7 +98,7 @@ func TestEventSourceStartTraces(t *testing.T) { }, Filters: &config.Filters{}, }, - 1, + 2, []etw.EventTraceFlags{0x6018203, 0}, }, {"start kernel and security telemetry logger sessions", @@ -199,10 +199,10 @@ func TestEventSourceEnableFlagsDynamically(t *testing.T) { require.NoError(t, evs.Open(cfg)) defer evs.Close() - flags := evs.(*EventSource).traces[0].enableFlagsDynamically(cfg.EventSource) - require.Len(t, evs.(*EventSource).traces, 2) + flags := evs.(*EventSource).traces[1].enableFlagsDynamically(cfg.EventSource) + require.True(t, flags&etw.FileIO != 0) require.True(t, flags&etw.Process != 0) // rules compile result doesn't have the thread event @@ -284,7 +284,9 @@ func TestEventSourceEnableFlagsDynamicallyWithYaraEnabled(t *testing.T) { require.NoError(t, evs.Open(cfg)) defer evs.Close() - flags := evs.(*EventSource).traces[0].enableFlagsDynamically(cfg.EventSource) + require.Len(t, evs.(*EventSource).traces, 2) + + flags := evs.(*EventSource).traces[1].enableFlagsDynamically(cfg.EventSource) // rules compile result doesn't have file events // but Yara file scanning is enabled diff --git a/internal/etw/trace.go b/internal/etw/trace.go index c64a6c420..98d7eb31b 100644 --- a/internal/etw/trace.go +++ b/internal/etw/trace.go @@ -56,6 +56,7 @@ func initEventTraceProps(c config.EventSourceConfig) etw.EventTraceProperties { if flushTimer < time.Second { flushTimer = time.Second } + mode := uint32(etw.ProcessTraceModeRealtime) return etw.EventTraceProperties{ @@ -88,6 +89,9 @@ type ProviderInfo struct { // EnableStacks indicates if callstacks are enabled for // this provider. EnableStacks bool + // CaptureState requests that the provider log its state + // information, such as rundown events. + CaptureState bool // stackExtensions manager stack tracing enablement. // For each event present in the stack identifiers, // the StackWalk event is published by the provider. @@ -146,8 +150,9 @@ type Trace struct { } type opts struct { - stackexts *StackExtensions - keywords uint64 + stackexts *StackExtensions + keywords uint64 + captureState bool } // Option represents the option for the trace. @@ -168,6 +173,14 @@ func WithKeywords(keywords uint64) Option { } } +// WithCaptureState indicates that the provider should +// emit its state information. +func WithCaptureState() Option { + return func(o *opts) { + o.captureState = true + } +} + // NewKernelTrace creates a new NT Kernel Logger trace. func NewKernelTrace(config *config.Config) *Trace { t := &Trace{Name: etw.KernelLoggerSession, GUID: etw.KernelTraceControlGUID, stackExtensions: NewStackExtensions(config.EventSource), config: config} @@ -193,7 +206,10 @@ func (t *Trace) AddProvider(guid windows.GUID, enableStacks bool, options ...Opt opt(&opts) } - t.Providers = append(t.Providers, ProviderInfo{GUID: guid, Keywords: opts.keywords, EnableStacks: enableStacks, stackExtensions: opts.stackexts}) + t.Providers = append( + t.Providers, + ProviderInfo{GUID: guid, Keywords: opts.keywords, EnableStacks: enableStacks, CaptureState: opts.captureState, stackExtensions: opts.stackexts}, + ) } // HasProviders determines if this trace contains providers. @@ -235,6 +251,7 @@ func (t *Trace) Start() error { cfg := t.config.EventSource props := initEventTraceProps(cfg) flags := t.enableFlagsDynamically(cfg) + if t.IsKernelTrace() { props.EnableFlags = flags props.Wnode.GUID = t.GUID @@ -408,6 +425,20 @@ func (t *Trace) Close() error { return etw.CloseTrace(t.openHandle) } +// CaptureState forces the provider to publish state +// information such as rundown events. +func (t *Trace) CaptureState() error { + for _, provider := range t.Providers { + if !provider.CaptureState { + continue + } + if err := etw.CaptureProviderState(provider.GUID, t.startHandle); err != nil { + return fmt.Errorf("unable to capture %s provider state: %v", provider.GUID, err) + } + } + return nil +} + // IsKernelTrace determines if this is the system logger trace. func (t *Trace) IsKernelTrace() bool { return t.GUID == etw.KernelTraceControlGUID } diff --git a/pkg/event/event_windows.go b/pkg/event/event_windows.go index 32e103863..7bc832d46 100644 --- a/pkg/event/event_windows.go +++ b/pkg/event/event_windows.go @@ -202,8 +202,8 @@ func (e *Event) IsSuccess() bool { // the tracing session to induce the arrival of rundown events // by calling into the `etw.SetTraceInformation` Windows API // function which causes duplicate rundown events. -// For more pointers check `kstream/controller_windows.go` -// and the `etw.SetTraceInformation` API function. +// For more pointers check `internal/etw/trace.go` and the +// `etw.SetTraceInformation` API function. func (e *Event) IsRundownProcessed() bool { mu.Lock() defer mu.Unlock() @@ -216,26 +216,28 @@ func (e *Event) IsRundownProcessed() bool { return false } -func (e *Event) IsCreateFile() bool { return e.Type == CreateFile } -func (e *Event) IsCreateProcess() bool { return e.Type == CreateProcess } -func (e *Event) IsCreateThread() bool { return e.Type == CreateThread } -func (e *Event) IsCloseFile() bool { return e.Type == CloseFile } -func (e *Event) IsCreateHandle() bool { return e.Type == CreateHandle } -func (e *Event) IsCloseHandle() bool { return e.Type == CloseHandle } -func (e *Event) IsDeleteFile() bool { return e.Type == DeleteFile } -func (e *Event) IsEnumDirectory() bool { return e.Type == EnumDirectory } -func (e *Event) IsTerminateProcess() bool { return e.Type == TerminateProcess } -func (e *Event) IsTerminateThread() bool { return e.Type == TerminateThread } -func (e *Event) IsUnloadImage() bool { return e.Type == UnloadImage } -func (e *Event) IsLoadImage() bool { return e.Type == LoadImage } -func (e *Event) IsImageRundown() bool { return e.Type == ImageRundown } -func (e *Event) IsFileOpEnd() bool { return e.Type == FileOpEnd } -func (e *Event) IsRegSetValue() bool { return e.Type == RegSetValue } -func (e *Event) IsProcessRundown() bool { return e.Type == ProcessRundown } -func (e *Event) IsVirtualAlloc() bool { return e.Type == VirtualAlloc } -func (e *Event) IsMapViewFile() bool { return e.Type == MapViewFile } -func (e *Event) IsUnmapViewFile() bool { return e.Type == UnmapViewFile } -func (e *Event) IsStackWalk() bool { return e.Type == StackWalk } +func (e *Event) IsCreateFile() bool { return e.Type == CreateFile } +func (e *Event) IsCreateProcess() bool { return e.Type == CreateProcess } +func (e *Event) IsCreateProcessInternal() bool { return e.Type == CreateProcessInternal } +func (e *Event) IsCreateThread() bool { return e.Type == CreateThread } +func (e *Event) IsCloseFile() bool { return e.Type == CloseFile } +func (e *Event) IsCreateHandle() bool { return e.Type == CreateHandle } +func (e *Event) IsCloseHandle() bool { return e.Type == CloseHandle } +func (e *Event) IsDeleteFile() bool { return e.Type == DeleteFile } +func (e *Event) IsEnumDirectory() bool { return e.Type == EnumDirectory } +func (e *Event) IsTerminateProcess() bool { return e.Type == TerminateProcess } +func (e *Event) IsTerminateThread() bool { return e.Type == TerminateThread } +func (e *Event) IsUnloadImage() bool { return e.Type == UnloadImage } +func (e *Event) IsLoadImage() bool { return e.Type == LoadImage } +func (e *Event) IsImageRundown() bool { return e.Type == ImageRundown } +func (e *Event) IsFileOpEnd() bool { return e.Type == FileOpEnd } +func (e *Event) IsRegSetValue() bool { return e.Type == RegSetValue } +func (e *Event) IsProcessRundown() bool { return e.Type == ProcessRundown } +func (e *Event) IsProcessRundownInternal() bool { return e.Type == ProcessRundownInternal } +func (e *Event) IsVirtualAlloc() bool { return e.Type == VirtualAlloc } +func (e *Event) IsMapViewFile() bool { return e.Type == MapViewFile } +func (e *Event) IsUnmapViewFile() bool { return e.Type == UnmapViewFile } +func (e *Event) IsStackWalk() bool { return e.Type == StackWalk } // InvalidPid indicates if the process generating the event is invalid. func (e *Event) InvalidPid() bool { return e.PID == sys.InvalidProcessID } diff --git a/pkg/event/metainfo_windows.go b/pkg/event/metainfo_windows.go index d400563eb..4560698e4 100644 --- a/pkg/event/metainfo_windows.go +++ b/pkg/event/metainfo_windows.go @@ -238,6 +238,8 @@ func AllWithState() []Type { s = append(s, ReleaseFile) s = append(s, MapFileRundown) s = append(s, StackWalk) + s = append(s, CreateProcessInternal) + s = append(s, ProcessRundownInternal) return s } diff --git a/pkg/event/param_windows.go b/pkg/event/param_windows.go index 021c04bb2..a8922d6a2 100644 --- a/pkg/event/param_windows.go +++ b/pkg/event/param_windows.go @@ -24,7 +24,9 @@ import ( "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/fs" htypes "github.com/rabbitstack/fibratus/pkg/handle/types" + "github.com/rabbitstack/fibratus/pkg/sys" "github.com/rabbitstack/fibratus/pkg/sys/etw" + "github.com/rabbitstack/fibratus/pkg/util/filetime" "github.com/rabbitstack/fibratus/pkg/util/ip" "github.com/rabbitstack/fibratus/pkg/util/key" "github.com/rabbitstack/fibratus/pkg/util/ntstatus" @@ -77,6 +79,9 @@ func (p Param) String() string { if err != nil { return "" } + if p.Name == params.ProcessIntegrityLevel { + return sys.RidToString(sid) + } return sid.String() case params.DOSPath: return devMapper.Convert(p.Value.(string)) @@ -180,7 +185,10 @@ func (pars Params) GetSID() (*windows.SID, error) { func getSID(param *Param) (*windows.SID, error) { sid, ok := param.Value.([]byte) if !ok { - return nil, fmt.Errorf("unable to type cast %q parameter to []byte value", params.UserSID) + return nil, fmt.Errorf("unable to type cast %q parameter to []byte value", param.Name) + } + if sid == nil { + return nil, fmt.Errorf("sid linked to parameter %s is empty", param.Name) } b := uintptr(unsafe.Pointer(&sid[0])) if param.Type == params.WbemSID { @@ -207,9 +215,7 @@ func (pars Params) MustGetSID() *windows.SID { // schema changes in order to parse new fields. func (e *Event) produceParams(evt *etw.EventRecord) { switch e.Type { - case ProcessRundown, - CreateProcess, - TerminateProcess: + case ProcessRundown, CreateProcess, TerminateProcess: var ( kproc uint64 pid, ppid uint32 @@ -247,7 +253,7 @@ func (e *Event) produceParams(evt *etw.EventRecord) { default: offset = 24 } - sid, soffset = evt.ReadSID(offset) + sid, soffset = evt.ReadSID(offset, true) name, noffset = evt.ReadAnsiString(soffset) cmdline, _ = evt.ReadUTF16String(soffset + noffset) e.AppendParam(params.ProcessObject, params.Address, kproc) @@ -261,6 +267,50 @@ func (e *Event) produceParams(evt *etw.EventRecord) { e.AppendParam(params.UserSID, params.WbemSID, sid) e.AppendParam(params.ProcessName, params.AnsiString, name) e.AppendParam(params.Cmdline, params.UnicodeString, cmdline) + case CreateProcessInternal, ProcessRundownInternal: + var ( + pid uint32 + createTime windows.Filetime + ppid uint32 + sessionID uint32 + flags uint32 + tokenElevationType uint32 + tokenIsElevated uint32 + tokenMandatoryLabel []byte + exe string + ) + + pid = evt.ReadUint32(0) + + if (e.IsCreateProcessInternal() && evt.Version() >= 3) || (e.IsProcessRundownInternal() && evt.Version() >= 1) { + createTime = windows.NsecToFiletime(int64(evt.ReadUint64(12))) // skip sequence number (8 bytes) + + ppid = evt.ReadUint32(20) + sessionID = evt.ReadUint32(32) // skip parent sequence number (8 bytes) + flags = evt.ReadUint32(36) + tokenElevationType = evt.ReadUint32(40) + tokenIsElevated = evt.ReadUint32(44) + + tokenMandatoryLabel, _ = evt.ReadSID(48, false) // integrity level SID size is 12 bytes + + exe, _ = evt.ReadNTUnicodeString(60) + } else { + createTime = windows.NsecToFiletime(int64(evt.ReadUint64(8))) + ppid = evt.ReadUint32(16) + sessionID = evt.ReadUint32(20) + flags = evt.ReadUint32(24) + exe, _ = evt.ReadNTUnicodeString(28) + } + + e.AppendParam(params.ProcessID, params.PID, pid) + e.AppendParam(params.StartTime, params.Time, filetime.ToEpoch(uint64(createTime.Nanoseconds()))) + e.AppendParam(params.ProcessParentID, params.PID, ppid) + e.AppendParam(params.SessionID, params.Uint32, sessionID) + e.AppendParam(params.ProcessFlags, params.Flags, flags, WithFlags(PsCreationFlags)) + e.AppendParam(params.ProcessTokenElevationType, params.Enum, tokenElevationType, WithEnum(PsTokenElevationTypes)) + e.AppendParam(params.ProcessTokenIsElevated, params.Bool, tokenIsElevated > 0) + e.AppendParam(params.ProcessIntegrityLevel, params.SID, tokenMandatoryLabel) + e.AppendParam(params.Exe, params.DOSPath, exe) case OpenProcess: processID := evt.ReadUint32(0) desiredAccess := evt.ReadUint32(4) @@ -276,9 +326,7 @@ func (e *Event) produceParams(evt *etw.EventRecord) { ((desiredAccess & windows.PROCESS_CREATE_THREAD) != 0) || ((desiredAccess & windows.PROCESS_SET_INFORMATION) != 0) { e.AppendParam(params.Callstack, params.Slice, evt.Callstack()) } - case CreateThread, - TerminateThread, - ThreadRundown: + case CreateThread, TerminateThread, ThreadRundown: var ( pid uint32 tid uint32 @@ -369,9 +417,7 @@ func (e *Event) produceParams(evt *etw.EventRecord) { e.AppendParam(params.HandleObjectTypeID, params.HandleType, typeID) e.AppendParam(params.ProcessID, params.PID, sourcePID) e.AppendParam(params.TargetProcessID, params.PID, targetPID) - case LoadImage, - UnloadImage, - ImageRundown: + case LoadImage, UnloadImage, ImageRundown: var ( pid uint32 checksum uint32 @@ -512,9 +558,7 @@ func (e *Event) produceParams(evt *etw.EventRecord) { e.AppendParam(params.FileObject, params.Address, fileObject) e.AppendParam(params.FileKey, params.Address, fileKey) e.AppendParam(params.ThreadID, params.TID, tid) - case DeleteFile, - RenameFile, - SetFileInformation: + case DeleteFile, RenameFile, SetFileInformation: var ( irp uint64 fileObject uint64 diff --git a/pkg/event/types_windows.go b/pkg/event/types_windows.go index 125f19d3f..3c2ee743f 100644 --- a/pkg/event/types_windows.go +++ b/pkg/event/types_windows.go @@ -65,6 +65,8 @@ var ( DNSEventGUID = windows.GUID{Data1: 0x1c95126e, Data2: 0x7eea, Data3: 0x49a9, Data4: [8]byte{0xa3, 0xfe, 0xa3, 0x78, 0xb0, 0x3d, 0xdb, 0x4d}} // ThreadpoolGUID represents the thread pool event GUID ThreadpoolGUID = windows.GUID{Data1: 0xc861d0e2, Data2: 0xa2c1, Data3: 0x4d36, Data4: [8]byte{0x9f, 0x9c, 0x97, 0x0b, 0xab, 0x94, 0x3a, 0x12}} + // ProcessKernelEventGUID represents the Process Kernel event GUID + ProcessKernelEventGUID = windows.GUID{Data1: 0x22fb2cd6, Data2: 0x0e7b, Data3: 0x422b, Data4: [8]byte{0xa0, 0xc7, 0x2f, 0xad, 0x1f, 0xd0, 0xe7, 0x16}} ) var ( @@ -76,6 +78,13 @@ var ( ProcessRundown = pack(ProcessEventGUID, 3) // OpenProcess identifies the kernel events that are triggered when the process handle is acquired OpenProcess = pack(AuditAPIEventGUID, 5) + // CreateProcessInternal identifies the process creation event emitted by the Microsoft Windows Kernel Process provider. + // The only purpose of this event is to enrich the process state with some extra attributes, and populates the snapshotter + // for events running in the Security Telemetry session that might miss process lookups because the core NT Kernel Provider + // hasn't still published the CreateProcess or ProcessRundown event + CreateProcessInternal = pack(ProcessKernelEventGUID, 1) + // ProcessRundownInternal same as above but for process rundown events originating from the Microsoft Windows Kernel Process provider. + ProcessRundownInternal = pack(ProcessKernelEventGUID, 15) // CreateThread identifies thread creation kernel events CreateThread = pack(ThreadEventGUID, 1) @@ -228,11 +237,11 @@ func NewTypeFromEventRecord(ev *etw.EventRecord) Type { // if the event type is not recognized. func (t Type) String() string { switch t { - case CreateProcess: + case CreateProcess, CreateProcessInternal: return "CreateProcess" case TerminateProcess: return "TerminateProcess" - case ProcessRundown: + case ProcessRundown, ProcessRundownInternal: return "ProcessRundown" case OpenProcess: return "OpenProcess" @@ -346,7 +355,7 @@ func (t Type) String() string { // Category determines the category to which the event type pertains. func (t Type) Category() Category { switch t { - case CreateProcess, TerminateProcess, OpenProcess, ProcessRundown: + case CreateProcess, CreateProcessInternal, TerminateProcess, OpenProcess, ProcessRundown, ProcessRundownInternal: return Process case CreateThread, TerminateThread, OpenThread, SetThreadContext, ThreadRundown, StackWalk: return Thread @@ -505,6 +514,8 @@ func (t Type) Exists() bool { func (t Type) OnlyState() bool { switch t { case ProcessRundown, + ProcessRundownInternal, + CreateProcessInternal, ThreadRundown, ImageRundown, FileRundown, @@ -585,10 +596,8 @@ func (t Type) ID() uint { // Source designates the provenance of this event type. func (t Type) Source() Source { - switch t { - case OpenProcess, OpenThread, SetThreadContext, CreateSymbolicLinkObject, - QueryDNS, ReplyDNS, SubmitThreadpoolWork, SubmitThreadpoolCallback, - SetThreadpoolTimer: + switch t.GUID() { + case AuditAPIEventGUID, DNSEventGUID, ThreadpoolGUID, ProcessKernelEventGUID: return SecurityTelemetryLogger default: return SystemLogger diff --git a/pkg/sys/etw/etw.go b/pkg/sys/etw/etw.go index 3874d7e94..32fbc1996 100644 --- a/pkg/sys/etw/etw.go +++ b/pkg/sys/etw/etw.go @@ -209,11 +209,23 @@ const ( // ControlCodeEnableProvider updates the session configuration so // that the session receives the requested events from the provider. ControlCodeEnableProvider = 1 + // ControlCodeCaptureState requests that the provider log its state + // information, such as rundown events + ControlCodeCaptureState = 2 ) // EnableTrace influences the behaviour of the specified event trace provider. -func EnableTrace(guid windows.GUID, handle TraceHandle, keyword uint64) error { - err := enableTraceEx2(handle, &guid, ControlCodeEnableProvider, TraceLevelInformation, keyword, 0, 0, nil) +func EnableTrace(guid windows.GUID, handle TraceHandle, keywords uint64) error { + err := enableTraceEx2(handle, &guid, ControlCodeEnableProvider, TraceLevelInformation, keywords, 0, 0, nil) + if err != nil { + return os.NewSyscallError("EnableTraceEx2", err) + } + return nil +} + +// CaptureProviderState requests that the provider log its state information. +func CaptureProviderState(guid windows.GUID, handle TraceHandle) error { + err := enableTraceEx2(handle, &guid, ControlCodeCaptureState, 0, 0, 0, 0, nil) if err != nil { return os.NewSyscallError("EnableTraceEx2", err) } @@ -228,7 +240,7 @@ type EnableTraceOpts struct { // EnableTraceWithOpts influences the behaviour of the specified event trace provider // by providing extra options to configure how events are writing to the session buffer. -func EnableTraceWithOpts(guid windows.GUID, handle TraceHandle, keyword uint64, opts EnableTraceOpts) error { +func EnableTraceWithOpts(guid windows.GUID, handle TraceHandle, keywords uint64, opts EnableTraceOpts) error { params := &EnableTraceParameters{ Version: EnableTraceParametersVersion, SourceID: guid, @@ -236,7 +248,7 @@ func EnableTraceWithOpts(guid windows.GUID, handle TraceHandle, keyword uint64, if opts.WithStacktrace { params.EnableProperty = EventEnablePropertyStacktrace } - err := enableTraceEx2(handle, &guid, ControlCodeEnableProvider, TraceLevelInformation, keyword, 0, 0, params) + err := enableTraceEx2(handle, &guid, ControlCodeEnableProvider, TraceLevelInformation, keywords, 0, 0, params) if err != nil { return os.NewSyscallError("EnableTraceEx2", err) } diff --git a/pkg/sys/etw/types.go b/pkg/sys/etw/types.go index ffc6c881b..3a5bea1e8 100644 --- a/pkg/sys/etw/types.go +++ b/pkg/sys/etw/types.go @@ -44,6 +44,9 @@ var DNSClientGUID = windows.GUID{Data1: 0x1c95126e, Data2: 0x7eea, Data3: 0x49a9 // ThreadpoolGUID represents the GUID for the thread pool provider var ThreadpoolGUID = windows.GUID{Data1: 0xc861d0e2, Data2: 0xa2c1, Data3: 0x4d36, Data4: [8]byte{0x9f, 0x9c, 0x97, 0x0b, 0xab, 0x94, 0x3a, 0x12}} +// WindowsKernelProcessGUID represents the GUID for the Microsoft Windows Kernel Process provider +var WindowsKernelProcessGUID = windows.GUID{Data1: 0x22fb2cd6, Data2: 0x0e7b, Data3: 0x422b, Data4: [8]byte{0xa0, 0xc7, 0x2f, 0xad, 0x1f, 0xd0, 0xe7, 0x16}} + const ( // TraceStackTracingInfo controls call stack tracing for kernel events TraceStackTracingInfo = uint8(3) @@ -51,6 +54,9 @@ const ( TraceSystemTraceEnableFlagsInfo = uint8(4) ) +// ProcessKeyword enables process events for Microsoft Windows Kernel Process provider +const ProcessKeyword = 0x10 + const ( // EventHeaderExtTypeStackTrace64 indicates that the extended data contains the call stack if the event is captured on a 64-bit host EventHeaderExtTypeStackTrace64 uint16 = 0x0006 @@ -650,7 +656,9 @@ func (e *EventRecord) ReadUTF16String(offset uint16) (string, uint16) { if offset > e.BufferLen { return "", 0 } + var length uint16 + if offset > 0 { length = e.BufferLen - offset } else { @@ -666,13 +674,47 @@ func (e *EventRecord) ReadUTF16String(offset uint16) (string, uint16) { i += 2 } } + s := (*[1<<30 - 1]uint16)(unsafe.Pointer(e.Buffer + uintptr(offset)))[:length:length] if offset > 0 { return utf16.Decode(s[:len(s)/2-1-2]), uint16(len(s) + 2) } + return utf16.Decode(s[:len(s)/2]), uint16(len(s) + 2) } +// ReadNTUnicodeString reads the native Unicode string at the given offset. +func (e *EventRecord) ReadNTUnicodeString(offset uint16) (string, uint16) { + if offset > e.BufferLen { + return "", offset + } + + i := offset + var length uint16 + for i < e.BufferLen { + c := *(*uint16)(unsafe.Pointer(e.Buffer + uintptr(i))) + if c == 0 { + break // null terminator + } + length += 2 + i += 2 + } + + if length == 0 { + return "", offset + } + + b := (*[1<<30 - 1]byte)(unsafe.Pointer(e.Buffer + uintptr(offset)))[:length:length] + + s := windows.NTUnicodeString{ + Length: uint16(len(b)), + MaximumLength: uint16(len(b)), + Buffer: (*uint16)(unsafe.Pointer(&b[0])), + } + + return s.String(), offset + s.Length +} + // ConsumeUTF16String reads the byte slice with UTF16-encoded string // when the UTF16 string is located at the end of the buffer. func (e *EventRecord) ConsumeUTF16String(offset uint16) string { @@ -684,7 +726,7 @@ func (e *EventRecord) ConsumeUTF16String(offset uint16) string { } // ReadSID reads the security identifier from the event buffer. -func (e *EventRecord) ReadSID(offset uint16) ([]byte, uint16) { +func (e *EventRecord) ReadSID(offset uint16, isWbemSid bool) ([]byte, uint16) { // this is a Security Token which can be null and takes 4 bytes. // Otherwise, it is an 8 byte structure (TOKEN_USER) followed by SID, // which is variable size depending on the 2nd byte in the SID @@ -692,7 +734,11 @@ func (e *EventRecord) ReadSID(offset uint16) ([]byte, uint16) { if sid == 0 { return nil, offset + 4 } - const tokenSize uint16 = 16 + + var tokenSize uint16 + if isWbemSid { + tokenSize = 16 // TOKEN_USER size + } authorities := e.ReadByte(offset + (tokenSize + 1)) end := offset + tokenSize + 8 + 4*uint16(authorities) diff --git a/pkg/sys/etw/types_test.go b/pkg/sys/etw/types_test.go index da44ba3dd..f82c7c7ef 100644 --- a/pkg/sys/etw/types_test.go +++ b/pkg/sys/etw/types_test.go @@ -75,7 +75,7 @@ func TestReadBuffer(t *testing.T) { assert.Equal(t, uint32(12520), ev.ReadUint32(8)) assert.Equal(t, uint32(5240), ev.ReadUint32(12)) - rawSid, offset := ev.ReadSID(36) + rawSid, offset := ev.ReadSID(36, true) b := uintptr(unsafe.Pointer(&rawSid[0])) b += uintptr(8 * 2) From 2503dcca28c5ac1125d80677d81ce0fa0b011f91 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Fri, 20 Jun 2025 18:05:15 +0200 Subject: [PATCH 17/42] feat(process): Enrich processes with integrity level and token elevation info --- pkg/alertsender/alert_test.go | 30 +++--- pkg/event/enum.go | 13 +++ pkg/event/params/params_windows.go | 6 ++ pkg/ps/snapshotter_windows.go | 142 ++++++++++++++++++++++++----- pkg/ps/snapshotter_windows_test.go | 126 +++++++++++++++++++++++++ pkg/ps/types/types_windows.go | 37 +++++--- pkg/sys/process.go | 20 ++++ pkg/sys/rid.go | 81 ++++++++++++++++ 8 files changed, 406 insertions(+), 49 deletions(-) create mode 100644 pkg/sys/rid.go diff --git a/pkg/alertsender/alert_test.go b/pkg/alertsender/alert_test.go index 697596269..e429d1f3e 100644 --- a/pkg/alertsender/alert_test.go +++ b/pkg/alertsender/alert_test.go @@ -52,16 +52,17 @@ func TestAlertString(t *testing.T) { Name: "CreateProcess", PID: 1023, PS: &pstypes.PS{ - Name: "svchost.exe", - Cmdline: "C:\\Windows\\System32\\svchost.exe", - Ppid: 345, - Username: "SYSTEM", - Domain: "NT AUTHORITY", - SID: "S-1-5-18", + Name: "svchost.exe", + Cmdline: "C:\\Windows\\System32\\svchost.exe", + Ppid: 345, + Username: "SYSTEM", + Domain: "NT AUTHORITY", + SID: "S-1-5-18", + TokenIntegrityLevel: "HIGH", }, }}), true, - "Credential discovery via VaultCmd.exe\n\nSuspicious vault enumeration via VaultCmd tool\n\nSeverity: low\n\nSystem event involved in this alert:\n\n\tEvent #1:\n\n\t\tSeq: 0\n\t\tPid: 1023\n\t\tTid: 0\n\t\tName: CreateProcess\n\t\tCategory: process\n\t\tHost: \n\t\tTimestamp: 0001-01-01 00:00:00 +0000 UTC\n\t\tParameters: cmdlineâžœ C:\\Windows\\system32\\svchost-fake.exe -k RPCSS, nameâžœ svchost-fake.exe\n \n\t\tPid: 0\n\t\tPpid: 345\n\t\tName: svchost.exe\n\t\tCmdline: C:\\Windows\\System32\\svchost.exe\n\t\tExe: \n\t\tCwd: \n\t\tSID: S-1-5-18\n\t\tUsername: SYSTEM\n\t\tDomain: NT AUTHORITY\n\t\tArgs: []\n\t\tSession ID: 0\n\t\tAncestors: \n\t\n", + "Credential discovery via VaultCmd.exe\n\nSuspicious vault enumeration via VaultCmd tool\n\nSeverity: low\n\nSystem event involved in this alert:\n\n\tEvent #1:\n\n\t\tSeq: 0\n\t\tPid: 1023\n\t\tTid: 0\n\t\tName: CreateProcess\n\t\tCategory: process\n\t\tHost: \n\t\tTimestamp: 0001-01-01 00:00:00 +0000 UTC\n\t\tParameters: cmdlineâžœ C:\\Windows\\system32\\svchost-fake.exe -k RPCSS, nameâžœ svchost-fake.exe\n \n\t\tPid: 0\n\t\tPpid: 345\n\t\tName: svchost.exe\n\t\tCmdline: C:\\Windows\\System32\\svchost.exe\n\t\tExe: \n\t\tCwd: \n\t\tSID: S-1-5-18\n\t\tIntegrity level: HIGH\n\t\tUsername: SYSTEM\n\t\tDomain: NT AUTHORITY\n\t\tArgs: []\n\t\tSession ID: 0\n\t\tAncestors: \n\t\n", }, { NewAlertWithEvents("Credential discovery via VaultCmd.exe", "", nil, Normal, []*event.Event{{ @@ -73,16 +74,17 @@ func TestAlertString(t *testing.T) { Name: "CreateProcess", PID: 1023, PS: &pstypes.PS{ - Name: "svchost.exe", - Cmdline: "C:\\Windows\\System32\\svchost.exe", - Ppid: 345, - Username: "SYSTEM", - Domain: "NT AUTHORITY", - SID: "S-1-5-18", + Name: "svchost.exe", + Cmdline: "C:\\Windows\\System32\\svchost.exe", + Ppid: 345, + Username: "SYSTEM", + Domain: "NT AUTHORITY", + SID: "S-1-5-18", + TokenIntegrityLevel: "HIGH", }, }}), true, - "Credential discovery via VaultCmd.exe\n\nSeverity: low\n\nSystem event involved in this alert:\n\n\tEvent #1:\n\n\t\tSeq: 0\n\t\tPid: 1023\n\t\tTid: 0\n\t\tName: CreateProcess\n\t\tCategory: process\n\t\tHost: \n\t\tTimestamp: 0001-01-01 00:00:00 +0000 UTC\n\t\tParameters: cmdlineâžœ C:\\Windows\\system32\\svchost-fake.exe -k RPCSS, nameâžœ svchost-fake.exe\n \n\t\tPid: 0\n\t\tPpid: 345\n\t\tName: svchost.exe\n\t\tCmdline: C:\\Windows\\System32\\svchost.exe\n\t\tExe: \n\t\tCwd: \n\t\tSID: S-1-5-18\n\t\tUsername: SYSTEM\n\t\tDomain: NT AUTHORITY\n\t\tArgs: []\n\t\tSession ID: 0\n\t\tAncestors: \n\t\n", + "Credential discovery via VaultCmd.exe\n\nSeverity: low\n\nSystem event involved in this alert:\n\n\tEvent #1:\n\n\t\tSeq: 0\n\t\tPid: 1023\n\t\tTid: 0\n\t\tName: CreateProcess\n\t\tCategory: process\n\t\tHost: \n\t\tTimestamp: 0001-01-01 00:00:00 +0000 UTC\n\t\tParameters: cmdlineâžœ C:\\Windows\\system32\\svchost-fake.exe -k RPCSS, nameâžœ svchost-fake.exe\n \n\t\tPid: 0\n\t\tPpid: 345\n\t\tName: svchost.exe\n\t\tCmdline: C:\\Windows\\System32\\svchost.exe\n\t\tExe: \n\t\tCwd: \n\t\tSID: S-1-5-18\n\t\tIntegrity level: HIGH\n\t\tUsername: SYSTEM\n\t\tDomain: NT AUTHORITY\n\t\tArgs: []\n\t\tSession ID: 0\n\t\tAncestors: \n\t\n", }, } diff --git a/pkg/event/enum.go b/pkg/event/enum.go index 6d2f71c52..e4cd47c44 100644 --- a/pkg/event/enum.go +++ b/pkg/event/enum.go @@ -114,3 +114,16 @@ var DNSResponseCodes = ParamEnum{ uint32(windows.ERROR_INVALID_PARAMETER): "INVALID", uint32(windows.DNS_INFO_NO_RECORDS): "NXDOMAIN", } + +const ( + TokenElevationTypeDefault uint32 = iota + 1 + TokenElevationTypeFull + TokenElevationTypeLimited +) + +// PsTokenElevationTypes describes process token elevation types +var PsTokenElevationTypes = ParamEnum{ + TokenElevationTypeDefault: "DEFAULT", + TokenElevationTypeFull: "FULL", + TokenElevationTypeLimited: "LIMITED", +} diff --git a/pkg/event/params/params_windows.go b/pkg/event/params/params_windows.go index afed096c0..8be05a3bf 100644 --- a/pkg/event/params/params_windows.go +++ b/pkg/event/params/params_windows.go @@ -58,6 +58,12 @@ const ( ExitStatus = "exit_status" // StartTime field denotes the process start time. StartTime = "start_time" + // ProcessIntegrityLevel field denotes the process integrity level. + ProcessIntegrityLevel = "integrity_level" + // ProcessTokenElevationType field designates the process token elevation type. + ProcessTokenElevationType = "token_elevation_type" + // ProcessTokenIsElevated field designates if the process token is elevated. + ProcessTokenIsElevated = "token_is_elevated" // DesiredAccess field denotes the access rights for different kernel objects such as processes or threads. DesiredAccess = "desired_access" diff --git a/pkg/ps/snapshotter_windows.go b/pkg/ps/snapshotter_windows.go index 0080d8b17..4749cc649 100644 --- a/pkg/ps/snapshotter_windows.go +++ b/pkg/ps/snapshotter_windows.go @@ -53,6 +53,7 @@ var ( moduleCount = expvar.NewInt("process.module.count") mmapCount = expvar.NewInt("process.mmap.count") pebReadErrors = expvar.NewInt("process.peb.read.errors") + processEnrichments = expvar.NewInt("process.enrichments") ) type snapshotter struct { @@ -146,6 +147,7 @@ func (s *snapshotter) Write(e *event.Event) error { s.mu.Lock() defer s.mu.Unlock() processCount.Add(1) + pid, err := e.Params.GetPid() if err != nil { return err @@ -154,8 +156,45 @@ func (s *snapshotter) Write(e *event.Event) error { if err != nil { return err } + proc, err := s.newProcState(pid, ppid, e) - s.procs[pid] = proc + if ps := s.procs[pid]; ps == nil && (e.IsCreateProcessInternal() || e.IsProcessRundownInternal()) { + // only modify the state if there is no process derived from the NT kernel logger process events + s.procs[pid] = proc + } else if ps, ok := s.procs[pid]; ok && (e.IsCreateProcessInternal() || e.IsProcessRundownInternal()) { + // process state derived from the core kernel events exists - enrich it + ps.TokenIntegrityLevel = proc.TokenIntegrityLevel + ps.TokenElevationType = proc.TokenElevationType + ps.IsTokenElevated = proc.IsTokenElevated + if len(proc.Exe) > len(ps.Exe) { + // prefer full executable path + ps.Exe = proc.Exe + } + s.procs[pid] = ps + } else if ps, ok := s.procs[pid]; ok && (e.IsCreateProcess() || e.IsProcessRundown()) && ps.TokenIntegrityLevel != "" { + // enrich the existing process state with the newly arrived NT kernel logger process events + // but obtain the integrity level and executable path from the previous proc state + processEnrichments.Add(1) + proc.TokenIntegrityLevel = ps.TokenIntegrityLevel + proc.TokenElevationType = ps.TokenElevationType + proc.IsTokenElevated = ps.IsTokenElevated + + if len(ps.Exe) > len(proc.Exe) { + // prefer full executable path + proc.Exe = ps.Exe + e.AppendParam(params.Exe, params.Path, ps.Exe) + } + + e.AppendParam(params.ProcessIntegrityLevel, params.AnsiString, ps.TokenIntegrityLevel) + e.AppendParam(params.ProcessTokenElevationType, params.AnsiString, ps.TokenElevationType) + e.AppendParam(params.ProcessTokenIsElevated, params.Bool, ps.IsTokenElevated) + + s.procs[pid] = proc + } else { + // in all other cases append the process state + s.procs[pid] = proc + } + // adjust the process which is generating // the event. For `CreateProcess` events // the process context is scoped to the @@ -165,9 +204,10 @@ func (s *snapshotter) Write(e *event.Event) error { // snapshot state if e.IsProcessRundown() { e.PS = proc - } else { + } else if !e.IsProcessRundownInternal() && !e.IsCreateProcessInternal() { e.PS = s.procs[e.PID] } + return err } @@ -317,6 +357,23 @@ func (s *snapshotter) Close() error { } func (s *snapshotter) newProcState(pid, ppid uint32, e *event.Event) (*pstypes.PS, error) { + if e.IsCreateProcessInternal() || e.IsProcessRundownInternal() { + proc := &pstypes.PS{ + PID: pid, + Ppid: ppid, + Exe: e.GetParamAsString(params.Exe), + TokenIntegrityLevel: e.GetParamAsString(params.ProcessIntegrityLevel), + TokenElevationType: e.GetParamAsString(params.ProcessTokenElevationType), + IsTokenElevated: e.Params.TryGetBool(params.ProcessTokenIsElevated), + Threads: make(map[uint32]pstypes.Thread), + Modules: make([]pstypes.Module, 0), + Handles: make([]htypes.Handle, 0), + Mmaps: make([]pstypes.Mmap, 0), + } + + return proc, nil + } + proc := pstypes.New( pid, ppid, @@ -369,12 +426,38 @@ func (s *snapshotter) newProcState(pid, ppid uint32, e *event.Event) (*pstypes.P access := uint32(windows.PROCESS_QUERY_INFORMATION | windows.PROCESS_VM_READ) process, err := windows.OpenProcess(access, false, pid) if err != nil { - return proc, nil + process, err = windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION, false, pid) + if err != nil { + return proc, nil + } } //nolint:errcheck defer windows.CloseHandle(process) - // read PEB + // query token attributes if not enriched by internal event + if s.procs[pid] == nil { + var token windows.Token + err = windows.OpenProcessToken(process, windows.TOKEN_QUERY, &token) + if err != nil { + goto readPEB + } + defer token.Close() + + // get process token integrity level + tokenMandatoryLabel, err := sys.GetProcessTokenInformation[windows.Tokenmandatorylabel](token, windows.TokenIntegrityLevel) + if err != nil { + goto readPEB + } + + proc.TokenIntegrityLevel = sys.RidToString(tokenMandatoryLabel.Label.Sid) + proc.IsTokenElevated = token.IsElevated() + + e.AppendParam(params.ProcessIntegrityLevel, params.AnsiString, proc.TokenIntegrityLevel) + e.AppendParam(params.ProcessTokenIsElevated, params.Bool, proc.IsTokenElevated) + } + +readPEB: + // read PEB (Process Environment Block) peb, err := ReadPEB(process) if err != nil { pebReadErrors.Add(1) @@ -525,27 +608,52 @@ func (s *snapshotter) Find(pid uint32) (bool, *pstypes.PS) { } proc.StartTime = time.Unix(0, ct.Nanoseconds()) + // get process creation attributes + var isWOW64 bool + if err := windows.IsWow64Process(process, &isWOW64); err == nil && isWOW64 { + proc.IsWOW64 = true + } + if isPackaged, err := sys.IsProcessPackaged(process); err == nil && isPackaged { + proc.IsPackaged = true + } + if prot, err := sys.QueryInformationProcess[sys.PsProtection](process, sys.ProcessProtectionInformation); err == nil && prot != nil { + proc.IsProtected = prot.IsProtected() + } + // get process token attributes var token windows.Token + var tokenUser *windows.Tokenuser + var tokenMandatoryLabel *windows.Tokenmandatorylabel + err = windows.OpenProcessToken(process, windows.TOKEN_QUERY, &token) if err != nil { - return false, proc + goto readPEB } defer token.Close() - usr, err := token.GetTokenUser() + tokenUser, err = token.GetTokenUser() if err != nil { - return false, proc + goto readPEB + } + proc.SID = tokenUser.User.Sid.String() + proc.Username, proc.Domain, _, _ = tokenUser.User.Sid.LookupAccount("") + + // get process token integrity level + tokenMandatoryLabel, err = sys.GetProcessTokenInformation[windows.Tokenmandatorylabel](token, windows.TokenIntegrityLevel) + if err != nil { + goto readPEB } - proc.SID = usr.User.Sid.String() - proc.Username, proc.Domain, _, _ = usr.User.Sid.LookupAccount("") + + proc.TokenIntegrityLevel = sys.RidToString(tokenMandatoryLabel.Label.Sid) + proc.IsTokenElevated = token.IsElevated() // retrieve process handles proc.Handles, err = s.hsnap.FindHandles(pid) if err != nil { - return false, proc + goto readPEB } - // read PEB +readPEB: + // read PEB (Process Environment Block) peb, err := ReadPEB(process) if err != nil { pebReadErrors.Add(1) @@ -556,18 +664,6 @@ func (s *snapshotter) Find(pid uint32) (bool, *pstypes.PS) { proc.SessionID = peb.GetSessionID() proc.Cwd = peb.GetCurrentWorkingDirectory() - // get process creation attributes - var isWOW64 bool - if err := windows.IsWow64Process(process, &isWOW64); err == nil && isWOW64 { - proc.IsWOW64 = true - } - if isPackaged, err := sys.IsProcessPackaged(process); err == nil && isPackaged { - proc.IsPackaged = true - } - if prot, err := sys.QueryInformationProcess[sys.PsProtection](process, sys.ProcessProtectionInformation); err == nil && prot != nil { - proc.IsProtected = prot.IsProtected() - } - return false, proc } diff --git a/pkg/ps/snapshotter_windows_test.go b/pkg/ps/snapshotter_windows_test.go index 3ec17f499..0a767fd7e 100644 --- a/pkg/ps/snapshotter_windows_test.go +++ b/pkg/ps/snapshotter_windows_test.go @@ -185,6 +185,131 @@ func TestWrite(t *testing.T) { } } +func TestWriteInternalEventsEnrichment(t *testing.T) { + hsnap := new(handle.SnapshotterMock) + hsnap.On("FindHandles", mock.Anything).Return([]htypes.Handle{}, nil) + + var tests = []struct { + name string + evts []*event.Event + psnap Snapshotter + assertions func(t *testing.T, psnap Snapshotter) + }{ + {"write internal event without previous state", + []*event.Event{ + { + Type: event.CreateProcessInternal, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1024)}, + params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(444)}, + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: `C:\Windows\System32\svchost.exe`}, + params.ProcessIntegrityLevel: {Name: params.ProcessIntegrityLevel, Type: params.AnsiString, Value: "HIGH"}, + params.ProcessTokenIsElevated: {Name: params.ProcessTokenIsElevated, Type: params.Bool, Value: true}, + params.ProcessTokenElevationType: {Name: params.ProcessTokenElevationType, Type: params.AnsiString, Value: "FULL"}, + }, + }, + }, + NewSnapshotter(hsnap, &config.Config{}), + func(t *testing.T, psnap Snapshotter) { + ok, proc := psnap.Find(1024) + assert.True(t, ok) + assert.Equal(t, "HIGH", proc.TokenIntegrityLevel) + assert.Equal(t, "FULL", proc.TokenElevationType) + assert.Equal(t, true, proc.IsTokenElevated) + assert.Equal(t, `C:\Windows\System32\svchost.exe`, proc.Exe) + }, + }, + {"enrich existing system provider proc state with internal event", + []*event.Event{ + { + Type: event.CreateProcess, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1024)}, + params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(444)}, + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: `svchost.exe`}, + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: `svchost.exe -k LocalSystemNetworkRestricted -p -s NcbService`}, + params.UserSID: {Name: params.UserSID, Type: params.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, + params.SessionID: {Name: params.SessionID, Type: params.Uint32, Value: uint32(1)}, + params.ProcessFlags: {Name: params.ProcessFlags, Type: params.Flags, Value: uint32(0x00000010)}, + }, + }, + { + Type: event.CreateProcessInternal, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1024)}, + params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(444)}, + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: `C:\Windows\System32\svchost.exe`}, + params.ProcessIntegrityLevel: {Name: params.ProcessIntegrityLevel, Type: params.AnsiString, Value: "HIGH"}, + params.ProcessTokenIsElevated: {Name: params.ProcessTokenIsElevated, Type: params.Bool, Value: true}, + params.ProcessTokenElevationType: {Name: params.ProcessTokenElevationType, Type: params.AnsiString, Value: "FULL"}, + }, + }, + }, + NewSnapshotter(hsnap, &config.Config{}), + func(t *testing.T, psnap Snapshotter) { + ok, proc := psnap.Find(1024) + assert.True(t, ok) + assert.Equal(t, "HIGH", proc.TokenIntegrityLevel) + assert.Equal(t, "FULL", proc.TokenElevationType) + assert.Equal(t, true, proc.IsTokenElevated) + assert.Equal(t, `C:\Windows\System32\svchost.exe`, proc.Exe) + assert.Equal(t, "svchost.exe -k LocalSystemNetworkRestricted -p -s NcbService", proc.Cmdline) + assert.Equal(t, uint32(1), proc.SessionID) + }, + }, + {"enrich newly arrived system provider proc with previous internal event state", + []*event.Event{ + { + Type: event.CreateProcessInternal, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1024)}, + params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(444)}, + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: `C:\Windows\System32\svchost.exe`}, + params.ProcessIntegrityLevel: {Name: params.ProcessIntegrityLevel, Type: params.AnsiString, Value: "HIGH"}, + params.ProcessTokenIsElevated: {Name: params.ProcessTokenIsElevated, Type: params.Bool, Value: true}, + params.ProcessTokenElevationType: {Name: params.ProcessTokenElevationType, Type: params.AnsiString, Value: "FULL"}, + }, + }, + { + Type: event.CreateProcess, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1024)}, + params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(444)}, + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: `svchost.exe`}, + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: `svchost.exe -k LocalSystemNetworkRestricted -p -s NcbService`}, + params.UserSID: {Name: params.UserSID, Type: params.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, + params.SessionID: {Name: params.SessionID, Type: params.Uint32, Value: uint32(1)}, + params.ProcessFlags: {Name: params.ProcessFlags, Type: params.Flags, Value: uint32(0x00000010)}, + }, + }, + }, + NewSnapshotter(hsnap, &config.Config{}), + func(t *testing.T, psnap Snapshotter) { + ok, proc := psnap.Find(1024) + assert.True(t, ok) + assert.Equal(t, "HIGH", proc.TokenIntegrityLevel) + assert.Equal(t, "FULL", proc.TokenElevationType) + assert.Equal(t, true, proc.IsTokenElevated) + assert.Equal(t, `C:\Windows\System32\svchost.exe`, proc.Exe) + assert.Equal(t, "svchost.exe -k LocalSystemNetworkRestricted -p -s NcbService", proc.Cmdline) + assert.Equal(t, uint32(1), proc.SessionID) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for _, evt := range tt.evts { + require.NoError(t, tt.psnap.Write(evt)) + } + if tt.assertions != nil { + tt.assertions(t, tt.psnap) + } + defer tt.psnap.Close() + }) + } +} + func TestRemove(t *testing.T) { hsnap := new(handle.SnapshotterMock) hsnap.On("FindHandles", mock.Anything).Return([]htypes.Handle{}, nil) @@ -615,6 +740,7 @@ func TestFindQueryOS(t *testing.T) { assert.True(t, len(proc.Envs) > 0) assert.Contains(t, proc.Cwd, "fibratus\\pkg\\ps") assert.Equal(t, uint32(1), proc.SessionID) + assert.Equal(t, "HIGH", proc.TokenIntegrityLevel) wts, err := sys.LookupActiveWTS() require.NoError(t, err) diff --git a/pkg/ps/types/types_windows.go b/pkg/ps/types/types_windows.go index bc372b312..3d22d484f 100644 --- a/pkg/ps/types/types_windows.go +++ b/pkg/ps/types/types_windows.go @@ -91,6 +91,13 @@ type PS struct { // IsProtected denotes a protected process. The system restricts access to protected // processes and the threads of protected processes. IsProtected bool `json:"is_protected"` + // TokenIntegrityLevel designates the process token integrity level. (e.g. High) + // Integrity level defines the trust between the process and a securable object. + TokenIntegrityLevel string `json:"token_integrity_level"` + // TokenElevationType designates the process token elevation type. (e.g. Limited) + TokenElevationType string `json:"token_elevation_type"` + // IsTokenElevated indicates if the process token is elevated. + IsTokenElevated bool `json:"is_token_elevated"` } // UUID is meant to offer a more robust version of process ID that @@ -147,9 +154,10 @@ func (ps *PS) String() string { Parent name: %s Cmdline: %s Parent cmdline: %s - Exe: %s - Cwd: %s - SID: %s + Exe: %s + Cwd: %s + SID: %s + Integrity level: %s Username: %s Domain: %s Args: %s @@ -165,6 +173,7 @@ func (ps *PS) String() string { ps.Exe, ps.Cwd, ps.SID, + ps.TokenIntegrityLevel, ps.Username, ps.Domain, ps.Args, @@ -177,9 +186,10 @@ func (ps *PS) String() string { Ppid: %d Name: %s Cmdline: %s - Exe: %s - Cwd: %s - SID: %s + Exe: %s + Cwd: %s + SID: %s + Integrity level: %s Username: %s Domain: %s Args: %s @@ -193,6 +203,7 @@ func (ps *PS) String() string { ps.Exe, ps.Cwd, ps.SID, + ps.TokenIntegrityLevel, ps.Username, ps.Domain, ps.Args, @@ -213,9 +224,9 @@ func (ps *PS) StringShort() string { Parent name: %s Cmdline: %s Parent cmdline: %s - Exe: %s - Cwd: %s - SID: %s + Exe: %s + Cwd: %s + SID: %s Username: %s Domain: %s Args: %s @@ -243,9 +254,10 @@ func (ps *PS) StringShort() string { Ppid: %d Name: %s Cmdline: %s - Exe: %s - Cwd: %s - SID: %s + Exe: %s + Cwd: %s + SID: %s + Integrity level: %s Username: %s Domain: %s Args: %s @@ -259,6 +271,7 @@ func (ps *PS) StringShort() string { ps.Exe, ps.Cwd, ps.SID, + ps.TokenIntegrityLevel, ps.Username, ps.Domain, ps.Args, diff --git a/pkg/sys/process.go b/pkg/sys/process.go index b4db01b09..04ad9b853 100644 --- a/pkg/sys/process.go +++ b/pkg/sys/process.go @@ -173,3 +173,23 @@ func getModuleFileName(proc, mod windows.Handle) (string, error) { } return windows.UTF16ToString(buf), nil } + +// GetProcessTokenInformation returns the specified process token information. +func GetProcessTokenInformation[C any](token windows.Token, class uint32) (*C, error) { + var n uint32 + // get the buffer size to accommodate the desired token class structure + if err := windows.GetTokenInformation(token, class, nil, 0, &n); err != nil { + if err != windows.ERROR_INSUFFICIENT_BUFFER { + return nil, err + } + } + + // allocate the buffer and obtain token information + b := make([]byte, n) + err := windows.GetTokenInformation(token, class, &b[0], n, &n) + if err != nil { + return nil, err + } + + return (*C)(unsafe.Pointer(&b[0])), nil +} diff --git a/pkg/sys/rid.go b/pkg/sys/rid.go new file mode 100644 index 000000000..1c60763de --- /dev/null +++ b/pkg/sys/rid.go @@ -0,0 +1,81 @@ +/* + * Copyright 2021-present by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sys + +import ( + "golang.org/x/sys/windows" +) + +const ( + // UntrustedRid designates the integrity level of anonymous + // logged on processes. Write access is mostly blocked. + UntrustedRid = 0x00000000 + + // LowRid designates low process token integrity. Used for + // AppContainers, browsers that access the Internet and + // prevent most write access to objects on the system. + LowRid = 0x00001000 + + // MediumRid designates the token integrity default for most + // processes. For authenticated users. + MediumRid = 0x00002000 + + // MediumPlusRid is often observed in AppContainer or UWP processes, + // especially when they require elevated trust compared to a typical + // Medium-level process but still shouldn't run with full administrative + // privileges. + MediumPlusRid = MediumRid | 0x100 + + // HighRid is the integrity level for Administrator-level processes. + // (Elevated) process with UAC. + HighRid = 0x00003000 + + // SystemRid is the integrity level reserved for system services/processes. + SystemRid = 0x00004000 + + // ProtectedProcessRid is not seen to be used by default. Windows Internals + // book says it can be set by a kernel-mode caller. + ProtectedProcessRid = 0x00005000 +) + +// RidToString given the SID representing the token mandatory label +// returns the string representation of the integrity level. +func RidToString(sid *windows.SID) string { + if sid == nil { + return "UNKNOWN" + } + switch sid.SubAuthority(uint32(sid.SubAuthorityCount() - 1)) { + case UntrustedRid: + return "UNTRUSTED" + case LowRid: + return "LOW" + case MediumRid: + return "MEDIUM" + case MediumPlusRid: + return "MEDIUM+" + case HighRid: + return "HIGH" + case SystemRid: + return "SYSTEM" + case ProtectedProcessRid: + return "PROTECTED" + default: + return "UNKNOWN" + } +} From cebc578315c9432b4644a2ee1043ce8720cee04f Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Mon, 23 Jun 2025 21:16:19 +0200 Subject: [PATCH 18/42] chore(process): Populate process modules from internal events If the event arrives from the provider and the process state hasn't been populated with the expected module, we leverage the internal load image event to append the module. --- internal/etw/processors/image_windows.go | 5 +++++ internal/etw/source.go | 2 +- pkg/event/event_windows.go | 1 + pkg/event/metainfo_windows.go | 1 + pkg/event/param_windows.go | 23 +++++++++++++++++++++++ pkg/event/types_windows.go | 7 +++++-- pkg/ps/snapshotter_windows.go | 9 ++++++++- pkg/sys/etw/types.go | 3 +++ 8 files changed, 47 insertions(+), 4 deletions(-) diff --git a/internal/etw/processors/image_windows.go b/internal/etw/processors/image_windows.go index c0d262a87..9ab7d2b2f 100644 --- a/internal/etw/processors/image_windows.go +++ b/internal/etw/processors/image_windows.go @@ -55,6 +55,11 @@ func newImageProcessor(psnap ps.Snapshotter) Processor { func (*imageProcessor) Name() ProcessorType { return Image } func (m *imageProcessor) ProcessEvent(e *event.Event) (*event.Event, bool, error) { + if e.IsLoadImageInternal() { + // state management + return e, false, m.psnap.AddModule(e) + } + if e.IsLoadImage() { // is image characteristics data cached? path := e.GetParamAsString(params.ImagePath) diff --git a/internal/etw/source.go b/internal/etw/source.go index a8b9d299c..888d72ee6 100644 --- a/internal/etw/source.go +++ b/internal/etw/source.go @@ -168,7 +168,7 @@ func (e *EventSource) Open(config *config.Config) error { // additional attributes and guaranteeing that any event published by // the security telemetry session doesn't miss its respective process // from the snapshotter - trace.AddProvider(etw.WindowsKernelProcessGUID, false, WithKeywords(etw.ProcessKeyword), WithCaptureState()) + trace.AddProvider(etw.WindowsKernelProcessGUID, false, WithKeywords(etw.ProcessKeyword|etw.ImageKeyword), WithCaptureState()) if config.EventSource.EnableDNSEvents { trace.AddProvider(etw.DNSClientGUID, false) diff --git a/pkg/event/event_windows.go b/pkg/event/event_windows.go index 7bc832d46..0782a9740 100644 --- a/pkg/event/event_windows.go +++ b/pkg/event/event_windows.go @@ -229,6 +229,7 @@ func (e *Event) IsTerminateProcess() bool { return e.Type == TerminateProc func (e *Event) IsTerminateThread() bool { return e.Type == TerminateThread } func (e *Event) IsUnloadImage() bool { return e.Type == UnloadImage } func (e *Event) IsLoadImage() bool { return e.Type == LoadImage } +func (e *Event) IsLoadImageInternal() bool { return e.Type == LoadImageInternal } func (e *Event) IsImageRundown() bool { return e.Type == ImageRundown } func (e *Event) IsFileOpEnd() bool { return e.Type == FileOpEnd } func (e *Event) IsRegSetValue() bool { return e.Type == RegSetValue } diff --git a/pkg/event/metainfo_windows.go b/pkg/event/metainfo_windows.go index 4560698e4..7794315f0 100644 --- a/pkg/event/metainfo_windows.go +++ b/pkg/event/metainfo_windows.go @@ -240,6 +240,7 @@ func AllWithState() []Type { s = append(s, StackWalk) s = append(s, CreateProcessInternal) s = append(s, ProcessRundownInternal) + s = append(s, LoadImageInternal) return s } diff --git a/pkg/event/param_windows.go b/pkg/event/param_windows.go index a8922d6a2..870dd7ef9 100644 --- a/pkg/event/param_windows.go +++ b/pkg/event/param_windows.go @@ -459,6 +459,29 @@ func (e *Event) produceParams(evt *etw.EventRecord) { e.AppendParam(params.ImagePath, params.DOSPath, filename) e.AppendParam(params.ImageSignatureLevel, params.Enum, uint32(sigLevel), WithEnum(signature.Levels)) e.AppendParam(params.ImageSignatureType, params.Enum, uint32(sigType), WithEnum(signature.Types)) + case LoadImageInternal: + var ( + pid uint32 + checksum uint32 + defaultBase uint64 + imageBase uint64 + imageSize uint64 + filename string + ) + + imageBase = evt.ReadUint64(0) + imageSize = evt.ReadUint64(8) + pid = evt.ReadUint32(16) + checksum = evt.ReadUint32(20) + defaultBase = evt.ReadUint64(28) // skip timestamp (4 bytes) + filename = evt.ConsumeUTF16String(36) + + e.AppendParam(params.ProcessID, params.PID, pid) + e.AppendParam(params.ImageCheckSum, params.Uint32, checksum) + e.AppendParam(params.ImageDefaultBase, params.Address, defaultBase) + e.AppendParam(params.ImageBase, params.Address, imageBase) + e.AppendParam(params.ImageSize, params.Uint64, imageSize) + e.AppendParam(params.ImagePath, params.DOSPath, filename) case RegOpenKey, RegCloseKey, RegCreateKCB, RegDeleteKCB, RegKCBRundown, RegCreateKey, diff --git a/pkg/event/types_windows.go b/pkg/event/types_windows.go index 3c2ee743f..52f7bb04b 100644 --- a/pkg/event/types_windows.go +++ b/pkg/event/types_windows.go @@ -156,6 +156,8 @@ var ( ImageRundown = pack(ImageEventGUID, 3) // LoadImage represents load image kernel events that are triggered when a DLL or executable file is loaded LoadImage = pack(ImageEventGUID, 10) + // LoadImageInternal same as for process internal event originating from the Microsoft Windows Kernel Process provider. + LoadImageInternal = pack(ProcessKernelEventGUID, 5) // AcceptTCPv4 represents the TCPv4 kernel events for accepting connection requests from the socket queue. AcceptTCPv4 = pack(NetworkTCPEventGUID, 15) @@ -309,7 +311,7 @@ func (t Type) String() string { return "RegCreateKCB" case RegSetValue: return "RegSetValue" - case LoadImage: + case LoadImage, LoadImageInternal: return "LoadImage" case UnloadImage: return "UnloadImage" @@ -359,7 +361,7 @@ func (t Type) Category() Category { return Process case CreateThread, TerminateThread, OpenThread, SetThreadContext, ThreadRundown, StackWalk: return Thread - case LoadImage, UnloadImage, ImageRundown: + case LoadImage, UnloadImage, ImageRundown, LoadImageInternal: return Image case CreateFile, ReadFile, WriteFile, EnumDirectory, DeleteFile, RenameFile, CloseFile, SetFileInformation, FileRundown, FileOpEnd, ReleaseFile, MapViewFile, UnmapViewFile, MapFileRundown: @@ -518,6 +520,7 @@ func (t Type) OnlyState() bool { CreateProcessInternal, ThreadRundown, ImageRundown, + LoadImageInternal, FileRundown, RegKCBRundown, FileOpEnd, diff --git a/pkg/ps/snapshotter_windows.go b/pkg/ps/snapshotter_windows.go index 4749cc649..e2576efca 100644 --- a/pkg/ps/snapshotter_windows.go +++ b/pkg/ps/snapshotter_windows.go @@ -246,7 +246,6 @@ func (s *snapshotter) AddModule(e *event.Event) error { if err != nil { return err } - moduleCount.Add(1) s.mu.Lock() defer s.mu.Unlock() @@ -265,6 +264,14 @@ func (s *snapshotter) AddModule(e *event.Event) error { module.Name = e.GetParamAsString(params.ImagePath) module.BaseAddress = e.Params.TryGetAddress(params.ImageBase) module.DefaultBaseAddress = e.Params.TryGetAddress(params.ImageDefaultBase) + + if e.IsLoadImageInternal() { + proc.AddModule(module) + return nil + } + + moduleCount.Add(1) + module.SignatureLevel, _ = e.Params.GetUint32(params.ImageSignatureLevel) module.SignatureType, _ = e.Params.GetUint32(params.ImageSignatureType) diff --git a/pkg/sys/etw/types.go b/pkg/sys/etw/types.go index 3a5bea1e8..cd4c78ae7 100644 --- a/pkg/sys/etw/types.go +++ b/pkg/sys/etw/types.go @@ -57,6 +57,9 @@ const ( // ProcessKeyword enables process events for Microsoft Windows Kernel Process provider const ProcessKeyword = 0x10 +// ImageKeyword enables images events for Microsoft Windows Kernel Process provider +const ImageKeyword = 0x40 + const ( // EventHeaderExtTypeStackTrace64 indicates that the extended data contains the call stack if the event is captured on a 64-bit host EventHeaderExtTypeStackTrace64 uint16 = 0x0006 From 743753bc7f9a3e6d2b75b0d3aeaf6209a8cd96ba Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Sat, 7 Jun 2025 23:10:54 +0200 Subject: [PATCH 19/42] chore(filament): Sunset useless filaments --- filaments/README.md | 2 - filaments/fishy_netio.py | 72 ------------------------ filaments/teamviewer_remote_file_copy.py | 67 ---------------------- 3 files changed, 141 deletions(-) delete mode 100644 filaments/fishy_netio.py delete mode 100644 filaments/teamviewer_remote_file_copy.py diff --git a/filaments/README.md b/filaments/README.md index 294d9bd95..4345e9c7e 100644 --- a/filaments/README.md +++ b/filaments/README.md @@ -6,9 +6,7 @@ Visit the [documentation](https://www.fibratus.io/#/filaments/writing) for a wal ### Available filaments -- `fishy_netio` alerts when atypical processes produce network requests - `top_in_packets` shows the top TCP / UDP inbound packets by IP/port tuple - `top_keys` shows the top registry keys by number of registry operations - `top_out_packets` shows the top TCP / UDP outbound packets by IP/port tuple - `watch_files` watches files and directories created in the file system -- `teamviewer_remote_file_copy` identifies an executable or script file remotely downloaded via a TeamViewer transfer session diff --git a/filaments/fishy_netio.py b/filaments/fishy_netio.py deleted file mode 100644 index a15570019..000000000 --- a/filaments/fishy_netio.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright 2019-2020 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Anomalous process attempts to make a network request or it accepts an inbound connection -""" - -from utils.dotdict import dotdictify - -__pids__ = [] -__procs__ = [ - 'calc.exe', - 'notepad.exe', - 'mspaint.exe', -] - - -def on_init(): - set_filter("evt.category = 'net' and ps.name in (%s)" % (', '.join([f'\'{ps}\'' for ps in __procs__]))) - - -@dotdictify -def on_next_event(event): - notify = True if event.pid in __pids__ else False - if not notify: - emit_alert( - f'Anomalous network I/O detected to {event.params.dip}:{event.params.dport}', - text(Event), - severity='critical', - tags=['anomalous netio'] - ) - __pids__.append(event.pid) - - -def text(event): - return """ - - Source IP: %s - Source port: %s - Destination IP: %s - Destination port: %s - Protocol: %s - - Process ================================================================================== - - Name: %s - Comm: %s - Cwd: %s - User: %s - - """ % ( - event.params.sip, - event.params.sport, - event.params.dip, - event.params.dport, - event.params.dport_name, - event.exe, - event.comm, - event.cwd, event.sid) diff --git a/filaments/teamviewer_remote_file_copy.py b/filaments/teamviewer_remote_file_copy.py deleted file mode 100644 index 1850e553d..000000000 --- a/filaments/teamviewer_remote_file_copy.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright 2019-2020 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Identifies an executable or script file remotely downloaded via a TeamViewer transfer session -""" - -from utils.dotdict import dotdictify - -__author__ = 'Nedim Sabic Sabic' -__tags__ = ['Command and Control', 'TeamViewer'] -__references__ = ['https://blog.menasec.net/2019/11/hunting-for-suspicious-use-of.html'] -__severity__ = 'medium' - -__catalog__ = { - 'framework': 'MITRE ATT&CK', - 'technique_id': 'T1105', - 'technique_name': 'Ingress Tool Transfer', - 'technique_ref': 'https://attack.mitre.org/techniques/T1105/', - 'tactic_id': 'TA0011', - 'tactic_name': 'Command and Control', - 'tactic_ref': 'https://attack.mitre.org/tactics/TA0011/' -} - - -extensions = [ - '.exe', - '.dll', - '.scr', - '.com', - '.bar', - '.ps1', - '.vbs', - '.vbe', - '.js', - '.wsh', - '.hta' -] - - -def on_init(): - set_filter("evt.name = 'CreateFile' and ps.name = 'TeamViewer.exe' and file.operation = 'create' " - "and file.extension in (%s)" - % (', '.join([f'\'{ext}\'' for ext in extensions]))) - - -@dotdictify -def on_next_event(event): - emit_alert( - f'Remote File Copy via TeamViewer', - f'TeamViewer downloaded an executable or script file {event.params.file_name} via transfer session', - severity=__severity__, - tags=[__tags__] - ) From 161de80a4c7011cee034b2cb6947dcbdb8044621 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Wed, 9 Jul 2025 20:14:33 +0200 Subject: [PATCH 20/42] refactor(bitset): Introduce IsInitalized method --- pkg/event/bitset.go | 15 ++++++++++++--- pkg/filter/ql/literal_test.go | 36 +++++++++++++++++------------------ 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/pkg/event/bitset.go b/pkg/event/bitset.go index 2a87e3a17..abfb0a157 100644 --- a/pkg/event/bitset.go +++ b/pkg/event/bitset.go @@ -89,6 +89,15 @@ func (b *BitSets) IsBitSet(evt *Event) bool { (b.cats != nil && b.cats.Test(uint(evt.Category.Index()))) } -func (b *BitSets) IsBitmaskInitialized() bool { return b.bitmask != nil } -func (b *BitSets) IsTypesInitialized() bool { return b.types != nil } -func (b *BitSets) IsCategoryInitialized() bool { return b.cats != nil } +// IsInitialized checks if the given bitset type is initialized. +func (b *BitSets) IsInitialized(bs BitSetType) bool { + switch bs { + case BitmaskBitSet: + return b.bitmask != nil + case TypeBitSet: + return b.types != nil + case CategoryBitSet: + return b.cats != nil + } + return false +} diff --git a/pkg/filter/ql/literal_test.go b/pkg/filter/ql/literal_test.go index 92269d2e9..3c978cbf5 100644 --- a/pkg/filter/ql/literal_test.go +++ b/pkg/filter/ql/literal_test.go @@ -34,9 +34,9 @@ func TestSequenceExprIsEvaluable(t *testing.T) { }{ {"evt.name = 'CreateProcess'", &event.Event{Type: event.CreateProcess, Category: event.Process}, true, func(t *testing.T, sexpr *SequenceExpr) { - assert.True(t, sexpr.bitsets.IsTypesInitialized()) - assert.False(t, sexpr.bitsets.IsBitmaskInitialized()) - assert.False(t, sexpr.bitsets.IsCategoryInitialized()) + assert.True(t, sexpr.bitsets.IsInitialized(event.TypeBitSet)) + assert.False(t, sexpr.bitsets.IsInitialized(event.BitmaskBitSet)) + assert.False(t, sexpr.bitsets.IsInitialized(event.CategoryBitSet)) }, }, {"evt.name = 'CreateProcess'", &event.Event{Type: event.TerminateProcess, Category: event.Process}, false, nil}, @@ -44,37 +44,37 @@ func TestSequenceExprIsEvaluable(t *testing.T) { {"evt.name = 'CreateProcess' or evt.category = 'object'", &event.Event{Type: event.TerminateProcess, Category: event.Process}, false, nil}, {"evt.name = 'CreateProcess' or evt.name = 'OpenProcess'", &event.Event{Type: event.OpenProcess, Category: event.Process}, true, func(t *testing.T, sexpr *SequenceExpr) { - assert.True(t, sexpr.bitsets.IsTypesInitialized()) - assert.False(t, sexpr.bitsets.IsBitmaskInitialized()) - assert.False(t, sexpr.bitsets.IsCategoryInitialized()) + assert.True(t, sexpr.bitsets.IsInitialized(event.TypeBitSet)) + assert.False(t, sexpr.bitsets.IsInitialized(event.BitmaskBitSet)) + assert.False(t, sexpr.bitsets.IsInitialized(event.CategoryBitSet)) }, }, {"evt.name = 'CreateProcess' or evt.name = 'CreateThread'", &event.Event{Type: event.CreateThread, Category: event.Thread}, true, func(t *testing.T, sexpr *SequenceExpr) { - assert.False(t, sexpr.bitsets.IsTypesInitialized()) - assert.True(t, sexpr.bitsets.IsBitmaskInitialized()) - assert.False(t, sexpr.bitsets.IsCategoryInitialized()) + assert.False(t, sexpr.bitsets.IsInitialized(event.TypeBitSet)) + assert.True(t, sexpr.bitsets.IsInitialized(event.BitmaskBitSet)) + assert.False(t, sexpr.bitsets.IsInitialized(event.CategoryBitSet)) }, }, {"evt.name = 'CreateProcess' or evt.category = 'registry'", &event.Event{Type: event.RegSetValue, Category: event.Registry}, true, func(t *testing.T, sexpr *SequenceExpr) { - assert.True(t, sexpr.bitsets.IsTypesInitialized()) - assert.False(t, sexpr.bitsets.IsBitmaskInitialized()) - assert.True(t, sexpr.bitsets.IsCategoryInitialized()) + assert.True(t, sexpr.bitsets.IsInitialized(event.TypeBitSet)) + assert.False(t, sexpr.bitsets.IsInitialized(event.BitmaskBitSet)) + assert.True(t, sexpr.bitsets.IsInitialized(event.CategoryBitSet)) }, }, {"evt.name = 'CreateProcess' or evt.name = 'OpenProcess' or evt.category = 'registry'", &event.Event{Type: event.OpenProcess, Category: event.Process}, true, func(t *testing.T, sexpr *SequenceExpr) { - assert.True(t, sexpr.bitsets.IsTypesInitialized()) - assert.False(t, sexpr.bitsets.IsBitmaskInitialized()) - assert.True(t, sexpr.bitsets.IsCategoryInitialized()) + assert.True(t, sexpr.bitsets.IsInitialized(event.TypeBitSet)) + assert.False(t, sexpr.bitsets.IsInitialized(event.BitmaskBitSet)) + assert.True(t, sexpr.bitsets.IsInitialized(event.CategoryBitSet)) }, }, {"evt.name = 'CreateProcess' or evt.name = 'SetThreadContext' or evt.category = 'registry'", &event.Event{Type: event.CreateProcess, Category: event.Process}, true, func(t *testing.T, sexpr *SequenceExpr) { - assert.False(t, sexpr.bitsets.IsTypesInitialized()) - assert.True(t, sexpr.bitsets.IsBitmaskInitialized()) - assert.True(t, sexpr.bitsets.IsCategoryInitialized()) + assert.False(t, sexpr.bitsets.IsInitialized(event.TypeBitSet)) + assert.True(t, sexpr.bitsets.IsInitialized(event.BitmaskBitSet)) + assert.True(t, sexpr.bitsets.IsInitialized(event.CategoryBitSet)) }, }, } From c5c8f972f51260b7e2257d205a721de00765eccb Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Wed, 9 Jul 2025 19:47:54 +0200 Subject: [PATCH 21/42] fix(fs): Expand SystemRoot in file paths --- pkg/fs/dev.go | 44 +++++++++++++++++++++++++++++++++----------- pkg/fs/dev_test.go | 31 ++++++++++++++++++++++++------- 2 files changed, 57 insertions(+), 18 deletions(-) diff --git a/pkg/fs/dev.go b/pkg/fs/dev.go index f6aa0d85b..c55719cbf 100644 --- a/pkg/fs/dev.go +++ b/pkg/fs/dev.go @@ -23,6 +23,7 @@ package fs import ( "github.com/rabbitstack/fibratus/pkg/sys" + "os" "strings" ) @@ -36,7 +37,8 @@ type DevMapper interface { } type mapper struct { - cache map[string]string + cache map[string]string + sysroot string } // NewDevMapper creates a new instance of the DOS device replacer. @@ -44,6 +46,7 @@ func NewDevMapper() DevMapper { m := &mapper{ cache: make(map[string]string), } + // loop through logical drives and query the DOS device name for _, drive := range sys.GetLogicalDrives() { device, err := sys.QueryDosDevice(drive) @@ -52,6 +55,13 @@ func NewDevMapper() DevMapper { } m.cache[device] = drive } + + // resolve the SystemRoot environment variable + m.sysroot = os.Getenv("SystemRoot") + if m.sysroot == "" { + m.sysroot = os.Getenv("SYSTEMROOT") + } + return m } @@ -59,23 +69,35 @@ func (m *mapper) Convert(filename string) string { if filename == "" || len(filename) < deviceOffset { return filename } - i := strings.Index(filename[deviceOffset:], "\\") - if i < 0 { + + // find the backslash index + n := strings.Index(filename[deviceOffset:], "\\") + if n < 0 { if f, ok := m.cache[filename]; ok { return f } return filename } - dev := filename[:i+deviceOffset] - // convert Windows Sandbox path to native path - if dev == vmsmbDevice { - n := strings.Index(filename, "os") - if n > 0 { - return "C:" + filename[n+2:] - } - } + + dev := filename[:n+deviceOffset] if drive, ok := m.cache[dev]; ok { + // the mapping for the DOS device exists return strings.Replace(filename, dev, drive, 1) } + + switch { + case dev == vmsmbDevice: + // convert Windows Sandbox path to native path + if n := strings.Index(filename, "os"); n > 0 { + return "C:" + filename[n+2:] + } + case strings.HasPrefix(filename, "\\SystemRoot"): + // normalize paths starting with SystemRoot + return strings.Replace(filename, "\\SystemRoot", m.sysroot, 1) + case strings.HasPrefix(filename, "\\SYSTEMROOT"): + // normalize paths starting with SYSTEMROOT + return strings.Replace(filename, "\\SYSTEMROOT", m.sysroot, 1) + } + return filename } diff --git a/pkg/fs/dev_test.go b/pkg/fs/dev_test.go index 29b75aff9..c993297fe 100644 --- a/pkg/fs/dev_test.go +++ b/pkg/fs/dev_test.go @@ -57,23 +57,40 @@ var drives = []string{ "Z"} func TestConvertDosDevice(t *testing.T) { - mapper := NewDevMapper() + m := NewDevMapper() files := make([]string, 0, len(drives)) + for _, drive := range drives { files = append(files, fmt.Sprintf("%s:\\Windows\\system32\\kernel32.dll", drive)) } + var filename string for i := 0; i < len(drives); i++ { - filename = mapper.Convert(fmt.Sprintf("\\Device\\HarddiskVolume%d\\Windows\\system32\\kernel32.dll", i)) + filename = m.Convert(fmt.Sprintf("\\Device\\HarddiskVolume%d\\Windows\\system32\\kernel32.dll", i)) if !strings.HasPrefix(filename, "\\Device") { break } } assert.Contains(t, files, filename) -} -func TestConvertVmsmbDevice(t *testing.T) { - mapper := NewDevMapper() - path := "\\Device\\vmsmb\\VSMB-{dcc079ae-60ba-4d07-847c-3493609c0870}\\os\\Windows\\System32\\ntdll.dll" - assert.Equal(t, "C:\\Windows\\System32\\ntdll.dll", mapper.Convert(path)) + m.(*mapper).cache["\\Device\\HarddiskVolume1"] = "C:" + m.(*mapper).sysroot = "C:\\Windows" + + var tests = []struct { + inputFilename string + expectedFilename string + }{ + {"\\Device\\HarddiskVolume1\\Windows\\system32\\kernel32.dll", "C:\\Windows\\system32\\kernel32.dll"}, + {"\\Device\\HarddiskVolume5\\Windows\\system32\\kernel32.dll", "\\Device\\HarddiskVolume5\\Windows\\system32\\kernel32.dll"}, + {"\\Device\\vmsmb\\VSMB-{dcc079ae-60ba-4d07-847c-3493609c0870}\\os\\Windows\\System32\\ntdll.dll", "C:\\Windows\\System32\\ntdll.dll"}, + {"\\SystemRoot\\system32\\drivers\\wd\\WdNisDrv.sys", "C:\\Windows\\system32\\drivers\\wd\\WdNisDrv.sys"}, + {"\\SYSTEMROOT\\system32\\drivers\\wd\\WdNisDrv.sys", "C:\\Windows\\system32\\drivers\\wd\\WdNisDrv.sys"}, + {"\\Device\\Mup", "\\Device\\Mup"}, + } + + for _, tt := range tests { + t.Run(tt.inputFilename, func(t *testing.T) { + assert.Equal(t, tt.expectedFilename, m.Convert(tt.inputFilename)) + }) + } } From fe98381de17caf777ea1a723ded149c9f3dbc079 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Wed, 9 Jul 2025 20:06:48 +0200 Subject: [PATCH 22/42] tests(ps): Verify process token retrieval from OS --- pkg/ps/snapshotter_windows.go | 8 +++++--- pkg/ps/snapshotter_windows_test.go | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/pkg/ps/snapshotter_windows.go b/pkg/ps/snapshotter_windows.go index e2576efca..0656076c6 100644 --- a/pkg/ps/snapshotter_windows.go +++ b/pkg/ps/snapshotter_windows.go @@ -628,9 +628,11 @@ func (s *snapshotter) Find(pid uint32) (bool, *pstypes.PS) { } // get process token attributes - var token windows.Token - var tokenUser *windows.Tokenuser - var tokenMandatoryLabel *windows.Tokenmandatorylabel + var ( + token windows.Token + tokenUser *windows.Tokenuser + tokenMandatoryLabel *windows.Tokenmandatorylabel + ) err = windows.OpenProcessToken(process, windows.TOKEN_QUERY, &token) if err != nil { diff --git a/pkg/ps/snapshotter_windows_test.go b/pkg/ps/snapshotter_windows_test.go index 0a767fd7e..ca4176969 100644 --- a/pkg/ps/snapshotter_windows_test.go +++ b/pkg/ps/snapshotter_windows_test.go @@ -295,6 +295,29 @@ func TestWriteInternalEventsEnrichment(t *testing.T) { assert.Equal(t, uint32(1), proc.SessionID) }, }, + {"consult process token integrity level from OS", + []*event.Event{ + { + Type: event.CreateProcess, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(os.Getpid())}, + params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(444)}, + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: `svchost.exe`}, + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: `svchost.exe -k LocalSystemNetworkRestricted -p -s NcbService`}, + params.UserSID: {Name: params.UserSID, Type: params.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, + params.SessionID: {Name: params.SessionID, Type: params.Uint32, Value: uint32(1)}, + params.ProcessFlags: {Name: params.ProcessFlags, Type: params.Flags, Value: uint32(0x00000010)}, + }, + }, + }, + NewSnapshotter(hsnap, &config.Config{}), + func(t *testing.T, psnap Snapshotter) { + ok, proc := psnap.Find(uint32(os.Getpid())) + assert.True(t, ok) + assert.Equal(t, "HIGH", proc.TokenIntegrityLevel) + assert.Equal(t, true, proc.IsTokenElevated) + }, + }, } for _, tt := range tests { From d783673d0c59589f7d9424136c0f6cc53e9af33c Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Fri, 11 Jul 2025 17:49:43 +0200 Subject: [PATCH 23/42] chore(deps): Bump Go to 1.24.5 --- .github/workflows/master.yml | 2 +- .github/workflows/pr.yml | 2 +- .github/workflows/release.yml | 2 +- go.mod | 4 +--- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 95ce6d833..fed24e2a3 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -10,7 +10,7 @@ on: - "docs/**" env: - GO_VERSION: 1.23.x + GO_VERSION: 1.24.x WIX_VERSION: 5.0.0 PYTHON_VERSION: 3.7.9 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 1f08d0888..14ecac72a 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -8,7 +8,7 @@ on: - "docs/**" env: - GO_VERSION: 1.23.x + GO_VERSION: 1.24.x WIX_VERSION: 5.0.0 PYTHON_VERSION: 3.7.9 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index df7a782eb..606e59141 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,7 @@ on: - 'v*' env: - GO_VERSION: 1.23.x + GO_VERSION: 1.24.x WIX_VERSION: 5.0.0 PYTHON_VERSION: 3.7.9 diff --git a/go.mod b/go.mod index ae98f599a..99cd017b4 100644 --- a/go.mod +++ b/go.mod @@ -94,6 +94,4 @@ require ( gopkg.in/yaml.v2 v2.3.0 // indirect ) -go 1.23.0 - -toolchain go1.23.1 +go 1.24.5 From 747e186cd1a1d5e9dd72a55ed2139ed61cd402f6 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Fri, 11 Jul 2025 18:17:36 +0200 Subject: [PATCH 24/42] chore: Bump golang-ci-lint to 2.2.2 --- .github/workflows/master.yml | 2 +- .github/workflows/pr.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index fed24e2a3..21970a42d 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -228,7 +228,7 @@ jobs: run: | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin $GOLANGCI_LINT_VER env: - GOLANGCI_LINT_VER: v1.61.0 + GOLANGCI_LINT_VER: v2.2.2 - name: Lint shell: bash run: | diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 14ecac72a..f4698bd18 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -210,7 +210,7 @@ jobs: run: | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin $GOLANGCI_LINT_VER env: - GOLANGCI_LINT_VER: v1.61.0 + GOLANGCI_LINT_VER: v2.2.2 - name: Lint shell: bash run: | From cda1ef637a8f7a22ef8ef599f7fc8c6669f80836 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Fri, 18 Jul 2025 22:23:35 +0200 Subject: [PATCH 25/42] refactor: Migrate golang-ci-lint to 2.2.2 Along the way, address/ignore staticcheck linter issues. --- .golangci.yml | 54 ++++++++++++++++------------ cmd/fibratus/app/rules/create.go | 4 +-- pkg/alertsender/eventlog/eventlog.go | 4 +-- pkg/event/marshaller.go | 6 ++-- pkg/event/param.go | 4 +-- pkg/filter/ql/functions/replace.go | 4 +-- pkg/filter/ql/lexer.go | 22 ++++++------ pkg/sys/device.go | 2 +- pkg/util/key/key.go | 2 +- 9 files changed, 57 insertions(+), 45 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 5cded955a..07e469cd0 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,46 +1,54 @@ +version: "2" run: build-tags: - cap - filament - deadline: 10m linters: - disable-all: true + default: none enable: - bodyclose - errcheck - goconst - goprintffuncname - - gosimple - govet - - gofmt - ineffassign - nakedret - noctx - nolintlint - rowserrcheck - staticcheck - - stylecheck - - typecheck - unconvert - unparam - unused - whitespace + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + - linters: + - errcheck + - nolintlint + - staticcheck + path: _test\.go + paths: + - third_party$ + - builtin$ + - examples$ -linters-settings: - gofmt: - simplify: false - -issues: - # List of regexps of issue texts to exclude. - # - # But independently of this option we use default exclude patterns, - # it can be disabled by `exclude-use-default: false`. - # To list all excluded by default patterns execute `golangci-lint run --help` - # - exclude-rules: - # Exclude some linters from running on tests files. - - path: _test\.go - linters: - - errcheck - - nolintlint +formatters: + enable: + - gofmt + settings: + gofmt: + simplify: false + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/cmd/fibratus/app/rules/create.go b/cmd/fibratus/app/rules/create.go index 0a8fb41f8..c21dc3bbc 100644 --- a/cmd/fibratus/app/rules/create.go +++ b/cmd/fibratus/app/rules/create.go @@ -87,9 +87,9 @@ func createRule(name string) error { return err } - n := fmt.Sprintf("%s.yml", strings.Replace(strings.ToLower(name), " ", "_", -1)) + n := fmt.Sprintf("%s.yml", strings.ReplaceAll(strings.ToLower(name), " ", "_")) if tacticID != "" { - n = strings.Replace(strings.ToLower(tactics[tacticID]), " ", "_", -1) + "_" + n + n = strings.ReplaceAll(strings.ToLower(tactics[tacticID]), " ", "_") + "_" + n } f, err := os.Create(n) if err != nil { diff --git a/pkg/alertsender/eventlog/eventlog.go b/pkg/alertsender/eventlog/eventlog.go index 3b83a1444..9608d86ba 100644 --- a/pkg/alertsender/eventlog/eventlog.go +++ b/pkg/alertsender/eventlog/eventlog.go @@ -77,7 +77,7 @@ func (s *eventlog) Send(alert alertsender.Alert) error { // assume alert ID has the UUID format // where we build the short version by // taking the first 12 characters - id := strings.Replace(alert.ID, "-", "", -1) + id := strings.ReplaceAll(alert.ID, "-", "") h := crc32.ChecksumIEEE([]byte(id[:minIDChars])) // take the lower 16 bits of the CRC32 hash code = uint16(h & 0xFFFF) @@ -86,7 +86,7 @@ func (s *eventlog) Send(alert alertsender.Alert) error { msg := alert.String(s.config.Verbose) // trim null characters to avoid // UTF16PtrFromString complaints - msg = strings.Replace(msg, "\x00", "", -1) + msg = strings.ReplaceAll(msg, "\x00", "") m, err := windows.UTF16PtrFromString(msg) if err != nil { diff --git a/pkg/event/marshaller.go b/pkg/event/marshaller.go index bcaec4aae..01008265f 100644 --- a/pkg/event/marshaller.go +++ b/pkg/event/marshaller.go @@ -124,6 +124,7 @@ func (js *jsonStream) writeMore() *jsonStream { } func (js *jsonStream) shouldWriteMore(i, l int) bool { + //nolint:staticcheck return !(i == l-1) } @@ -295,9 +296,10 @@ func writeStringSlowPath(stream *jsonStream, i int, s string, valLen int) { func writeFirstBuf(space []byte, v uint32) []byte { start := v >> 24 - if start == 0 { + switch start { + case 0: space = append(space, byte(v>>16), byte(v>>8)) - } else if start == 1 { + case 1: space = append(space, byte(v>>8)) } space = append(space, byte(v)) diff --git a/pkg/event/param.go b/pkg/event/param.go index 62e5209c2..894bf27c8 100644 --- a/pkg/event/param.go +++ b/pkg/event/param.go @@ -706,9 +706,9 @@ func (pars Params) String() string { case SnakeCase: sb.WriteString(par.Name + ParamKVDelimiter + par.String()) case DotCase: - sb.WriteString(strings.Replace(par.Name, "_", ".", -1) + ParamKVDelimiter + par.String()) + sb.WriteString(strings.ReplaceAll(par.Name, "_", ".") + ParamKVDelimiter + par.String()) case PascalCase: - sb.WriteString(strings.Replace(caser.String(strings.Replace(par.Name, "_", " ", -1)), " ", "", -1) + ParamKVDelimiter + par.String()) + sb.WriteString(strings.ReplaceAll(caser.String(strings.ReplaceAll(par.Name, "_", " ")), " ", "") + ParamKVDelimiter + par.String()) case CamelCase: } if i != len(pars)-1 { diff --git a/pkg/filter/ql/functions/replace.go b/pkg/filter/ql/functions/replace.go index f547826b0..f34610d9c 100644 --- a/pkg/filter/ql/functions/replace.go +++ b/pkg/filter/ql/functions/replace.go @@ -36,7 +36,7 @@ func (f Replace) Call(args []interface{}) (interface{}, bool) { if len(args) == 3 { o := parseString(1, args) n := parseString(2, args) - return strings.Replace(s, o, n, -1), true + return strings.ReplaceAll(s, o, n), true } // apply multiple replacements repl := s @@ -49,7 +49,7 @@ func (f Replace) Call(args []interface{}) (interface{}, bool) { if !ok { break } - repl = strings.Replace(repl, o, n, -1) + repl = strings.ReplaceAll(repl, o, n) } return repl, true } diff --git a/pkg/filter/ql/lexer.go b/pkg/filter/ql/lexer.go index e6b69d650..4733285e4 100644 --- a/pkg/filter/ql/lexer.go +++ b/pkg/filter/ql/lexer.go @@ -326,15 +326,16 @@ func (s *scanner) scanString() (tok token, pos int, lit string) { s.r.unread() _, pos = s.r.curr() - var err error - lit, err = ScanString(s.r) - if err == errBadString { + lit, err := ScanString(s.r) + switch err { + case errBadString: return Badstr, pos, lit - } else if err == errBadEscape { + case errBadEscape: _, pos = s.r.curr() return Badstr, pos, lit + default: + return Str, pos, lit } - return Str, pos, lit } var errBadString = errors.New("bad string") @@ -358,15 +359,16 @@ func ScanString(r io.RuneScanner) (string, error) { // If the next character is an escape then write the escaped char. // If it's not a valid escape then return an error. ch1, _, _ := r.ReadRune() - if ch1 == 'n' { + switch ch1 { + case 'n': _, _ = buf.WriteRune('\n') - } else if ch1 == '\\' { + case '\\': _, _ = buf.WriteRune('\\') - } else if ch1 == '"' { + case '"': _, _ = buf.WriteRune('"') - } else if ch1 == '\'' { + case '\'': _, _ = buf.WriteRune('\'') - } else { + default: return string(ch0) + string(ch1), errBadEscape } } else { diff --git a/pkg/sys/device.go b/pkg/sys/device.go index 43ea7f6f5..c12f8b92b 100644 --- a/pkg/sys/device.go +++ b/pkg/sys/device.go @@ -74,7 +74,7 @@ func EnumDevices() []Driver { continue } dev := syscall.UTF16ToString(filename) - drv.Filename = strings.Replace(dev, "\\SystemRoot", os.Getenv("SYSTEMROOT"), -1) + drv.Filename = strings.Replace(dev, "\\SystemRoot", os.Getenv("SYSTEMROOT"), 1) drivers[i] = drv } return drivers diff --git a/pkg/util/key/key.go b/pkg/util/key/key.go index f1325600e..cf59affc1 100644 --- a/pkg/util/key/key.go +++ b/pkg/util/key/key.go @@ -188,7 +188,7 @@ func Format(key string) (Key, string) { if strings.HasSuffix(sid, "_Classes") { return CurrentUser, "Software\\Classes\\" + path[n+1:] } - return CurrentUser, strings.Replace(path[n+1:], "_Classes", "Software\\Classes", -1) + return CurrentUser, strings.Replace(path[n+1:], "_Classes", "Software\\Classes", 1) case sid == loggedSID: if len(path) == len(loggedSID) { return CurrentUser, "" From 23593909f5fad7143f556cf87b2abb98f73bcab5 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Mon, 21 Jul 2025 20:24:08 +0200 Subject: [PATCH 26/42] fix(tests,ps): Ensure session ID is greater than zero --- pkg/ps/snapshotter_windows_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/ps/snapshotter_windows_test.go b/pkg/ps/snapshotter_windows_test.go index ca4176969..92b1879fd 100644 --- a/pkg/ps/snapshotter_windows_test.go +++ b/pkg/ps/snapshotter_windows_test.go @@ -762,7 +762,7 @@ func TestFindQueryOS(t *testing.T) { assert.Equal(t, filepath.Join(os.Getenv("windir"), "notepad.exe"), proc.Cmdline) assert.True(t, len(proc.Envs) > 0) assert.Contains(t, proc.Cwd, "fibratus\\pkg\\ps") - assert.Equal(t, uint32(1), proc.SessionID) + assert.True(t, proc.SessionID > 0) assert.Equal(t, "HIGH", proc.TokenIntegrityLevel) wts, err := sys.LookupActiveWTS() From dcab3fcc0ecb9907fc3fc1ea1d8dce1b777c455f Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Mon, 21 Jul 2025 20:51:33 +0200 Subject: [PATCH 27/42] feat(rules): New Windows Defender driver unloading rule Detects the unloading of Windows Defender kernel-mode drivers, such as WdFilter.sys or WdBoot.sys, which may indicate an attempt to impair or disable antivirus protections. Adversaries may unload these drivers to bypass or disable real-time scanning, file system filtering, or ELAM (Early Launch Anti-Malware) protections. Legitimate driver unloads are rare and should be investigated to rule out malicious tampering or post-exploitation activity. --- ...sion_windows_defender_driver_unloading.yml | 28 +++++++++++++++++++ rules/macros/macros.yml | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 rules/defense_evasion_windows_defender_driver_unloading.yml diff --git a/rules/defense_evasion_windows_defender_driver_unloading.yml b/rules/defense_evasion_windows_defender_driver_unloading.yml new file mode 100644 index 000000000..77468eaf1 --- /dev/null +++ b/rules/defense_evasion_windows_defender_driver_unloading.yml @@ -0,0 +1,28 @@ +name: Windows Defender driver unloading +id: c9b93fbc-8845-4f39-a74b-26862615432c +version: 1.0.0 +description: | + Detects the unloading of Windows Defender kernel-mode drivers, such as WdFilter.sys or WdBoot.sys, + which may indicate an attempt to impair or disable antivirus protections. + Adversaries may unload these drivers to bypass or disable real-time scanning, file system filtering, + or ELAM (Early Launch Anti-Malware) protections. Legitimate driver unloads are rare and should be + investigated to rule out malicious tampering or post-exploitation activity. +labels: + tactic.id: TA0005 + tactic.name: Defense Evasion + tactic.ref: https://attack.mitre.org/tactics/TA0005/ + technique.id: T1562 + technique.name: Impair Defenses + technique.ref: https://attack.mitre.org/techniques/T1562/ + subtechnique.id: T1562.001 + subtechnique.name: Disable or Modify Tools + subtechnique.ref: https://attack.mitre.org/techniques/T1562/001 + +condition: > + unload_driver and image.path imatches ('?:\\Windows\\System32\\drivers\\wd\\*.sys', '?:\\Windows\\System32\\drivers\\Wd*.sys') + +output: > + Windows Defender driver %image.path unloaded by process %ps.exe +severity: high + +min-engine-version: 3.0.0 diff --git a/rules/macros/macros.yml b/rules/macros/macros.yml index d0e70d2a4..5af256764 100644 --- a/rules/macros/macros.yml +++ b/rules/macros/macros.yml @@ -135,7 +135,7 @@ watching for driver objects being created. - macro: unload_driver - expr: unload_image and (image.name iendswith '.sys' or image.is_driver) + expr: unload_module and (image.name iendswith '.sys' or image.is_driver) - macro: load_unsigned_module expr: > From 72f0973b2b65eb292c7d7af81ceb16a792e1ec9a Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Tue, 22 Jul 2025 18:51:03 +0200 Subject: [PATCH 28/42] feat(event): Parse RegSetValueInternal event parameters --- pkg/event/event_windows.go | 1 + pkg/event/metainfo_windows.go | 1 + pkg/event/param_windows.go | 33 +++++++++++++++++++++++++++++- pkg/event/params/params_windows.go | 2 ++ pkg/event/types_windows.go | 15 ++++++++++---- 5 files changed, 47 insertions(+), 5 deletions(-) diff --git a/pkg/event/event_windows.go b/pkg/event/event_windows.go index 0782a9740..70ffb16ad 100644 --- a/pkg/event/event_windows.go +++ b/pkg/event/event_windows.go @@ -233,6 +233,7 @@ func (e *Event) IsLoadImageInternal() bool { return e.Type == LoadImageInte func (e *Event) IsImageRundown() bool { return e.Type == ImageRundown } func (e *Event) IsFileOpEnd() bool { return e.Type == FileOpEnd } func (e *Event) IsRegSetValue() bool { return e.Type == RegSetValue } +func (e *Event) IsRegSetValueInternal() bool { return e.Type == RegSetValueInternal } func (e *Event) IsProcessRundown() bool { return e.Type == ProcessRundown } func (e *Event) IsProcessRundownInternal() bool { return e.Type == ProcessRundownInternal } func (e *Event) IsVirtualAlloc() bool { return e.Type == VirtualAlloc } diff --git a/pkg/event/metainfo_windows.go b/pkg/event/metainfo_windows.go index 7794315f0..d7913cf22 100644 --- a/pkg/event/metainfo_windows.go +++ b/pkg/event/metainfo_windows.go @@ -241,6 +241,7 @@ func AllWithState() []Type { s = append(s, CreateProcessInternal) s = append(s, ProcessRundownInternal) s = append(s, LoadImageInternal) + s = append(s, RegSetValueInternal) return s } diff --git a/pkg/event/param_windows.go b/pkg/event/param_windows.go index 870dd7ef9..0130265b8 100644 --- a/pkg/event/param_windows.go +++ b/pkg/event/param_windows.go @@ -19,6 +19,7 @@ package event import ( + "encoding/binary" "expvar" "fmt" "github.com/rabbitstack/fibratus/pkg/event/params" @@ -33,7 +34,9 @@ import ( "github.com/rabbitstack/fibratus/pkg/util/signature" "github.com/rabbitstack/fibratus/pkg/util/va" "golang.org/x/sys/windows" + "golang.org/x/sys/windows/registry" "net" + "path/filepath" "strconv" "strings" "time" @@ -255,7 +258,7 @@ func (e *Event) produceParams(evt *etw.EventRecord) { } sid, soffset = evt.ReadSID(offset, true) name, noffset = evt.ReadAnsiString(soffset) - cmdline, _ = evt.ReadUTF16String(soffset + noffset) + cmdline, _ = evt.ReadUTF16String(noffset) e.AppendParam(params.ProcessObject, params.Address, kproc) e.AppendParam(params.ProcessID, params.PID, pid) e.AppendParam(params.ProcessParentID, params.PID, ppid) @@ -508,6 +511,34 @@ func (e *Event) produceParams(evt *etw.EventRecord) { e.AppendParam(params.RegKeyHandle, params.Address, keyHandle) e.AppendParam(params.RegPath, params.Key, keyName) e.AppendParam(params.NTStatus, params.Status, status) + case RegSetValueInternal: + keyObject := evt.ReadUint64(0) + status := evt.ReadUint32(8) + valueType := evt.ReadUint32(12) + keyName, koffset := evt.ReadUTF16String(20) // skip data size param (4 bytes) + valueName, voffset := evt.ReadUTF16String(koffset) + capturedSize := evt.ReadUint16(voffset) + capturedData := evt.ReadBytes(2+voffset, capturedSize) + + e.AppendParam(params.RegKeyHandle, params.Address, keyObject) + e.AppendParam(params.NTStatus, params.Status, status) + e.AppendParam(params.RegPath, params.Key, filepath.Join(keyName, valueName)) + e.AppendEnum(params.RegValueType, valueType, key.RegistryValueTypes) + + if len(capturedData) > 0 { + switch valueType { + case registry.SZ, registry.MULTI_SZ, registry.EXPAND_SZ: + e.AppendParam(params.RegData, params.UnicodeString, string(capturedData)) + case registry.BINARY: + e.AppendParam(params.RegData, params.Binary, capturedData) + case registry.DWORD: + e.AppendParam(params.RegData, params.Uint32, binary.LittleEndian.Uint32(capturedData)) + case registry.DWORD_BIG_ENDIAN: + e.AppendParam(params.RegData, params.Uint32, binary.BigEndian.Uint32(capturedData)) + case registry.QWORD: + e.AppendParam(params.RegData, params.Uint64, binary.LittleEndian.Uint64(capturedData)) + } + } case CreateFile: var ( irp uint64 diff --git a/pkg/event/params/params_windows.go b/pkg/event/params/params_windows.go index 8be05a3bf..1a860094d 100644 --- a/pkg/event/params/params_windows.go +++ b/pkg/event/params/params_windows.go @@ -149,6 +149,8 @@ const ( RegValue = "value" // RegValueType identifies the parameter that represents registry value type e.g (DWORD, BINARY) RegValueType = "value_type" + // RegData identifies the parameter that stores the captured registry data + RegData = "data" // ImageBase identifies the parameter name for the base address of the process in which the image is loaded. ImageBase = "base_address" diff --git a/pkg/event/types_windows.go b/pkg/event/types_windows.go index 52f7bb04b..d727e9178 100644 --- a/pkg/event/types_windows.go +++ b/pkg/event/types_windows.go @@ -67,6 +67,8 @@ var ( ThreadpoolGUID = windows.GUID{Data1: 0xc861d0e2, Data2: 0xa2c1, Data3: 0x4d36, Data4: [8]byte{0x9f, 0x9c, 0x97, 0x0b, 0xab, 0x94, 0x3a, 0x12}} // ProcessKernelEventGUID represents the Process Kernel event GUID ProcessKernelEventGUID = windows.GUID{Data1: 0x22fb2cd6, Data2: 0x0e7b, Data3: 0x422b, Data4: [8]byte{0xa0, 0xc7, 0x2f, 0xad, 0x1f, 0xd0, 0xe7, 0x16}} + // RegistryKernelEventGUID represents the Registry Kernel event GUID + RegistryKernelEventGUID = windows.GUID{Data1: 0x70eb4f03, Data2: 0xc1de, Data3: 0x4f73, Data4: [8]byte{0xa0, 0x51, 0x33, 0xd1, 0x3d, 0x54, 0x13, 0xbd}} ) var ( @@ -149,6 +151,10 @@ var ( RegDeleteKCB = pack(RegistryEventGUID, 23) // RegKCBRundown enumerates the registry keys open at the start of the kernel session. RegKCBRundown = pack(RegistryEventGUID, 25) + // RegSetValueInternal is the internal event that is used to + // enrich the corresponding public RegSetValue event with + // extra attributes + RegSetValueInternal = pack(RegistryKernelEventGUID, 36) // UnloadImage represents unload image kernel events UnloadImage = pack(ImageEventGUID, 2) @@ -309,7 +315,7 @@ func (t Type) String() string { return "RegQueryValue" case RegCreateKCB: return "RegCreateKCB" - case RegSetValue: + case RegSetValue, RegSetValueInternal: return "RegSetValue" case LoadImage, LoadImageInternal: return "LoadImage" @@ -367,7 +373,7 @@ func (t Type) Category() Category { FileRundown, FileOpEnd, ReleaseFile, MapViewFile, UnmapViewFile, MapFileRundown: return File case RegCreateKey, RegDeleteKey, RegOpenKey, RegCloseKey, RegQueryKey, RegQueryValue, RegSetValue, RegDeleteValue, - RegKCBRundown, RegDeleteKCB, RegCreateKCB: + RegKCBRundown, RegDeleteKCB, RegCreateKCB, RegSetValueInternal: return Registry case AcceptTCPv4, AcceptTCPv6, ConnectTCPv4, ConnectTCPv6, @@ -527,7 +533,8 @@ func (t Type) OnlyState() bool { ReleaseFile, MapFileRundown, RegCreateKCB, - RegDeleteKCB: + RegDeleteKCB, + RegSetValueInternal: return true default: return false @@ -600,7 +607,7 @@ func (t Type) ID() uint { // Source designates the provenance of this event type. func (t Type) Source() Source { switch t.GUID() { - case AuditAPIEventGUID, DNSEventGUID, ThreadpoolGUID, ProcessKernelEventGUID: + case AuditAPIEventGUID, DNSEventGUID, ThreadpoolGUID, ProcessKernelEventGUID, RegistryKernelEventGUID: return SecurityTelemetryLogger default: return SystemLogger From 80a6b2d54e859c48f56bd60ca756d942e8e7ad96 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Tue, 22 Jul 2025 18:53:26 +0200 Subject: [PATCH 29/42] feat(sys): Pass filter data to ETW provider session's callback --- pkg/sys/etw/etw.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pkg/sys/etw/etw.go b/pkg/sys/etw/etw.go index 32fbc1996..16905b101 100644 --- a/pkg/sys/etw/etw.go +++ b/pkg/sys/etw/etw.go @@ -236,6 +236,9 @@ func CaptureProviderState(guid windows.GUID, handle TraceHandle) error { type EnableTraceOpts struct { // WithStacktrace indicates call stack trace is added to the extended data of events. WithStacktrace bool + // EventFilterDescriptors defines the filter data that a session passes to the provider's + // enable callback function. + EventFilterDescriptors []EventFilterDescriptor } // EnableTraceWithOpts influences the behaviour of the specified event trace provider @@ -245,9 +248,16 @@ func EnableTraceWithOpts(guid windows.GUID, handle TraceHandle, keywords uint64, Version: EnableTraceParametersVersion, SourceID: guid, } + if opts.WithStacktrace { params.EnableProperty = EventEnablePropertyStacktrace } + + if len(opts.EventFilterDescriptors) > 0 { + params.EnableFilterDesc = uintptr(unsafe.Pointer(&opts.EventFilterDescriptors[0])) + params.FilterDescCount = uint32(len(opts.EventFilterDescriptors)) + } + err := enableTraceEx2(handle, &guid, ControlCodeEnableProvider, TraceLevelInformation, keywords, 0, 0, params) if err != nil { return os.NewSyscallError("EnableTraceEx2", err) From b4c44cc6e07dba11bb6a4210451f8fbb6c586558 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Tue, 22 Jul 2025 18:56:14 +0200 Subject: [PATCH 30/42] fix(sys,etw): Correct string parsing functions --- pkg/sys/etw/types.go | 62 +++++++++++++++++++++++---------------- pkg/sys/etw/types_test.go | 2 +- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/pkg/sys/etw/types.go b/pkg/sys/etw/types.go index cd4c78ae7..5fdba491e 100644 --- a/pkg/sys/etw/types.go +++ b/pkg/sys/etw/types.go @@ -555,6 +555,14 @@ type ClassicEventID struct { _ [7]uint8 // reserved } +// EventFilterDescriptor defines the filter data that +// a session passes to the provider's enable callback. +type EventFilterDescriptor struct { + Ptr uintptr + Size uint32 + Type uint32 +} + // NewClassicEventID creates a new instance of classic event identifier. func NewClassicEventID(guid windows.GUID, typ uint16) ClassicEventID { return ClassicEventID{GUID: guid, Type: uint8(typ)} @@ -604,7 +612,7 @@ func (e *EventRecord) ReadBytes(offset uint16, count uint16) []byte { if offset > e.BufferLen { return nil } - return (*[1<<30 - 1]byte)(unsafe.Pointer(e.Buffer + uintptr(offset) + uintptr(count)))[:count:count] + return (*[1<<30 - 1]byte)(unsafe.Pointer(e.Buffer + uintptr(offset)))[:count:count] } // ReadUint16 reads the uint16 value from the buffer at the specified offset. @@ -637,6 +645,7 @@ func (e *EventRecord) ReadAnsiString(offset uint16) (string, uint16) { if offset > e.BufferLen { return "", 0 } + b := make([]byte, e.BufferLen) var i uint16 for i < e.BufferLen { @@ -647,49 +656,52 @@ func (e *EventRecord) ReadAnsiString(offset uint16) (string, uint16) { b[i] = c i++ } + + if i == 0 { + return "", offset + 1 + } + if int(i) > len(b) { - return string(b[:len(b)-1]), uint16(len(b)) + return string(b[:len(b)-1]), uint16(len(b)) + offset } - return string(b[:i]), i + 1 + + return string(b[:i]), i + 1 + offset } // ReadUTF16String reads the UTF-16 string from the buffer at the specified offset. -// Returns the UTF-8 string and the number of bytes read from the string. +// Returns the UTF-8 string and the number of bytes read from the string + the offset. func (e *EventRecord) ReadUTF16String(offset uint16) (string, uint16) { if offset > e.BufferLen { return "", 0 } + // we're reading the leading string. First, calculate + // the length of the null-terminated UTF16 string + i := offset var length uint16 - - if offset > 0 { - length = e.BufferLen - offset - } else { - // we're reading the leading string. First, calculate - // the length of the null-terminated UTF16 string - var i uint16 - for i < e.BufferLen { - c := *(*uint16)(unsafe.Pointer(e.Buffer + uintptr(i))) - if c == 0 { - break // null terminator - } - length += 2 - i += 2 + for i < e.BufferLen { + c := *(*uint16)(unsafe.Pointer(e.Buffer + uintptr(i))) + if c == 0 { + break // null terminator } + length += 2 + i += 2 } - s := (*[1<<30 - 1]uint16)(unsafe.Pointer(e.Buffer + uintptr(offset)))[:length:length] - if offset > 0 { - return utf16.Decode(s[:len(s)/2-1-2]), uint16(len(s) + 2) + if length == 0 { + return "", offset + 2 // null terminator size } - return utf16.Decode(s[:len(s)/2]), uint16(len(s) + 2) + b := (*[1<<30 - 1]uint16)(unsafe.Pointer(e.Buffer + uintptr(offset)))[:length:length] + s := b[:len(b)/2] + + return utf16.Decode(s), uint16((len(s)+1)*2) + offset } // ReadNTUnicodeString reads the native Unicode string at the given offset. func (e *EventRecord) ReadNTUnicodeString(offset uint16) (string, uint16) { if offset > e.BufferLen { - return "", offset + return "", 0 } i := offset @@ -704,7 +716,7 @@ func (e *EventRecord) ReadNTUnicodeString(offset uint16) (string, uint16) { } if length == 0 { - return "", offset + return "", offset + 2 // null terminator size } b := (*[1<<30 - 1]byte)(unsafe.Pointer(e.Buffer + uintptr(offset)))[:length:length] @@ -715,7 +727,7 @@ func (e *EventRecord) ReadNTUnicodeString(offset uint16) (string, uint16) { Buffer: (*uint16)(unsafe.Pointer(&b[0])), } - return s.String(), offset + s.Length + return s.String(), s.Length + offset } // ConsumeUTF16String reads the byte slice with UTF16-encoded string diff --git a/pkg/sys/etw/types_test.go b/pkg/sys/etw/types_test.go index f82c7c7ef..f65318e81 100644 --- a/pkg/sys/etw/types_test.go +++ b/pkg/sys/etw/types_test.go @@ -84,7 +84,7 @@ func TestReadBuffer(t *testing.T) { name, noffset := ev.ReadAnsiString(offset) assert.Equal(t, "cmd.exe", name) - cmdline, _ := ev.ReadUTF16String(noffset + offset) + cmdline, _ := ev.ReadUTF16String(noffset) assert.Equal(t, "C:\\WINDOWS\\system32\\cmd.exe /c dir /-C /W \"\\\\?\\c:\\Users\\nedo\\AppData\\Roaming\\RabbitMQ\\db\\rabbit@archrabbit-mnesia\"", cmdline) }, }, From e38d5a343ebd292147665e054fe6ddba06d86b16 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Tue, 22 Jul 2025 18:58:26 +0200 Subject: [PATCH 31/42] feat(etw): Enable Windows Kernel Registry provider This provider is enabled with the hidden filter flag to make the ETW session write captured registry data. The registry event processor keeps the queue of received internal set value events and enriches subsequent RegSetValue events emitted by the NT Kernel Logger provider. --- internal/etw/consumer.go | 5 +- internal/etw/processors/registry_windows.go | 184 +++++++++++++++--- .../etw/processors/registry_windows_test.go | 55 +++++- internal/etw/source.go | 59 ++++-- internal/etw/trace.go | 31 ++- pkg/sys/etw/types.go | 6 + 6 files changed, 284 insertions(+), 56 deletions(-) diff --git a/internal/etw/consumer.go b/internal/etw/consumer.go index 6f5d2d979..03787376a 100644 --- a/internal/etw/consumer.go +++ b/internal/etw/consumer.go @@ -23,7 +23,6 @@ import ( "github.com/rabbitstack/fibratus/pkg/config" "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/filter" - "github.com/rabbitstack/fibratus/pkg/handle" "github.com/rabbitstack/fibratus/pkg/ps" "github.com/rabbitstack/fibratus/pkg/sys/etw" ) @@ -47,15 +46,15 @@ type Consumer struct { // NewConsumer builds a new event consumer. func NewConsumer( psnap ps.Snapshotter, - hsnap handle.Snapshotter, config *config.Config, sequencer *event.Sequencer, evts chan *event.Event, + processors processors.Chain, ) *Consumer { return &Consumer{ q: event.NewQueueWithChannel(evts, config.EventSource.StackEnrichment, config.ForwardMode || config.IsCaptureSet()), sequencer: sequencer, - processors: processors.NewChain(psnap, hsnap, config), + processors: processors, psnap: psnap, config: config, } diff --git a/internal/etw/processors/registry_windows.go b/internal/etw/processors/registry_windows.go index 02f132aa7..a3ca6c7ea 100644 --- a/internal/etw/processors/registry_windows.go +++ b/internal/etw/processors/registry_windows.go @@ -27,6 +27,7 @@ import ( "os" "path/filepath" "strings" + "sync" "sync/atomic" "time" @@ -41,11 +42,20 @@ var ( return fmt.Errorf("unable to read value %s : %v", filepath.Join(key, subkey), err) } + // valueTTL specifies the maximum allowed period for RegSetValueInternal events + // to remain in the queue + valueTTL = time.Minute * 2 + // valuePurgerInterval specifies the purge interval for stale values + valuePurgerInterval = time.Minute + // kcbCount counts the total KCBs found during the duration of the kernel session kcbCount = expvar.NewInt("registry.kcb.count") kcbMissCount = expvar.NewInt("registry.kcb.misses") keyHandleHits = expvar.NewInt("registry.key.handle.hits") + readValueOps = expvar.NewInt("registry.read.value.ops") + capturedDataHits = expvar.NewInt("registry.data.hits") + handleThrottleCount uint32 ) @@ -57,6 +67,13 @@ type registryProcessor struct { // keys stores the mapping between the KCB (Key Control Block) and the key name. keys map[uint64]string hsnap handle.Snapshotter + + values map[uint32][]*event.Event + mu sync.Mutex + + purger *time.Ticker + + quit chan struct{} } func newRegistryProcessor(hsnap handle.Snapshotter) Processor { @@ -68,10 +85,18 @@ func newRegistryProcessor(hsnap handle.Snapshotter) Processor { atomic.StoreUint32(&handleThrottleCount, 0) } }() - return ®istryProcessor{ - keys: make(map[uint64]string), - hsnap: hsnap, + + r := ®istryProcessor{ + keys: make(map[uint64]string), + hsnap: hsnap, + values: make(map[uint32][]*event.Event), + purger: time.NewTicker(valuePurgerInterval), + quit: make(chan struct{}, 1), } + + go r.housekeep() + + return r } func (r *registryProcessor) ProcessEvent(e *event.Event) (*event.Event, bool, error) { @@ -93,6 +118,12 @@ func (r *registryProcessor) processEvent(e *event.Event) (*event.Event, error) { delete(r.keys, khandle) kcbCount.Add(-1) default: + if e.IsRegSetValueInternal() { + // store the event in temporary queue + r.pushSetValue(e) + return e, nil + } + khandle := e.Params.MustGetUint64(params.RegKeyHandle) // we have to obey a straightforward algorithm to connect relative // key names to their root keys. If key handle is equal to zero we @@ -116,7 +147,32 @@ func (r *registryProcessor) processEvent(e *event.Event) (*event.Event, error) { } } - // get the type/value of the registry key and append to parameters + if e.IsRegSetValue() { + // previously stored RegSetValueInternal event + // is popped from the queue. RegSetValue can + // be enriched with registry value type/data + v := r.popSetValue(e) + if v == nil { + // try to read captured data from userspace + goto readValue + } + + capturedDataHits.Add(1) + + // enrich the event with value data/type parameters + typ, err := v.Params.GetUint32(params.RegValueType) + if err == nil { + e.AppendEnum(params.RegValueType, typ, key.RegistryValueTypes) + } + data, err := v.Params.Get(params.RegData) + if err == nil { + e.AppendParam(params.RegData, data.Type, data.Value) + } + + return e, nil + } + + readValue: if !e.IsRegSetValue() || !e.IsSuccess() { return e, nil } @@ -126,36 +182,42 @@ func (r *registryProcessor) processEvent(e *event.Event) (*event.Event, error) { return e, nil } + // get the type/value of the registry key and append to parameters rootkey, subkey := key.Format(keyName) - if rootkey != key.Invalid { - typ, val, err := rootkey.ReadValue(subkey) - if err != nil { - errno, ok := err.(windows.Errno) - if ok && (errno.Is(os.ErrNotExist) || err == windows.ERROR_ACCESS_DENIED) { - return e, nil - } - return e, ErrReadValue(rootkey.String(), keyName, err) - } - e.AppendEnum(params.RegValueType, typ, key.RegistryValueTypes) - switch typ { - case registry.SZ, registry.EXPAND_SZ: - e.AppendParam(params.RegValue, params.UnicodeString, val) - case registry.MULTI_SZ: - e.AppendParam(params.RegValue, params.Slice, val) - case registry.BINARY: - e.AppendParam(params.RegValue, params.Binary, val) - case registry.QWORD: - e.AppendParam(params.RegValue, params.Uint64, val) - case registry.DWORD: - e.AppendParam(params.RegValue, params.Uint32, uint32(val.(uint64))) + if rootkey == key.Invalid { + return e, nil + } + + readValueOps.Add(1) + typ, val, err := rootkey.ReadValue(subkey) + if err != nil { + errno, ok := err.(windows.Errno) + if ok && (errno.Is(os.ErrNotExist) || err == windows.ERROR_ACCESS_DENIED) { + return e, nil } + return e, ErrReadValue(rootkey.String(), keyName, err) + } + e.AppendEnum(params.RegValueType, typ, key.RegistryValueTypes) + + switch typ { + case registry.SZ, registry.EXPAND_SZ: + e.AppendParam(params.RegData, params.UnicodeString, val) + case registry.MULTI_SZ: + e.AppendParam(params.RegData, params.Slice, val) + case registry.BINARY: + e.AppendParam(params.RegData, params.Binary, val) + case registry.QWORD: + e.AppendParam(params.RegData, params.Uint64, val) + case registry.DWORD: + e.AppendParam(params.RegData, params.Uint32, uint32(val.(uint64))) } } + return e, nil } -func (registryProcessor) Name() ProcessorType { return Registry } -func (registryProcessor) Close() {} +func (*registryProcessor) Name() ProcessorType { return Registry } +func (r *registryProcessor) Close() { r.quit <- struct{}{} } func (r *registryProcessor) findMatchingKey(pid uint32, relativeKeyName string) string { // we want to prevent too frequent queries on the process' handles @@ -166,10 +228,12 @@ func (r *registryProcessor) findMatchingKey(pid uint32, relativeKeyName string) if atomic.LoadUint32(&handleThrottleCount) > maxHandleQueries { return relativeKeyName } + handles, err := r.hsnap.FindHandles(pid) if err != nil { return relativeKeyName } + for _, h := range handles { if h.Type != handle.Key { continue @@ -179,5 +243,71 @@ func (r *registryProcessor) findMatchingKey(pid uint32, relativeKeyName string) return h.Name } } + return relativeKeyName } + +// pushSetValue stores the internal RegSetValue event +// into per process identifier queue. +func (r *registryProcessor) pushSetValue(e *event.Event) { + r.mu.Lock() + defer r.mu.Unlock() + vals, ok := r.values[e.PID] + if !ok { + r.values[e.PID] = []*event.Event{e} + } else { + r.values[e.PID] = append(vals, e) + } +} + +// popSetValue traverses the internal RegSetValue queue +// and pops the event if the suffixes match. +func (r *registryProcessor) popSetValue(e *event.Event) *event.Event { + r.mu.Lock() + defer r.mu.Unlock() + vals, ok := r.values[e.PID] + if !ok { + return nil + } + + var v *event.Event + for i := len(vals) - 1; i >= 0; i-- { + val := vals[i] + if strings.HasSuffix(e.GetParamAsString(params.RegPath), val.GetParamAsString(params.RegPath)) { + v = val + r.values[e.PID] = append(vals[:i], vals[i+1:]...) + break + } + } + + return v +} + +func (r *registryProcessor) valuesSize(pid uint32) int { + r.mu.Lock() + defer r.mu.Unlock() + return len(r.values[pid]) +} + +func (r *registryProcessor) housekeep() { + for { + select { + case <-r.purger.C: + r.mu.Lock() + for pid, vals := range r.values { + for i, val := range vals { + if time.Since(val.Timestamp) < valueTTL { + continue + } + r.values[pid] = append(vals[:i], vals[i+1:]...) + } + if len(vals) == 0 { + delete(r.values, pid) + } + } + r.mu.Unlock() + case <-r.quit: + return + } + } +} diff --git a/internal/etw/processors/registry_windows_test.go b/internal/etw/processors/registry_windows_test.go index 16c0d88db..fb2b6f014 100644 --- a/internal/etw/processors/registry_windows_test.go +++ b/internal/etw/processors/registry_windows_test.go @@ -23,11 +23,18 @@ import ( "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/handle" htypes "github.com/rabbitstack/fibratus/pkg/handle/types" + "github.com/rabbitstack/fibratus/pkg/util/key" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "testing" + "time" ) +func init() { + valueTTL = time.Millisecond * 150 + valuePurgerInterval = time.Millisecond * 300 +} + func TestRegistryProcessor(t *testing.T) { var tests = []struct { name string @@ -161,7 +168,53 @@ func TestRegistryProcessor(t *testing.T) { func(e *event.Event, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { assert.Equal(t, `HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Windows\Directory`, e.GetParamAsString(params.RegPath)) assert.Equal(t, `REG_EXPAND_SZ`, e.GetParamAsString(params.RegValueType)) - assert.Equal(t, `%SystemRoot%`, e.GetParamAsString(params.RegValue)) + assert.Equal(t, `%SystemRoot%`, e.GetParamAsString(params.RegData)) + }, + }, + { + "process registry set value from internal event", + &event.Event{ + Type: event.RegSetValue, + Category: event.Registry, + PID: 23234, + Params: event.Params{ + params.RegPath: {Name: params.RegPath, Type: params.Key, Value: `\REGISTRY\MACHINE\SYSTEM\CurrentControlSet\Control\Windows\Directory`}, + params.RegKeyHandle: {Name: params.RegKeyHandle, Type: params.Uint64, Value: uint64(0)}, + }, + }, + func(p Processor) { + p.(*registryProcessor).values[23234] = []*event.Event{ + { + Type: event.RegSetValueInternal, + Timestamp: time.Now(), + Params: event.Params{ + params.RegPath: {Name: params.RegPath, Type: params.Key, Value: `\SessionId`}, + params.RegData: {Name: params.RegData, Type: params.UnicodeString, Value: "{ABD9EA10-87F6-11EB-9ED5-645D86501328}"}, + params.RegValueType: {Name: params.RegValueType, Type: params.Enum, Value: uint32(1), Enum: key.RegistryValueTypes}, + params.RegKeyHandle: {Name: params.RegKeyHandle, Type: params.Uint64, Value: uint64(0)}}, + }, + { + Type: event.RegSetValueInternal, + Timestamp: time.Now(), + Params: event.Params{ + params.RegPath: {Name: params.RegPath, Type: params.Key, Value: `\Directory`}, + params.RegData: {Name: params.RegData, Type: params.UnicodeString, Value: "%SYSTEMROOT%"}, + params.RegValueType: {Name: params.RegValueType, Type: params.Enum, Value: uint32(2), Enum: key.RegistryValueTypes}, + params.RegKeyHandle: {Name: params.RegKeyHandle, Type: params.Uint64, Value: uint64(0)}}, + }, + } + }, + func() *handle.SnapshotterMock { + hsnap := new(handle.SnapshotterMock) + return hsnap + }, + func(e *event.Event, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { + assert.Equal(t, `HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Windows\Directory`, e.GetParamAsString(params.RegPath)) + assert.Equal(t, `REG_EXPAND_SZ`, e.GetParamAsString(params.RegValueType)) + assert.Equal(t, `%SYSTEMROOT%`, e.GetParamAsString(params.RegData)) + assert.Equal(t, p.(*registryProcessor).valuesSize(23234), 1) + time.Sleep(time.Millisecond * 500) + assert.Equal(t, p.(*registryProcessor).valuesSize(23234), 0) }, }, } diff --git a/internal/etw/source.go b/internal/etw/source.go index 888d72ee6..347b99160 100644 --- a/internal/etw/source.go +++ b/internal/etw/source.go @@ -22,6 +22,7 @@ import ( "errors" "expvar" "fmt" + "github.com/rabbitstack/fibratus/internal/etw/processors" "github.com/rabbitstack/fibratus/pkg/config" errs "github.com/rabbitstack/fibratus/pkg/errors" "github.com/rabbitstack/fibratus/pkg/event" @@ -34,6 +35,7 @@ import ( log "github.com/sirupsen/logrus" "golang.org/x/sys/windows/registry" "time" + "unsafe" ) const ( @@ -69,9 +71,10 @@ var ( // starting ETW tracing sessions and setting up event // consumers. type EventSource struct { - r *config.RulesCompileResult - traces []*Trace - consumers []*Consumer + r *config.RulesCompileResult + traces []*Trace + consumers []*Consumer + processors processors.Chain errs chan error evts chan *event.Event @@ -96,17 +99,18 @@ func NewEventSource( compiler *config.RulesCompileResult, ) source.EventSource { evs := &EventSource{ - r: compiler, - traces: make([]*Trace, 0), - consumers: make([]*Consumer, 0), - errs: make(chan error, 1000), - evts: make(chan *event.Event, 500), - sequencer: event.NewSequencer(), - config: config, - stop: make(chan struct{}), - psnap: psnap, - hsnap: hsnap, - listeners: make([]event.Listener, 0), + r: compiler, + traces: make([]*Trace, 0), + consumers: make([]*Consumer, 0), + processors: processors.NewChain(psnap, hsnap, config), + errs: make(chan error, 1000), + evts: make(chan *event.Event, 500), + sequencer: event.NewSequencer(), + config: config, + stop: make(chan struct{}), + psnap: psnap, + hsnap: hsnap, + listeners: make([]event.Listener, 0), } return evs } @@ -170,6 +174,20 @@ func (e *EventSource) Open(config *config.Config) error { // from the snapshotter trace.AddProvider(etw.WindowsKernelProcessGUID, false, WithKeywords(etw.ProcessKeyword|etw.ImageKeyword), WithCaptureState()) + // in a similar vein, Windows Kernel Registry provider publishes + // the RegSetValue event with the full captured data for the + // modified value. This data is used to attach various parameters + // to the RegSetValue event published by the NT Kernel Logger + if config.EventSource.EnableRegistryEvents { + // undocumented ETW feature to enable captured data in RegSetValue events + val := 0x2 + eventFilterDescriptor := etw.EventFilterDescriptor{ + Ptr: uintptr(unsafe.Pointer(&val)), + Size: 4, + } + trace.AddProvider(etw.WindowsKernelRegistryGUID, false, WithKeywords(etw.SetValueKeyword), WithEventFilterDescriptors(eventFilterDescriptor)) + } + if config.EventSource.EnableDNSEvents { trace.AddProvider(etw.DNSClientGUID, false) } @@ -228,7 +246,13 @@ func (e *EventSource) Open(config *config.Config) error { } // Init consumer and open the trace for processing - consumer := NewConsumer(e.psnap, e.hsnap, config, e.sequencer, e.evts) + consumer := NewConsumer( + e.psnap, + config, + e.sequencer, + e.evts, + e.processors, + ) consumer.SetFilter(e.filter) // Attach event listeners @@ -245,9 +269,8 @@ func (e *EventSource) Open(config *config.Config) error { log.Infof("starting [%s] trace processing", t.Name) // Instruct the provider to emit state information - err = t.CaptureState() - if err != nil { - log.Warn(err) + if err := t.CaptureState(); err != nil { + log.Warnf("unable to capture trace %s state: %v", t.Name, err) } // Start event processing loop diff --git a/internal/etw/trace.go b/internal/etw/trace.go index 98d7eb31b..e12969b8d 100644 --- a/internal/etw/trace.go +++ b/internal/etw/trace.go @@ -95,13 +95,18 @@ type ProviderInfo struct { // stackExtensions manager stack tracing enablement. // For each event present in the stack identifiers, // the StackWalk event is published by the provider. - stackExtensions *StackExtensions + stackExtensions *StackExtensions + eventFilterDescriptors []etw.EventFilterDescriptor } func (p *ProviderInfo) HasStackExtensions() bool { return p.stackExtensions != nil && !p.stackExtensions.Empty() } +func (p *ProviderInfo) HasEventFilterDescriptors() bool { + return len(p.eventFilterDescriptors) > 0 +} + // Trace is the essential building block for controlling // trace sessions and configuring event consumers. Such // operations include starting, stopping, and flushing @@ -150,9 +155,10 @@ type Trace struct { } type opts struct { - stackexts *StackExtensions - keywords uint64 - captureState bool + stackexts *StackExtensions + keywords uint64 + captureState bool + eventFilterDescriptors []etw.EventFilterDescriptor } // Option represents the option for the trace. @@ -181,6 +187,14 @@ func WithCaptureState() Option { } } +// WithEventFilterDescriptors assigns filters to be passed +// to the provider's enable callback function. +func WithEventFilterDescriptors(descriptors ...etw.EventFilterDescriptor) Option { + return func(o *opts) { + o.eventFilterDescriptors = descriptors + } +} + // NewKernelTrace creates a new NT Kernel Logger trace. func NewKernelTrace(config *config.Config) *Trace { t := &Trace{Name: etw.KernelLoggerSession, GUID: etw.KernelTraceControlGUID, stackExtensions: NewStackExtensions(config.EventSource), config: config} @@ -208,7 +222,7 @@ func (t *Trace) AddProvider(guid windows.GUID, enableStacks bool, options ...Opt t.Providers = append( t.Providers, - ProviderInfo{GUID: guid, Keywords: opts.keywords, EnableStacks: enableStacks, CaptureState: opts.captureState, stackExtensions: opts.stackexts}, + ProviderInfo{GUID: guid, Keywords: opts.keywords, EnableStacks: enableStacks, CaptureState: opts.captureState, stackExtensions: opts.stackexts, eventFilterDescriptors: opts.eventFilterDescriptors}, ) } @@ -325,8 +339,11 @@ func (t *Trace) Start() error { if err := etw.EnableTrace(provider.GUID, t.startHandle, provider.Keywords); err != nil { return err } - case provider.EnableStacks: - opts := etw.EnableTraceOpts{WithStacktrace: true} + case provider.EnableStacks || provider.HasEventFilterDescriptors(): + opts := etw.EnableTraceOpts{ + WithStacktrace: provider.EnableStacks, + EventFilterDescriptors: provider.eventFilterDescriptors, + } if err := etw.EnableTraceWithOpts(provider.GUID, t.startHandle, provider.Keywords, opts); err != nil { return err } diff --git a/pkg/sys/etw/types.go b/pkg/sys/etw/types.go index 5fdba491e..34a9c6c6b 100644 --- a/pkg/sys/etw/types.go +++ b/pkg/sys/etw/types.go @@ -47,6 +47,9 @@ var ThreadpoolGUID = windows.GUID{Data1: 0xc861d0e2, Data2: 0xa2c1, Data3: 0x4d3 // WindowsKernelProcessGUID represents the GUID for the Microsoft Windows Kernel Process provider var WindowsKernelProcessGUID = windows.GUID{Data1: 0x22fb2cd6, Data2: 0x0e7b, Data3: 0x422b, Data4: [8]byte{0xa0, 0xc7, 0x2f, 0xad, 0x1f, 0xd0, 0xe7, 0x16}} +// WindowsKernelRegistryGUID represents the GUID for the Microsoft Windows Kernel Registry provider +var WindowsKernelRegistryGUID = windows.GUID{Data1: 0x70eb4f03, Data2: 0xc1de, Data3: 0x4f73, Data4: [8]byte{0xa0, 0x51, 0x33, 0xd1, 0x3d, 0x54, 0x13, 0xbd}} + const ( // TraceStackTracingInfo controls call stack tracing for kernel events TraceStackTracingInfo = uint8(3) @@ -60,6 +63,9 @@ const ProcessKeyword = 0x10 // ImageKeyword enables images events for Microsoft Windows Kernel Process provider const ImageKeyword = 0x40 +// SetValueKeyword enables registry key value set events for Microsoft Windows Kernel Registry provider +const SetValueKeyword = 0x100 + const ( // EventHeaderExtTypeStackTrace64 indicates that the extended data contains the call stack if the event is captured on a 64-bit host EventHeaderExtTypeStackTrace64 uint16 = 0x0006 From 7fb5a9052cb4b2ec8917538ae86aa6f840fe46b7 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Tue, 22 Jul 2025 19:00:27 +0200 Subject: [PATCH 32/42] refactor(yara): Use the new registry data parameter name --- pkg/yara/scanner.go | 2 +- pkg/yara/scanner_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/yara/scanner.go b/pkg/yara/scanner.go index fb3bf21bd..513ea64ce 100644 --- a/pkg/yara/scanner.go +++ b/pkg/yara/scanner.go @@ -334,7 +334,7 @@ func (s scanner) Scan(e *event.Event) (bool, error) { if typ := e.Params.TryGetUint32(params.RegValueType); typ != registry.BINARY { return false, nil } - v, err := e.Params.Get(params.RegValue) + v, err := e.Params.Get(params.RegData) if err != nil { // value not attached to the event return false, nil diff --git a/pkg/yara/scanner_test.go b/pkg/yara/scanner_test.go index 80f0d3a36..500a9df1d 100644 --- a/pkg/yara/scanner_test.go +++ b/pkg/yara/scanner_test.go @@ -933,7 +933,7 @@ func TestScan(t *testing.T) { PID: 565, Params: event.Params{ params.RegValueType: {Name: params.RegValueType, Type: params.Uint32, Value: uint32(registry.BINARY)}, - params.RegValue: {Name: params.RegValue, Type: params.Binary, Value: data}, + params.RegData: {Name: params.RegValue, Type: params.Binary, Value: data}, params.RegPath: {Name: params.RegPath, Type: params.UnicodeString, Value: `HKEY_LOCAL_MACHINE\CurrentControlSet\Control\DeviceGuard\Mal`}, }, Metadata: make(map[event.MetadataKey]any), From e0e4541d579f9ab6e16eb621b95a14ab25638108 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Tue, 22 Jul 2025 19:18:54 +0200 Subject: [PATCH 33/42] refactor(filter): Change registry.value filter field semantics The registry.value filter field yields the name of the created or modified registry value. The new registry.data field returns the captured value data. --- pkg/filter/accessor_windows.go | 7 ++++++- pkg/filter/fields/fields_windows.go | 9 ++++++--- pkg/filter/filter_test.go | 5 +++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/pkg/filter/accessor_windows.go b/pkg/filter/accessor_windows.go index e3069ee59..d2ea35fd6 100644 --- a/pkg/filter/accessor_windows.go +++ b/pkg/filter/accessor_windows.go @@ -940,9 +940,14 @@ func (r *registryAccessor) Get(f Field, e *event.Event) (params.Value, error) { case fields.RegistryKeyHandle: return e.GetParamAsString(params.RegKeyHandle), nil case fields.RegistryValue: - return e.Params.GetRaw(params.RegValue) + if e.IsRegSetValue() { + return filepath.Base(filepath.Base(e.GetParamAsString(params.RegPath))), nil + } + return nil, nil case fields.RegistryValueType: return e.Params.GetString(params.RegValueType) + case fields.RegistryData: + return e.GetParamAsString(params.RegData), nil case fields.RegistryStatus: return e.GetParamAsString(params.NTStatus), nil } diff --git a/pkg/filter/fields/fields_windows.go b/pkg/filter/fields/fields_windows.go index 4c50c826f..84904fe02 100644 --- a/pkg/filter/fields/fields_windows.go +++ b/pkg/filter/fields/fields_windows.go @@ -479,10 +479,12 @@ const ( RegistryKeyName Field = "registry.key.name" // RegistryKeyHandle represents the registry KCB address RegistryKeyHandle Field = "registry.key.handle" - // RegistryValue represents the registry value + // RegistryValue represents the registry value name field RegistryValue Field = "registry.value" - // RegistryValueType represents the registry value type + // RegistryValueType represents the registry value type field RegistryValueType Field = "registry.value.type" + // RegistryData represents the captured registry data field + RegistryData Field = "registry.data" // RegistryStatus represent the registry operation status RegistryStatus Field = "registry.status" @@ -1000,8 +1002,9 @@ var fields = map[Field]FieldInfo{ RegistryPath: {RegistryPath, "fully qualified registry path", params.UnicodeString, []string{"registry.path = 'HKEY_LOCAL_MACHINE\\SYSTEM'"}, nil, nil}, RegistryKeyName: {RegistryKeyName, "registry key name", params.UnicodeString, []string{"registry.key.name = 'CurrentControlSet'"}, nil, nil}, RegistryKeyHandle: {RegistryKeyHandle, "registry key object address", params.Address, []string{"registry.key.handle = 'FFFFB905D60C2268'"}, nil, nil}, - RegistryValue: {RegistryValue, "registry value content", params.UnicodeString, []string{"registry.value = '%SystemRoot%\\system32'"}, nil, nil}, + RegistryValue: {RegistryValue, "registry value name", params.UnicodeString, []string{"registry.value = 'Epoch'"}, nil, nil}, RegistryValueType: {RegistryValueType, "type of registry value", params.UnicodeString, []string{"registry.value.type = 'REG_SZ'"}, nil, nil}, + RegistryData: {RegistryData, "registry value captured data", params.Object, []string{"registry.data = '%SystemRoot%'"}, nil, nil}, RegistryStatus: {RegistryStatus, "status of registry operation", params.UnicodeString, []string{"registry.status != 'success'"}, nil, nil}, NetDIP: {NetDIP, "destination IP address", params.IP, []string{"net.dip = 172.17.0.3"}, nil, nil}, diff --git a/pkg/filter/filter_test.go b/pkg/filter/filter_test.go index a105f25d7..2b46669fc 100644 --- a/pkg/filter/filter_test.go +++ b/pkg/filter/filter_test.go @@ -863,7 +863,7 @@ func TestRegistryFilter(t *testing.T) { Category: event.Registry, Params: event.Params{ params.RegPath: {Name: params.RegPath, Type: params.UnicodeString, Value: `HKEY_LOCAL_MACHINE\SYSTEM\Setup\Pid`}, - params.RegValue: {Name: params.RegValue, Type: params.Uint32, Value: uint32(10234)}, + params.RegData: {Name: params.RegData, Type: params.Uint32, Value: uint32(10234)}, params.RegValueType: {Name: params.RegValueType, Type: params.AnsiString, Value: "DWORD"}, params.NTStatus: {Name: params.NTStatus, Type: params.AnsiString, Value: "success"}, params.RegKeyHandle: {Name: params.RegKeyHandle, Type: params.Address, Value: uint64(18446666033449935464)}, @@ -878,8 +878,9 @@ func TestRegistryFilter(t *testing.T) { {`registry.status startswith ('key not', 'succ')`, true}, {`registry.path = 'HKEY_LOCAL_MACHINE\\SYSTEM\\Setup\\Pid'`, true}, {`registry.key.name icontains ('Setup', 'setup')`, true}, - {`registry.value = 10234`, true}, + {`registry.value = 'Pid'`, true}, {`registry.value.type in ('DWORD', 'QWORD')`, true}, + {`registry.data = '10234'`, true}, {`MD5(registry.path) = 'eab870b2a516206575d2ffa2b98d8af5'`, true}, } From 64ac06a2cfae7882095c72bbdfc58837e94260e4 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Tue, 29 Jul 2025 19:39:54 +0200 Subject: [PATCH 34/42] feat(config,rules): Add authors field in rule definition The new authors field allows specifying one or more authors of the detection rule. --- pkg/config/_fixtures/filters/default.yml | 3 +++ pkg/config/filters.go | 1 + pkg/config/filters_test.go | 1 + pkg/config/schema_windows.go | 1 + 4 files changed, 6 insertions(+) diff --git a/pkg/config/_fixtures/filters/default.yml b/pkg/config/_fixtures/filters/default.yml index 58c988c68..601d65498 100644 --- a/pkg/config/_fixtures/filters/default.yml +++ b/pkg/config/_fixtures/filters/default.yml @@ -23,3 +23,6 @@ notes: | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut ut ante id ligula molestie varius. Duis efficitur eros quis turpis accumsan, nec scelerisque libero euismod. +authors: + - rabbitstack + - skynova diff --git a/pkg/config/filters.go b/pkg/config/filters.go index c89fd0a11..6c856cd6c 100644 --- a/pkg/config/filters.go +++ b/pkg/config/filters.go @@ -55,6 +55,7 @@ type FilterConfig struct { Notes string `json:"notes" yaml:"notes"` MinEngineVersion string `json:"min-engine-version" yaml:"min-engine-version"` Enabled *bool `json:"enabled" yaml:"enabled"` + Authors []string `json:"authors" yaml:"authors"` } // FilterAction wraps all possible filter actions. diff --git a/pkg/config/filters_test.go b/pkg/config/filters_test.go index 0e036b71e..c7018085e 100644 --- a/pkg/config/filters_test.go +++ b/pkg/config/filters_test.go @@ -58,6 +58,7 @@ func TestLoadRulesFromPaths(t *testing.T) { assert.NotNil(t, f1.Action) assert.Contains(t, f1.References, "ref2") assert.NotEmpty(t, f1.Notes) + assert.Len(t, f1.Authors, 2) acts, err := f1.DecodeActions() require.NoError(t, err) diff --git a/pkg/config/schema_windows.go b/pkg/config/schema_windows.go index 69f0b2b0a..ae19d93eb 100644 --- a/pkg/config/schema_windows.go +++ b/pkg/config/schema_windows.go @@ -524,6 +524,7 @@ var rulesSchema = ` }, "tags": {"type": "array", "items": [{"type": "string", "minLength": 1}]}, "references": {"type": "array", "items": [{"type": "string", "minLength": 1}]}, + "authors": {"type": "array", "items": [{"type": "string", "minLength": 1}]}, "action": { "type": "array", "items": { From bb3e60cecebedb3a4387a3443bae9899d3600429 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Wed, 30 Jul 2025 19:11:20 +0200 Subject: [PATCH 35/42] refactor(config): Move JSON schema definitions to files --- pkg/config/config.schema.json | 1133 +++++++++++++++++++++++++++++++++ pkg/config/config_windows.go | 4 +- pkg/config/macros.schema.json | 47 ++ pkg/config/rules.schema.json | 143 +++++ pkg/config/schema_windows.go | 586 +---------------- pkg/config/validation_test.go | 2 +- 6 files changed, 1333 insertions(+), 582 deletions(-) create mode 100644 pkg/config/config.schema.json create mode 100644 pkg/config/macros.schema.json create mode 100644 pkg/config/rules.schema.json diff --git a/pkg/config/config.schema.json b/pkg/config/config.schema.json new file mode 100644 index 000000000..4fd9b9ae1 --- /dev/null +++ b/pkg/config/config.schema.json @@ -0,0 +1,1133 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "yara": { + "$id": "#yara", + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + } + } + }, + "type": "object", + "properties": { + "aggregator": { + "type": "object", + "properties": { + "flush-period": { + "type": "string", + "minLength": 2, + "pattern": "[0-9]+ms|s" + }, + "flush-timeout": { + "type": "string", + "minLength": 2, + "pattern": "[0-9]+s" + } + }, + "additionalProperties": false + }, + "alertsenders": { + "type": "object", + "anyOf": [ + { + "properties": { + "mail": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "host": { + "type": "string" + }, + "port": { + "type": "number" + }, + "user": { + "type": "string" + }, + "password": { + "type": "string" + }, + "from": { + "type": "string" + }, + "to": { + "type": "array", + "items": { + "type": "string", + "format": "email" + } + }, + "content-type": { + "type": "string" + }, + "use-template": { + "type": "boolean" + } + }, + "if": { + "properties": { + "enabled": { + "const": true + } + } + }, + "then": { + "properties": { + "from": { + "type": "string", + "format": "email" + }, + "to": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "format": "email" + } + } + } + }, + "additionalProperties": false + }, + "slack": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "url": { + "type": "string" + }, + "workspace": { + "type": "string" + }, + "channel": { + "type": "string" + }, + "emoji": { + "type": "string" + } + }, + "if": { + "properties": { + "enabled": { + "const": true + } + } + }, + "then": { + "properties": { + "url": { + "type": "string", + "format": "uri", + "minLength": 1, + "pattern": "^(https?|http?)://" + } + } + }, + "additionalProperties": false + }, + "systray": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "sound": { + "type": "boolean" + }, + "quiet-mode": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "eventlog": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "verbose": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "api": { + "type": "object", + "properties": { + "transport": { + "type": "string", + "minLength": 3 + }, + "timeout": { + "type": "string", + "minLength": 2, + "pattern": "[0-9]+s" + } + }, + "additionalProperties": false + }, + "config-file": { + "type": "string" + }, + "debug-privilege": { + "type": "boolean" + }, + "forward": { + "type": "boolean" + }, + "symbol-paths": { + "type": "string" + }, + "symbolize-kernel-addresses": { + "type": "boolean" + }, + "handle": { + "type": "object", + "properties": { + "init-snapshot": { + "type": "boolean" + }, + "enumerate-handles": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "cap": { + "type": "object", + "properties": { + "file": { + "type": "string" + } + }, + "additionalProperties": false + }, + "filament": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "flush-period": { + "type": "string", + "minLength": 2, + "pattern": "[0-9]+ms|s" + } + }, + "additionalProperties": false + }, + "filters": { + "type": "object", + "properties": { + "match-all": { + "type": "boolean" + }, + "rules": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "from-paths": { + "type": [ + "array", + "null" + ], + "items": [ + { + "type": "string", + "minLength": 4 + } + ] + }, + "from-urls": { + "type": [ + "array", + "null" + ], + "items": [ + { + "type": "string", + "minLength": 8 + } + ] + } + }, + "additionalProperties": false + }, + "macros": { + "type": "object", + "properties": { + "from-paths": { + "type": [ + "array", + "null" + ], + "items": [ + { + "type": "string", + "minLength": 4 + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "event": { + "type": "object", + "properties": { + "serialize-threads": { + "type": "boolean" + }, + "serialize-images": { + "type": "boolean" + }, + "serialize-handles": { + "type": "boolean" + }, + "serialize-pe": { + "type": "boolean" + }, + "serialize-envs": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "eventsource": { + "type": "object", + "properties": { + "enable-thread": { + "type": "boolean" + }, + "enable-image": { + "type": "boolean" + }, + "enable-registry": { + "type": "boolean" + }, + "enable-fileio": { + "type": "boolean" + }, + "enable-vamap": { + "type": "boolean" + }, + "enable-handle": { + "type": "boolean" + }, + "enable-net": { + "type": "boolean" + }, + "enable-mem": { + "type": "boolean" + }, + "enable-audit-api": { + "type": "boolean" + }, + "enable-dns": { + "type": "boolean" + }, + "enable-threadpool": { + "type": "boolean" + }, + "stack-enrichment": { + "type": "boolean" + }, + "min-buffers": { + "type": "integer", + "minimum": 1 + }, + "max-buffers": { + "type": "integer", + "minimum": 2 + }, + "buffer-size": { + "type": "integer" + }, + "flush-interval": { + "type": "string", + "minLength": 2, + "pattern": "[0-9]+s" + }, + "blacklist": { + "type": "object", + "properties": { + "events": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "CreateThread", + "TerminateThread", + "OpenProcess", + "OpenThread", + "SetThreadContext", + "LoadImage", + "UnloadImage", + "CreateFile", + "CloseFile", + "ReadFile", + "WriteFile", + "DeleteFile", + "RenameFile", + "SetFileInformation", + "EnumDirectory", + "MapViewFile", + "UnmapViewFile", + "RegCreateKey", + "RegOpenKey", + "RegSetValue", + "RegQueryValue", + "RegQueryKey", + "RegDeleteKey", + "RegDeleteValue", + "RegCloseKey", + "Accept", + "Send", + "Recv", + "Connect", + "Disconnect", + "Reconnect", + "Retransmit", + "CreateHandle", + "CloseHandle", + "DuplicateHandle", + "QueryDns", + "ReplyDns", + "VirtualAlloc", + "VirtualFree", + "CreateSymbolicLinkObject", + "SubmitThreadpoolWork", + "SubmitThreadpoolCallback", + "SetThreadpoolTimer" + ] + } + }, + "images": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "logging": { + "type": "object", + "properties": { + "level": { + "type": "string" + }, + "max-age": { + "type": "integer" + }, + "max-backups": { + "type": "integer", + "minimum": 1 + }, + "max-size": { + "type": "integer", + "minimum": 1 + }, + "formatter": { + "type": "string", + "enum": [ + "json", + "text" + ] + }, + "path": { + "type": "string" + }, + "log-stdout": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "output": { + "type": "object", + "anyOf": [ + { + "properties": { + "console": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "format": { + "type": "string", + "enum": [ + "json", + "pretty" + ] + }, + "template": { + "type": "string" + }, + "kv-delimiter": { + "type": "string" + } + }, + "additionalProperties": false + }, + "elasticsearch": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "servers": { + "type": "array", + "items": [ + { + "type": "string", + "minItems": 1, + "format": "uri", + "minLength": 1, + "maxLength": 255, + "pattern": "^(https?|http?)://" + } + ] + }, + "timeout": { + "type": "string" + }, + "index-name": { + "type": "string", + "minLength": 1 + }, + "template-config": { + "type": "string" + }, + "template-name": { + "type": "string", + "minLength": 1 + }, + "healthcheck": { + "type": "boolean" + }, + "bulk-workers": { + "type": "integer", + "minimum": 1 + }, + "sniff": { + "type": "boolean" + }, + "trace-log": { + "type": "boolean" + }, + "gzip-compression": { + "type": "boolean" + }, + "healthcheck-interval": { + "type": "string", + "minLength": 2, + "pattern": "[0-9]+s|m}" + }, + "healthcheck-timeout": { + "type": "string", + "minLength": 2, + "pattern": "[0-9]+s|m}" + }, + "flush-period": { + "type": "string", + "minLength": 2, + "pattern": "[0-9]+s|m}" + }, + "username": { + "type": "string" + }, + "password": { + "type": "string" + }, + "tls-key": { + "type": "string" + }, + "tls-cert": { + "type": "string" + }, + "tls-ca": { + "type": "string" + }, + "tls-insecure-skip-verify": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "amqp": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "url": { + "type": "string", + "format": "uri", + "minLength": 1, + "maxLength": 255, + "pattern": "^(amqps?|amqp?)://" + }, + "timeout": { + "type": "string", + "minLength": 2, + "pattern": "[0-9]+s|m}" + }, + "exchange": { + "type": "string", + "minLength": 1 + }, + "exchange-type": { + "type": "string", + "enum": [ + "direct", + "topic", + "fanout", + "header", + "x-consistent-hash" + ] + }, + "routing-key": { + "type": "string", + "minLength": 1 + }, + "delivery-mode": { + "type": "string", + "enum": [ + "transient", + "persistent" + ] + }, + "vhost": { + "type": "string", + "minLength": 1 + }, + "passive": { + "type": "boolean" + }, + "durable": { + "type": "boolean" + }, + "username": { + "type": "string" + }, + "password": { + "type": "string" + }, + "tls-key": { + "type": "string" + }, + "tls-cert": { + "type": "string" + }, + "tls-ca": { + "type": "string" + }, + "tls-insecure-skip-verify": { + "type": "boolean" + }, + "headers": { + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": false + }, + "http": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "endpoints": { + "type": "array", + "items": [ + { + "type": "string", + "minItems": 1, + "format": "uri", + "minLength": 1, + "maxLength": 255, + "pattern": "^(https?|http?)://" + } + ] + }, + "timeout": { + "type": "string", + "minLength": 2, + "pattern": "[0-9]+s|m}" + }, + "method": { + "type": "string", + "enum": [ + "POST", + "PUT" + ] + }, + "serializer": { + "type": "string", + "enum": [ + "json" + ] + }, + "enable-gzip": { + "type": "boolean" + }, + "proxy-url": { + "type": "string" + }, + "proxy-username": { + "type": "string" + }, + "proxy-password": { + "type": "string" + }, + "username": { + "type": "string" + }, + "password": { + "type": "string" + }, + "tls-key": { + "type": "string" + }, + "tls-cert": { + "type": "string" + }, + "tls-ca": { + "type": "string" + }, + "tls-insecure-skip-verify": { + "type": "boolean" + }, + "headers": { + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": false + }, + "eventlog": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "level": { + "type": "string", + "enum": [ + "INFO", + "info", + "warn", + "warning", + "WARN", + "WARNING", + "error", + "erro", + "ERROR", + "ERRO" + ] + }, + "remote-host": { + "type": "string" + }, + "template": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "pe": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "read-resources": { + "type": "boolean" + }, + "read-symbols": { + "type": "boolean" + }, + "read-sections": { + "type": "boolean" + }, + "excluded-images": { + "type": "array", + "items": [ + { + "type": "string" + } + ] + } + }, + "additionalProperties": false + }, + "transformers": { + "type": "object", + "anyOf": [ + { + "properties": { + "remove": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "params": { + "type": "array", + "items": [ + { + "type": "string" + } + ] + } + }, + "if": { + "properties": { + "enabled": { + "const": true + } + } + }, + "then": { + "properties": { + "params": { + "type": "array", + "minItems": 1, + "items": [ + { + "type": "string" + } + ] + } + } + }, + "additionalProperties": false + }, + "rename": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "params": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "old": { + "type": "string", + "minLength": 1 + }, + "new": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false + } + ] + } + }, + "if": { + "properties": { + "enabled": { + "const": true + } + } + }, + "then": { + "properties": { + "params": { + "minItems": 1 + } + } + }, + "additionalProperties": false + }, + "replace": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "replacements": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "param": { + "type": "string", + "minLength": 1 + }, + "old": { + "type": "string", + "minLength": 1 + }, + "new": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + }, + "if": { + "properties": { + "enabled": { + "const": true + } + } + }, + "then": { + "properties": { + "replacements": { + "minItems": 1 + } + } + }, + "additionalProperties": false + }, + "tags": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "tags": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1 + }, + "value": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false + } + ] + } + }, + "if": { + "properties": { + "enabled": { + "const": true + } + } + }, + "then": { + "properties": { + "tags": { + "minItems": 1 + } + } + }, + "additionalProperties": false + }, + "trim": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "prefixes": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "param": { + "type": "string", + "minLength": 1 + }, + "trim": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false + } + ] + }, + "suffixes": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "param": { + "type": "string", + "minLength": 1 + }, + "trim": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false + } + ] + } + }, + "if": { + "properties": { + "enabled": { + "const": true + } + } + }, + "then": { + "properties": { + "suffixes": { + "minItems": 1 + }, + "prefixes": { + "minItems": 1 + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "yara": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "rule": { + "type": "object", + "anyOf": [ + { + "properties": { + "paths": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "namespace": { + "type": "string" + } + }, + "if": { + "properties": { + "enabled": { + "$ref": "#yara", + "const": true + } + } + }, + "then": { + "properties": { + "path": { + "minLength": 0 + } + } + }, + "additionalProperties": false + } + ] + }, + "strings": { + "type": "array" + } + }, + "additionalProperties": false + } + ] + }, + "alert-template": { + "type": "string" + }, + "fastscan": { + "type": "boolean" + }, + "skip-files": { + "type": "boolean" + }, + "skip-allocs": { + "type": "boolean" + }, + "skip-mmaps": { + "type": "boolean" + }, + "skip-registry": { + "type": "boolean" + }, + "scan-timeout": { + "type": "string", + "minLength": 2, + "pattern": "[0-9]+s" + }, + "excluded-files": { + "type": "array", + "items": [ + { + "type": "string", + "minLength": 1 + } + ] + }, + "excluded-procs": { + "type": "array", + "items": [ + { + "type": "string", + "minLength": 1 + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} \ No newline at end of file diff --git a/pkg/config/config_windows.go b/pkg/config/config_windows.go index 3d51cb0c0..2ca5cca84 100644 --- a/pkg/config/config_windows.go +++ b/pkg/config/config_windows.go @@ -338,12 +338,12 @@ func (c *Config) Validate() error { return fmt.Errorf("couldn't read the config file: %v", err) } // validate config file content - valid, errs := validate(interpolateSchema(), out) + valid, errs := validate(configSchema, out) if !valid || len(errs) > 0 { return fmt.Errorf("invalid config: %v", multierror.Wrap(errs...)) } // now validate the Viper config flags - valid, errs = validate(interpolateSchema(), c.viper.AllSettings()) + valid, errs = validate(configSchema, c.viper.AllSettings()) if !valid || len(errs) > 0 { return fmt.Errorf("invalid config: %v", multierror.Wrap(errs...)) } diff --git a/pkg/config/macros.schema.json b/pkg/config/macros.schema.json new file mode 100644 index 000000000..e86182006 --- /dev/null +++ b/pkg/config/macros.schema.json @@ -0,0 +1,47 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "array", + "items": { + "type": "object", + "properties": { + "macro": { + "type": "string", + "minLength": 2, + "pattern": "^[A-Za-z0-9_-]+$" + }, + "description": { + "type": "string" + }, + "expr": { + "type": "string", + "minLength": 5 + }, + "list": { + "type": "array", + "items": [ + { + "type": "string", + "minLength": 1 + } + ] + } + }, + "required": [ + "macro" + ], + "oneOf": [ + { + "required": [ + "expr" + ] + }, + { + "required": [ + "list" + ] + } + ], + "additionalProperties": false + }, + "additionalProperties": false +} diff --git a/pkg/config/rules.schema.json b/pkg/config/rules.schema.json new file mode 100644 index 000000000..c8c7648b8 --- /dev/null +++ b/pkg/config/rules.schema.json @@ -0,0 +1,143 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 36, + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + }, + "version": { + "type": "string", + "minLength": 5, + "pattern": "^([0-9]+.)([0-9]+.)([0-9]+)$" + }, + "name": { + "type": "string", + "minLength": 3 + }, + "description": { + "type": "string" + }, + "output": { + "type": "string", + "minLength": 5 + }, + "notes": { + "type": "string" + }, + "severity": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "critical" + ] + }, + "min-engine-version": { + "type": "string", + "minLength": 5, + "pattern": "^([0-9]+.)([0-9]+.)([0-9]+)$" + }, + "enabled": { + "type": "boolean" + }, + "condition": { + "type": "string", + "minLength": 3 + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "tags": { + "type": "array", + "items": [ + { + "type": "string", + "minLength": 1 + } + ] + }, + "references": { + "type": "array", + "items": [ + { + "type": "string", + "minLength": 1 + } + ] + }, + "authors": { + "type": "array", + "items": [ + { + "type": "string", + "minLength": 1 + } + ] + }, + "action": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "enum": [ + "kill", + "isolate" + ] + }, + "whitelist": true + }, + "required": [ + "name" + ], + "if": { + "properties": { + "name": { + "const": "isolate" + } + } + }, + "then": { + "properties": { + "whitelist": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "format": "ipv4" + } + } + } + }, + "else": { + "properties": { + "name": { + "type": "string", + "enum": [ + "kill", + "isolate" + ] + } + }, + "additionalProperties": false + } + } + } + }, + "required": [ + "id", + "version", + "name", + "condition", + "min-engine-version" + ], + "additionalProperties": false +} diff --git a/pkg/config/schema_windows.go b/pkg/config/schema_windows.go index ae19d93eb..a18907cc7 100644 --- a/pkg/config/schema_windows.go +++ b/pkg/config/schema_windows.go @@ -19,586 +19,14 @@ package config import ( - "bytes" - "text/template" + _ "embed" ) -var schema = ` -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": {"yara": {"$id": "#yara", "type": "object", "properties": {"enabled": {"type": "boolean"}}}}, +//go:embed config.schema.json +var configSchema string - "type": "object", - "properties": { - "aggregator": { - "type": "object", - "properties": { - "flush-period": {"type": "string", "minLength": 2, "pattern": "[0-9]+ms|s"}, - "flush-timeout": {"type": "string", "minLength": 2, "pattern": "[0-9]+s"} - }, - "additionalProperties": false - }, - "alertsenders": { - "type": "object", - "anyOf": [{ - "properties": { - "mail": { - "type": "object", - "properties": { - "enabled": {"type": "boolean"}, - "host": {"type": "string"}, - "port": {"type": "number"}, - "user": {"type": "string"}, - "password": {"type": "string"}, - "from": {"type": "string"}, - "to": {"type": "array", "items": {"type": "string", "format": "email"}}, - "content-type": {"type": "string"}, - "use-template": {"type": "boolean"} - }, - "if": { - "properties": {"enabled": { "const": true }} - }, - "then": { - "properties": { - "from": {"type": "string", "format": "email"}, - "to": {"type": "array", "minItems": 1, "items": {"type": "string", "format": "email"}} - } - }, - "additionalProperties": false - }, - "slack": { - "type": "object", - "properties": { - "enabled": {"type": "boolean"}, - "url": {"type": "string"}, - "workspace": {"type": "string"}, - "channel": {"type": "string"}, - "emoji": {"type": "string"} - }, - "if": { - "properties": {"enabled": { "const": true }} - }, - "then": { - "properties": {"url": {"type": "string", "format": "uri", "minLength": 1, "pattern": "^(https?|http?)://"}} - }, - "additionalProperties": false - }, - "systray": { - "type": "object", - "properties": { - "enabled": {"type": "boolean"}, - "sound": {"type": "boolean"}, - "quiet-mode": {"type": "boolean"} - }, - "additionalProperties": false +//go:embed rules.schema.json +var rulesSchema string - }, - "eventlog": { - "type": "object", - "properties": { - "enabled": {"type": "boolean"}, - "verbose": {"type": "boolean"} - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "api": { - "type": "object", - "properties": { - "transport": {"type": "string", "minLength": 3}, - "timeout": {"type": "string", "minLength": 2, "pattern": "[0-9]+s"} - }, - "additionalProperties": false - }, - "config-file": {"type": "string"}, - "debug-privilege": {"type": "boolean"}, - "forward": {"type": "boolean"}, - "symbol-paths": {"type": "string"}, - "symbolize-kernel-addresses": {"type": "boolean"}, - "handle": { - "type": "object", - "properties": { - "init-snapshot": {"type": "boolean"}, - "enumerate-handles": {"type": "boolean"} - }, - "additionalProperties": false - }, - "cap": { - "type": "object", - "properties": { - "file": {"type": "string"} - }, - "additionalProperties": false - }, - "filament": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "path": {"type": "string"}, - "flush-period": {"type": "string", "minLength": 2, "pattern": "[0-9]+ms|s"} - }, - "additionalProperties": false - }, - "filters": { - "type": "object", - "properties": { - "match-all": {"type": "boolean"}, - "rules": { - "type": "object", - "properties": { - "enabled": {"type": "boolean"}, - "from-paths": {"type": ["array", "null"], "items": [{"type": "string", "minLength": 4}]}, - "from-urls": {"type": ["array", "null"], "items": [{"type": "string", "minLength": 8}]} - }, - "additionalProperties": false - }, - "macros": { - "type": "object", - "properties": { - "from-paths": {"type": ["array", "null"], "items": [{"type": "string", "minLength": 4}]} - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - "event": { - "type": "object", - "properties": { - "serialize-threads": {"type": "boolean"}, - "serialize-images": {"type": "boolean"}, - "serialize-handles": {"type": "boolean"}, - "serialize-pe": {"type": "boolean"}, - "serialize-envs": {"type": "boolean"} - }, - "additionalProperties": false - }, - "eventsource": { - "type": "object", - "properties": { - "enable-thread": {"type": "boolean"}, - "enable-image": {"type": "boolean"}, - "enable-registry": {"type": "boolean"}, - "enable-fileio": {"type": "boolean"}, - "enable-vamap": {"type": "boolean"}, - "enable-handle": {"type": "boolean"}, - "enable-net": {"type": "boolean"}, - "enable-mem": {"type": "boolean"}, - "enable-audit-api": {"type": "boolean"}, - "enable-dns": {"type": "boolean"}, - "enable-threadpool": {"type": "boolean"}, - "stack-enrichment": {"type": "boolean"}, - "min-buffers": {"type": "integer", "minimum": 1, "maximum": {{ .MinBuffers }}}, - "max-buffers": {"type": "integer", "minimum": 2, "maximum": {{ .MaxBuffers }}}, - "buffer-size": {"type": "integer", "maximum": {{ .MaxBufferSize }}}, - "flush-interval": {"type": "string", "minLength": 2, "pattern": "[0-9]+s"}, - "blacklist": { - "type": "object", - "properties": { - "events": {"type": "array", "items": {"type": "string", "enum": ["CreateThread", "TerminateThread", "OpenProcess", "OpenThread", "SetThreadContext", "LoadImage", "UnloadImage", "CreateFile", "CloseFile", "ReadFile", "WriteFile", "DeleteFile", "RenameFile", "SetFileInformation", "EnumDirectory", "MapViewFile", "UnmapViewFile", "RegCreateKey", "RegOpenKey", "RegSetValue", "RegQueryValue", "RegQueryKey", "RegDeleteKey", "RegDeleteValue", "RegCloseKey", "Accept", "Send", "Recv", "Connect", "Disconnect", "Reconnect", "Retransmit", "CreateHandle", "CloseHandle", "DuplicateHandle", "QueryDns", "ReplyDns", "VirtualAlloc", "VirtualFree", "CreateSymbolicLinkObject", "SubmitThreadpoolWork", "SubmitThreadpoolCallback", "SetThreadpoolTimer"]}}, - "images": {"type": "array", "items": {"type": "string", "minLength": 1}} - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - "logging": { - "type": "object", - "properties": { - "level": {"type": "string"}, - "max-age": {"type": "integer"}, - "max-backups": {"type": "integer", "minimum": 1}, - "max-size": {"type": "integer", "minimum": 1}, - "formatter": {"type": "string", "enum": ["json", "text"]}, - "path": {"type": "string"}, - "log-stdout": {"type": "boolean"} - }, - "additionalProperties": false - }, - "output": { - "type": "object", - "anyOf": [{ - "properties": { - "console": { - "type": "object", - "properties": { - "enabled": {"type": "boolean"}, - "format": {"type": "string", "enum": ["json", "pretty"]}, - "template": {"type": "string"}, - "kv-delimiter": {"type": "string"} - }, - "additionalProperties": false - }, - "elasticsearch": { - "type": "object", - "properties": { - "enabled": {"type": "boolean"}, - "servers": {"type": "array", "items": [{"type": "string", "minItems": 1, "format": "uri", "minLength": 1, "maxLength": 255, "pattern": "^(https?|http?)://"}]}, - "timeout": {"type": "string"}, - "index-name": {"type": "string", "minLength": 1}, - "template-config": {"type": "string"}, - "template-name": {"type": "string", "minLength": 1}, - "healthcheck": {"type": "boolean"}, - "bulk-workers": {"type": "integer", "minimum": 1}, - "sniff": {"type": "boolean"}, - "trace-log": {"type": "boolean"}, - "gzip-compression": {"type": "boolean"}, - "healthcheck-interval": {"type": "string", "minLength": 2, "pattern": "[0-9]+s|m}"}, - "healthcheck-timeout": {"type": "string", "minLength": 2, "pattern": "[0-9]+s|m}"}, - "flush-period": {"type": "string", "minLength": 2, "pattern": "[0-9]+s|m}"}, - "username": {"type": "string"}, - "password": {"type": "string"}, - "tls-key": {"type": "string"}, - "tls-cert": {"type": "string"}, - "tls-ca": {"type": "string"}, - "tls-insecure-skip-verify": {"type": "boolean"} - }, - "additionalProperties": false - }, - "amqp": { - "type": "object", - "properties": { - "enabled": {"type": "boolean"}, - "url": {"type": "string", "format": "uri", "minLength": 1, "maxLength": 255, "pattern": "^(amqps?|amqp?)://"}, - "timeout": {"type": "string", "minLength": 2, "pattern": "[0-9]+s|m}"}, - "exchange": {"type": "string", "minLength": 1}, - "exchange-type": {"type": "string", "enum": ["direct", "topic", "fanout", "header", "x-consistent-hash"]}, - "routing-key": {"type": "string", "minLength": 1}, - "delivery-mode": {"type": "string", "enum": ["transient", "persistent"]}, - "vhost": {"type": "string", "minLength": 1}, - "passive": {"type": "boolean"}, - "durable": {"type": "boolean"}, - "username": {"type": "string"}, - "password": {"type": "string"}, - "tls-key": {"type": "string"}, - "tls-cert": {"type": "string"}, - "tls-ca": {"type": "string"}, - "tls-insecure-skip-verify": {"type": "boolean"}, - "headers": {"type": "object", "additionalProperties": true} - }, - "additionalProperties": false - }, - "http": { - "type": "object", - "properties": { - "enabled": {"type": "boolean"}, - "endpoints": {"type": "array", "items": [{"type": "string", "minItems": 1, "format": "uri", "minLength": 1, "maxLength": 255, "pattern": "^(https?|http?)://"}]}, - "timeout": {"type": "string", "minLength": 2, "pattern": "[0-9]+s|m}"}, - "method": {"type": "string", "enum": ["POST", "PUT"]}, - "serializer": {"type": "string", "enum": ["json"]}, - "enable-gzip": {"type": "boolean"}, - "proxy-url": {"type": "string"}, - "proxy-username": {"type": "string"}, - "proxy-password": {"type": "string"}, - "username": {"type": "string"}, - "password": {"type": "string"}, - "tls-key": {"type": "string"}, - "tls-cert": {"type": "string"}, - "tls-ca": {"type": "string"}, - "tls-insecure-skip-verify": {"type": "boolean"}, - "headers": {"type": "object", "additionalProperties": true} - }, - "additionalProperties": false - }, - "eventlog": { - "type": "object", - "properties": { - "enabled": {"type": "boolean"}, - "level": {"type": "string", "enum": ["INFO", "info", "warn", "warning", "WARN", "WARNING", "error", "erro", "ERROR", "ERRO"]}, - "remote-host": {"type": "string"}, - "template": {"type": "string"} - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "pe": { - "type": "object", - "properties": { - "enabled": {"type": "boolean"}, - "read-resources": {"type": "boolean"}, - "read-symbols": {"type": "boolean"}, - "read-sections": {"type": "boolean"}, - "excluded-images": {"type": "array", "items": [{"type": "string"}]} - }, - "additionalProperties": false - }, - "transformers": { - "type": "object", - "anyOf": [{ - "properties": { - "remove": { - "type": "object", - "properties": { - "enabled": {"type": "boolean"}, - "params": {"type": "array", "items": [{"type": "string"}]} - }, - "if": { - "properties": {"enabled": { "const": true }} - }, - "then": { - "properties": {"params": {"type": "array", "minItems": 1, "items": [{"type": "string"}]}} - }, - "additionalProperties": false - }, - "rename": { - "type": "object", - "properties": { - "enabled": {"type": "boolean"}, - "params": {"type": "array", "items": [ - { - "type": "object", - "properties": { - "old": {"type": "string", "minLength": 1}, - "new": {"type": "string", "minLength": 1} - }, - "additionalProperties": false - } - ]} - }, - "if": { - "properties": {"enabled": { "const": true }} - }, - "then": { - "properties": {"params": {"minItems": 1}} - }, - "additionalProperties": false - }, - "replace": { - "type": "object", - "properties": { - "enabled": {"type": "boolean"}, - "replacements": {"type": "array", "items": [ - { - "type": "object", - "properties": { - "param": {"type": "string", "minLength": 1}, - "old": {"type": "string", "minLength": 1}, - "new": {"type": "string"} - }, - "additionalProperties": false - } - ]} - }, - "if": { - "properties": {"enabled": { "const": true }} - }, - "then": { - "properties": {"replacements": {"minItems": 1}} - }, - "additionalProperties": false - }, - "tags": { - "type": "object", - "properties": { - "enabled": {"type": "boolean"}, - "tags": {"type": "array", "items": [ - { - "type": "object", - "properties": { - "key": {"type": "string", "minLength": 1}, - "value": {"type": "string", "minLength": 1} - }, - "additionalProperties": false - } - ]} - }, - "if": { - "properties": {"enabled": { "const": true }} - }, - "then": { - "properties": {"tags": {"minItems": 1}} - }, - "additionalProperties": false - }, - "trim": { - "type": "object", - "properties": { - "enabled": {"type": "boolean"}, - "prefixes": {"type": "array", "items": [ - { - "type": "object", - "properties": { - "param": {"type": "string", "minLength": 1}, - "trim": {"type": "string", "minLength": 1} - }, - "additionalProperties": false - } - ]}, - "suffixes": {"type": "array", "items": [ - { - "type": "object", - "properties": { - "param": {"type": "string", "minLength": 1}, - "trim": {"type": "string", "minLength": 1} - }, - "additionalProperties": false - } - ]} - }, - "if": { - "properties": {"enabled": { "const": true }} - }, - "then": { - "properties": {"suffixes": {"minItems": 1}, "prefixes": {"minItems": 1}} - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "yara": { - "type": "object", - "properties": { - "enabled": {"type": "boolean"}, - "rule": { - "type": "object", - "anyOf": [{ - "properties": { - "paths": {"type": "array", "items": [ - { - "type": "object", - "properties": { - "path": {"type": "string"}, - "namespace": {"type": "string"} - }, - "if": { - "properties": {"enabled": {"$ref": "#yara", "const": true }} - }, - "then": { - "properties": {"path": {"minLength": 0}} - }, - "additionalProperties": false - }] - }, - "strings": {"type": "array"} - }, - "additionalProperties": false - }] - }, - "alert-template": {"type": "string"}, - "fastscan": {"type": "boolean"}, - "skip-files": {"type": "boolean"}, - "skip-allocs": {"type": "boolean"}, - "skip-mmaps": {"type": "boolean"}, - "skip-registry": {"type": "boolean"}, - "scan-timeout": {"type": "string", "minLength": 2, "pattern": "[0-9]+s"}, - "excluded-files": {"type": "array", "items": [{"type": "string", "minLength": 1}]}, - "excluded-procs": {"type": "array", "items": [{"type": "string", "minLength": 1}]} - }, - "additionalProperties": false - } - }, - "additionalProperties": false -} -` - -var rulesSchema = ` -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "id": {"type": "string", "minLength": 36, "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"}, - "version": {"type": "string", "minLength": 5, "pattern": "^([0-9]+.)([0-9]+.)([0-9]+)$"}, - "name": {"type": "string", "minLength": 3}, - "description": {"type": "string"}, - "output": {"type": "string", "minLength": 5}, - "notes": {"type": "string"}, - "severity": {"type": "string", "enum": ["low", "medium", "high", "critical"]}, - "min-engine-version": {"type": "string", "minLength": 5, "pattern": "^([0-9]+.)([0-9]+.)([0-9]+)$"}, - "enabled": {"type": "boolean"}, - "condition": {"type": "string", "minLength": 3}, - "labels": { - "type": "object", - "additionalProperties": { "type": "string"} - }, - "tags": {"type": "array", "items": [{"type": "string", "minLength": 1}]}, - "references": {"type": "array", "items": [{"type": "string", "minLength": 1}]}, - "authors": {"type": "array", "items": [{"type": "string", "minLength": 1}]}, - "action": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "name": {"type": "string", "enum": ["kill", "isolate"]}, - "whitelist": true - }, - "required": ["name"], - "if": { - "properties": {"name": {"const": "isolate"}} - }, - "then": { - "properties": { - "whitelist": {"type": "array", "minItems": 1, "items": {"type": "string", "format": "ipv4"}} - } - }, - "else": { - "properties": { - "name": {"type": "string", "enum": ["kill", "isolate"]} - }, - "additionalProperties": false - } - } - } - }, - "required": ["id", "version", "name", "condition", "min-engine-version"], - "additionalProperties": false -} -` - -var macrosSchema = ` -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "array", - "items": - { - "type": "object", - "properties": { - "macro": {"type": "string", "minLength": 2, "pattern": "^[A-Za-z0-9_-]+$"}, - "description": {"type": "string"}, - "expr": {"type": "string", "minLength": 5}, - "list": {"type": "array", "items": [{"type": "string", "minLength": 1}]} - }, - "required": ["macro"], - "oneOf": [ - {"required": ["expr"]}, - {"required": ["list"]} - ], - "additionalProperties": false - }, - "additionalProperties": false -} -` - -type schemaConfig struct { - MaxBuffers uint32 - MinBuffers uint32 - MaxBufferSize uint32 -} - -func interpolateSchema() string { - tmpl := template.Must(template.New("schema").Parse(schema)) - - var b bytes.Buffer - err := tmpl.Execute(&b, &schemaConfig{ - MaxBuffers: defaultMaxBuffers, - MinBuffers: defaultMinBuffers, - MaxBufferSize: maxBufferSize, - }) - if err != nil { - return "" - } - - return b.String() -} +//go:embed macros.schema.json +var macrosSchema string diff --git a/pkg/config/validation_test.go b/pkg/config/validation_test.go index df73f1447..69c3c3e5e 100644 --- a/pkg/config/validation_test.go +++ b/pkg/config/validation_test.go @@ -82,7 +82,7 @@ func TestValidate(t *testing.T) { if err != nil { t.Fatal(err) } - valid, errs := validate(interpolateSchema(), m) + valid, errs := validate(configSchema, m) if valid != tt.valid { t.Errorf("%d. valid mismatch: text=%q exp=%#v got=%#v errs=%#v", i, tt.text, tt.valid, valid, errs) } else if len(errs) != tt.errs { From 1dd69e37f6e35871c08ae5807af41c8a0316bedd Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Mon, 30 Jun 2025 21:38:26 +0200 Subject: [PATCH 36/42] fix(bootstrap): Don't compile rules if filament is supplied Avoid compiling ruleset if the Fibratus is instructed to the run the filament. --- internal/bootstrap/bootstrap.go | 14 +++++++------- pkg/config/config_windows.go | 3 +++ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go index 7cd552db1..49b7de979 100644 --- a/internal/bootstrap/bootstrap.go +++ b/internal/bootstrap/bootstrap.go @@ -138,7 +138,7 @@ func NewApp(cfg *config.Config, options ...Option) (*App, error) { var engine *rules.Engine var rs *config.RulesCompileResult - if cfg.Filters.Rules.Enabled && !cfg.ForwardMode && !cfg.IsCaptureSet() { + if cfg.Filters.Rules.Enabled && !cfg.ForwardMode && !cfg.IsCaptureSet() && !cfg.IsFilamentSet() { engine = rules.NewEngine(psnap, cfg) var err error rs, err = engine.Compile() @@ -203,9 +203,8 @@ func (f *App) Run(args []string) error { // In case of a regular run, we additionally set up the aggregator. // The aggregator will grab the events from the queue, assemble them // into batches and hand over to output sinks. - filamentName := cfg.Filament.Name - if filamentName != "" { - f.filament, err = filament.New(filamentName, f.psnap, f.hsnap, cfg) + if cfg.IsFilamentSet() { + f.filament, err = filament.New(cfg.Filament.Name, f.psnap, f.hsnap, cfg) if err != nil { return err } @@ -314,9 +313,9 @@ func (f *App) ReadCapture(ctx context.Context, args []string) error { if err != nil { return err } - filamentName := f.config.Filament.Name - if filamentName != "" { - f.filament, err = filament.New(filamentName, f.psnap, f.hsnap, f.config) + + if f.config.IsFilamentSet() { + f.filament, err = filament.New(f.config.Filament.Name, f.psnap, f.hsnap, f.config) if err != nil { return err } @@ -355,6 +354,7 @@ func (f *App) ReadCapture(ctx context.Context, args []string) error { return err } } + return api.StartServer(f.config) } diff --git a/pkg/config/config_windows.go b/pkg/config/config_windows.go index 2ca5cca84..4f2fc40c9 100644 --- a/pkg/config/config_windows.go +++ b/pkg/config/config_windows.go @@ -310,6 +310,9 @@ func (c *Config) Init() error { // in the capture file. func (c *Config) IsCaptureSet() bool { return c.CapFile != "" } +// IsFilamentSet indicates if the filament is supplied. +func (c *Config) IsFilamentSet() bool { return c.Filament.Name != "" } + // TryLoadFile attempts to load the configuration file from specified path on the file system. func (c *Config) TryLoadFile(file string) error { c.viper.SetConfigFile(file) From 18c7d009683e11c0f0a4b9dd0b4f743c29569cdb Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Thu, 7 Aug 2025 18:29:15 +0200 Subject: [PATCH 37/42] feat(rule_engine): Validate event/category names The rule engine compiler ensures that the provided event or category names are match the internal catalog nomenclature. --- pkg/event/category.go | 10 +++++++ pkg/event/metainfo_windows.go | 10 +++++++ .../correct_category_name_field.yml | 5 ++++ .../field_values/correct_event_name_field.yml | 5 ++++ .../incorrect_category_name_field.yml | 5 ++++ .../incorrect_event_name_field.yml | 5 ++++ .../incorrect_event_name_in_operator.yml | 5 ++++ pkg/rules/compiler.go | 25 +++++++++++++++++- pkg/rules/compiler_test.go | 26 +++++++++++++++++++ 9 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 pkg/rules/_fixtures/field_values/correct_category_name_field.yml create mode 100644 pkg/rules/_fixtures/field_values/correct_event_name_field.yml create mode 100644 pkg/rules/_fixtures/field_values/incorrect_category_name_field.yml create mode 100644 pkg/rules/_fixtures/field_values/incorrect_event_name_field.yml create mode 100644 pkg/rules/_fixtures/field_values/incorrect_event_name_in_operator.yml diff --git a/pkg/event/category.go b/pkg/event/category.go index 0f86043c7..f136415c9 100644 --- a/pkg/event/category.go +++ b/pkg/event/category.go @@ -122,3 +122,13 @@ func Categories() []string { string(Threadpool), } } + +// IsCategoryKnown indicates if the category is known given its name. +func IsCategoryKnown(name string) bool { + for _, category := range Categories() { + if category == name { + return true + } + } + return false +} diff --git a/pkg/event/metainfo_windows.go b/pkg/event/metainfo_windows.go index d7913cf22..6f8df0e9d 100644 --- a/pkg/event/metainfo_windows.go +++ b/pkg/event/metainfo_windows.go @@ -314,6 +314,16 @@ outer: return typs } +// IsKnown indicates if the event type is known given the event name. +func IsKnown(name string) bool { + for _, evt := range GetTypesMeta() { + if evt.Name == name { + return true + } + } + return false +} + // GetTypesMetaIndexed returns indexed event types metadata // that is guaranteed to always return the same event indices. func GetTypesMetaIndexed() []Info { return indexedEvents } diff --git a/pkg/rules/_fixtures/field_values/correct_category_name_field.yml b/pkg/rules/_fixtures/field_values/correct_category_name_field.yml new file mode 100644 index 000000000..377b123cb --- /dev/null +++ b/pkg/rules/_fixtures/field_values/correct_category_name_field.yml @@ -0,0 +1,5 @@ +name: match https connections +id: 8f36f8e0-a5c2-498f-9563-eea306daa586 +version: 1.0.0 +condition: evt.category = 'net' and net.dport = 443 +min-engine-version: 2.0.0 diff --git a/pkg/rules/_fixtures/field_values/correct_event_name_field.yml b/pkg/rules/_fixtures/field_values/correct_event_name_field.yml new file mode 100644 index 000000000..55e643f66 --- /dev/null +++ b/pkg/rules/_fixtures/field_values/correct_event_name_field.yml @@ -0,0 +1,5 @@ +name: match https connections +id: 8f36f8e0-a5c2-498f-9563-eea306daa586 +version: 1.0.0 +condition: evt.name = 'Recv' and net.dport = 443 +min-engine-version: 2.0.0 diff --git a/pkg/rules/_fixtures/field_values/incorrect_category_name_field.yml b/pkg/rules/_fixtures/field_values/incorrect_category_name_field.yml new file mode 100644 index 000000000..0970eb35a --- /dev/null +++ b/pkg/rules/_fixtures/field_values/incorrect_category_name_field.yml @@ -0,0 +1,5 @@ +name: match https connections +id: 8f36f8e0-a5c2-498f-9563-eea306daa586 +version: 1.0.0 +condition: evt.category = 'network' and net.dport = 443 +min-engine-version: 2.0.0 diff --git a/pkg/rules/_fixtures/field_values/incorrect_event_name_field.yml b/pkg/rules/_fixtures/field_values/incorrect_event_name_field.yml new file mode 100644 index 000000000..132c12621 --- /dev/null +++ b/pkg/rules/_fixtures/field_values/incorrect_event_name_field.yml @@ -0,0 +1,5 @@ +name: match https connections +id: 8f36f8e0-a5c2-498f-9563-eea306daa586 +version: 1.0.0 +condition: evt.name = 'RecvTcp4' and net.dport = 443 +min-engine-version: 2.0.0 diff --git a/pkg/rules/_fixtures/field_values/incorrect_event_name_in_operator.yml b/pkg/rules/_fixtures/field_values/incorrect_event_name_in_operator.yml new file mode 100644 index 000000000..67870eea6 --- /dev/null +++ b/pkg/rules/_fixtures/field_values/incorrect_event_name_in_operator.yml @@ -0,0 +1,5 @@ +name: match https connections +id: 8f36f8e0-a5c2-498f-9563-eea306daa586 +version: 1.0.0 +condition: evt.name in ('Recv', 'Accept', 'CreateProc') and net.dport = 443 +min-engine-version: 2.0.0 diff --git a/pkg/rules/compiler.go b/pkg/rules/compiler.go index 7d964f3c2..8b038a810 100644 --- a/pkg/rules/compiler.go +++ b/pkg/rules/compiler.go @@ -44,6 +44,12 @@ var ( ErrMalformedMinEngineVer = func(rule, v string, err error) error { return fmt.Errorf("rule %q has a malformed minimum engine version: %s: %v", rule, v, err) } + ErrUnknownEventName = func(rule, name string) error { + return fmt.Errorf("rule %s references an invalid event name %q in the evt.name field", rule, name) + } + ErrUnknownCategoryName = func(rule, name string) error { + return fmt.Errorf("rule %s references an invalid event category %q in the evt.category field", rule, name) + } ) type compiler struct { @@ -89,6 +95,7 @@ func (c *compiler) compile() (map[*config.FilterConfig]filter.Filter, *config.Ru return nil, nil, ErrIncompatibleFilter(f.Name, f.MinEngineVersion) } } + // output warning for deprecated fields for _, field := range fltr.GetFields() { deprecated, d := fields.IsDeprecated(field.Name) @@ -97,7 +104,23 @@ func (c *compiler) compile() (map[*config.FilterConfig]filter.Filter, *config.Ru "was deprecated starting from version %s. "+ "Please consider migrating to %s field(s) "+ "because [%s] will be removed in future versions.", - f.Name, field, d.Since, d.Fields, field) + f.Name, field.Name, d.Since, d.Fields, field.Name) + } + } + + // validate the value of the event/category fields + for field, values := range fltr.GetStringFields() { + for _, v := range values { + switch field { + case fields.EvtName, fields.KevtName: + if !event.IsKnown(v) { + return nil, nil, ErrUnknownEventName(f.Name, v) + } + case fields.EvtCategory, fields.KevtCategory: + if !event.IsCategoryKnown(v) { + return nil, nil, ErrUnknownCategoryName(f.Name, v) + } + } } } diff --git a/pkg/rules/compiler_test.go b/pkg/rules/compiler_test.go index 2bc6ac531..fff233bf5 100644 --- a/pkg/rules/compiler_test.go +++ b/pkg/rules/compiler_test.go @@ -70,3 +70,29 @@ func TestCompileMinEngineVersion(t *testing.T) { }) } } + +func TestCompileEventCategoryFieldNames(t *testing.T) { + var tests = []struct { + rules string + err error + }{ + {"_fixtures/field_values/correct_event_name_field.yml", nil}, + {"_fixtures/field_values/incorrect_event_name_field.yml", ErrUnknownEventName("match https connections", "RecvTcp4")}, + {"_fixtures/field_values/incorrect_event_name_in_operator.yml", ErrUnknownEventName("match https connections", "CreateProc")}, + {"_fixtures/field_values/correct_category_name_field.yml", nil}, + {"_fixtures/field_values/incorrect_category_name_field.yml", ErrUnknownCategoryName("match https connections", "network")}, + } + + for _, tt := range tests { + t.Run(tt.rules, func(t *testing.T) { + c := newCompiler(new(ps.SnapshotterMock), newConfig(tt.rules)) + _, _, err := c.compile() + if err != nil && tt.err != nil { + require.Error(t, err) + } + if err != nil { + require.EqualError(t, err, tt.err.Error()) + } + }) + } +} From 36430eb190336796ff2461099bda3889ff948bc6 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Sun, 10 Aug 2025 19:18:49 +0200 Subject: [PATCH 38/42] fix(ps): Initialize process name on internal event arrival --- pkg/ps/snapshotter_windows.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/ps/snapshotter_windows.go b/pkg/ps/snapshotter_windows.go index 0656076c6..d95b475ef 100644 --- a/pkg/ps/snapshotter_windows.go +++ b/pkg/ps/snapshotter_windows.go @@ -369,6 +369,7 @@ func (s *snapshotter) newProcState(pid, ppid uint32, e *event.Event) (*pstypes.P PID: pid, Ppid: ppid, Exe: e.GetParamAsString(params.Exe), + Name: filepath.Base(e.GetParamAsString(params.Exe)), TokenIntegrityLevel: e.GetParamAsString(params.ProcessIntegrityLevel), TokenElevationType: e.GetParamAsString(params.ProcessTokenElevationType), IsTokenElevated: e.Params.TryGetBool(params.ProcessTokenIsElevated), From 8d71b628091a30e7498c196f582b67996c5fa1bc Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Mon, 11 Aug 2025 18:33:53 +0200 Subject: [PATCH 39/42] feat(evasion): Evasion scanner This changeset establishes the architecture for the evasion scanner. Evasion scanner piggybacks on top of different evasion detectors such as direct syscall (being the first evasion behaviour implemented). When the evasion behaviour is detected, the event is decorated with such evasion in its metadata. --- .github/PULL_REQUEST_TEMPLATE.md | 2 + .github/workflows/master.yml | 4 + .github/workflows/pr.yml | 4 + configs/fibratus.yml | 15 ++ internal/bootstrap/bootstrap.go | 5 + internal/etw/_fixtures/Taskfile.yml | 20 +++ .../etw/_fixtures/direct-syscall/.gitignore | 6 + .../_fixtures/direct-syscall/Makefile.msvc | 7 + internal/etw/_fixtures/direct-syscall/main.c | 9 ++ internal/etw/source_test.go | 123 +++++++++++++++ internal/evasion/config.go | 49 ++++++ internal/evasion/direct_syscall.go | 64 ++++++++ internal/evasion/direct_syscall_test.go | 141 ++++++++++++++++++ internal/evasion/scanner.go | 79 ++++++++++ internal/evasion/scanner_test.go | 74 +++++++++ internal/evasion/types.go | 50 +++++++ pkg/callstack/callstack.go | 18 +++ pkg/callstack/callstack_test.go | 6 + pkg/config/config.schema.json | 12 ++ pkg/config/config_windows.go | 13 ++ pkg/event/event.go | 20 +++ 21 files changed, 721 insertions(+) create mode 100644 internal/etw/_fixtures/Taskfile.yml create mode 100644 internal/etw/_fixtures/direct-syscall/.gitignore create mode 100644 internal/etw/_fixtures/direct-syscall/Makefile.msvc create mode 100644 internal/etw/_fixtures/direct-syscall/main.c create mode 100644 internal/evasion/config.go create mode 100644 internal/evasion/direct_syscall.go create mode 100644 internal/evasion/direct_syscall_test.go create mode 100644 internal/evasion/scanner.go create mode 100644 internal/evasion/scanner_test.go create mode 100644 internal/evasion/types.go diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index de714d4cd..afdede38c 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -68,6 +68,8 @@ > /area deps +> /area evasion + > /area other diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 21970a42d..106647c9c 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -169,6 +169,10 @@ jobs: run: | cd yara make install + - name: Setup test fixtures + run: | + choco install -y go-task + task --taskfile internal/etw/_fixtures/Taskfile.yml all - name: Test shell: bash run: | diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index f4698bd18..364111125 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -151,6 +151,10 @@ jobs: run: | cd yara make install + - name: Setup test fixtures + run: | + choco install -y go-task + task --taskfile internal/etw/_fixtures/Taskfile.yml all - name: Test shell: bash run: | diff --git a/configs/fibratus.yml b/configs/fibratus.yml index 28a2cb34e..573a7146b 100644 --- a/configs/fibratus.yml +++ b/configs/fibratus.yml @@ -156,6 +156,21 @@ handle: # Indicates if process handles are collected during startup or when a new process is spawn. enumerate-handles: false +# =============================== Evasion ==================================================== + +# Tweaks for controlling evasion scanner behaviours. Evasion behaviours can represent strong +# IoC (Indicators of Compromise) such as direct syscall or require a combination of fine-tune +# exceptions to reduce the alert fatigue. +evasion: + # Indicates if evasion detections are enabled global-wise. If disabled, evasion scanner will + # not try to classify ad-hoc evasion techniques. + enabled: true + + # Indicates if direct syscall evasion detection is enabled. A direct syscall bypasses Windows + # API functions and calls the underlying system call directly using the syscall instruction, + # skipping the NTDLL stub that normally performs the transition to kernel mode. + #enable-direct-syscall: true + # =============================== Event =============================================== # The following settings control the state of the event. diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go index 49b7de979..5014b87d9 100644 --- a/internal/bootstrap/bootstrap.go +++ b/internal/bootstrap/bootstrap.go @@ -21,6 +21,7 @@ package bootstrap import ( "context" "errors" + "github.com/rabbitstack/fibratus/internal/evasion" "github.com/rabbitstack/fibratus/pkg/aggregator" "github.com/rabbitstack/fibratus/pkg/alertsender" "github.com/rabbitstack/fibratus/pkg/api" @@ -233,6 +234,10 @@ func (f *App) Run(args []string) error { f.symbolizer = symbolize.NewSymbolizer(symbolize.NewDebugHelpResolver(cfg), f.psnap, cfg, false) f.evs.RegisterEventListener(f.symbolizer) } + // register evasion scanner + if cfg.Evasion.Enabled { + f.evs.RegisterEventListener(evasion.NewScanner(cfg.Evasion)) + } // register rule engine if f.engine != nil { f.evs.RegisterEventListener(f.engine) diff --git a/internal/etw/_fixtures/Taskfile.yml b/internal/etw/_fixtures/Taskfile.yml new file mode 100644 index 000000000..58fb07264 --- /dev/null +++ b/internal/etw/_fixtures/Taskfile.yml @@ -0,0 +1,20 @@ +version: '3' + +vars: + SYSWHISPERS3_REPO: https://github.com/klezVirus/SysWhispers3.git + VISUAL_STUDIO_EDITION: Enterprise + VISUAL_STUDIO_VERSION: 2022 + +tasks: + direct-syscall: + desc: Builds the binary to perform direct syscalls via Syswhispers generated stubs + dir: direct-syscall + cmds: + - git clone {{ .SYSWHISPERS3_REPO }} + - python SysWhispers3/syswhispers.py -a x64 -c msvc -p common -o syscalls + - cmd.exe /c 'C:\"Program Files"\"Microsoft Visual Studio"\{{ .VISUAL_STUDIO_VERSION }}\{{ .VISUAL_STUDIO_EDITION }}\VC\Auxiliary\Build\vcvars64.bat && nmake -f Makefile.msvc' + silent: true + + all: + deps: + - direct-syscall diff --git a/internal/etw/_fixtures/direct-syscall/.gitignore b/internal/etw/_fixtures/direct-syscall/.gitignore new file mode 100644 index 000000000..321a5cb03 --- /dev/null +++ b/internal/etw/_fixtures/direct-syscall/.gitignore @@ -0,0 +1,6 @@ +SysWhishpers3 +*.obj +*.asm +*.exe +syscalls.c +syscalls.h diff --git a/internal/etw/_fixtures/direct-syscall/Makefile.msvc b/internal/etw/_fixtures/direct-syscall/Makefile.msvc new file mode 100644 index 000000000..9b93f9026 --- /dev/null +++ b/internal/etw/_fixtures/direct-syscall/Makefile.msvc @@ -0,0 +1,7 @@ +OPTIONS = -Zp8 -c -nologo -Gy -Os -O1 -GR- -EHa -Oi -GS- +LIBS = libvcruntime.lib libcmt.lib ucrt.lib kernel32.lib + +main: + ML64 /c syscalls-asm.x64.asm /link /NODEFAULTLIB /RELEASE /MACHINE:X64 + cl.exe $(OPTIONS) syscalls.c main.c + link.exe /OUT:direct-syscall.exe -nologo $(LIBS) /MACHINE:X64 -subsystem:console -nodefaultlib syscalls-asm.x64.obj syscalls.obj main.obj diff --git a/internal/etw/_fixtures/direct-syscall/main.c b/internal/etw/_fixtures/direct-syscall/main.c new file mode 100644 index 000000000..ba12537ff --- /dev/null +++ b/internal/etw/_fixtures/direct-syscall/main.c @@ -0,0 +1,9 @@ +#include "syscalls.h" + +#include + +int main(int argc, char* argv[]) +{ + Sw3NtSetContextThread(-1, NULL); + return 0; +} diff --git a/internal/etw/source_test.go b/internal/etw/source_test.go index adf7959d6..88b27c1ce 100644 --- a/internal/etw/source_test.go +++ b/internal/etw/source_test.go @@ -20,6 +20,7 @@ package etw import ( "context" "fmt" + "github.com/rabbitstack/fibratus/internal/evasion" "github.com/rabbitstack/fibratus/pkg/config" "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/event/params" @@ -41,6 +42,7 @@ import ( "net" "net/http" "os" + "os/exec" "path/filepath" "runtime" "strings" @@ -1294,6 +1296,127 @@ func testCallstackEnrichment(t *testing.T, hsnap handle.Snapshotter, psnap ps.Sn } } +func containsEvasion(e *event.Event, evasion string) bool { + m := e.GetMeta(event.EvasionsKey) + evas, ok := m.([]string) + if !ok { + return false + } + for _, eva := range evas { + if eva == evasion { + return true + } + } + return false +} + +func TestEvasionScanner(t *testing.T) { + var tests = []*struct { + name string + gen func() error + want func(e *event.Event) bool + completed bool + }{ + { + "direct syscall", + func() error { + cmd := exec.Command("_fixtures/direct-syscall/direct-syscall.exe") + return cmd.Run() + }, + func(e *event.Event) bool { + if strings.Contains(strings.ToLower(e.Callstack.String()), strings.ToLower("direct-syscall.exe")) && e.Type == event.SetThreadContext { + log.Info(e, e.Callstack) + return containsEvasion(e, "direct_syscall") + } + return false + }, + false, + }, + } + + evsConfig := config.EventSourceConfig{ + EnableThreadEvents: true, + EnableImageEvents: true, + EnableFileIOEvents: false, + EnableVAMapEvents: true, + EnableNetEvents: true, + EnableRegistryEvents: false, + EnableMemEvents: false, + EnableHandleEvents: false, + EnableDNSEvents: false, + EnableAuditAPIEvents: true, + StackEnrichment: true, + } + evsConfig.Init() + + hsnap := new(handle.SnapshotterMock) + hsnap.On("FindByObject", mock.Anything).Return(htypes.Handle{}, false) + hsnap.On("FindHandles", mock.Anything).Return([]htypes.Handle{}, nil) + hsnap.On("Write", mock.Anything).Return(nil) + hsnap.On("Remove", mock.Anything).Return(nil) + + cfg := &config.Config{EventSource: evsConfig, Filters: &config.Filters{}} + + psnap := ps.NewSnapshotter(hsnap, cfg) + + evs := NewEventSource(psnap, hsnap, cfg, nil) + + l := &MockListener{} + evs.RegisterEventListener(l) + + symbolizer := symbolize.NewSymbolizer(symbolize.NewDebugHelpResolver(cfg), psnap, cfg, true) + defer symbolizer.Close() + evs.RegisterEventListener(symbolizer) + + scanner := evasion.NewScanner(evasion.Config{Enabled: true, EnableDirectSyscall: true}) + evs.RegisterEventListener(scanner) + + require.NoError(t, evs.Open(cfg)) + defer evs.Close() + + time.Sleep(time.Second * 2) + + for _, tt := range tests { + gen := tt.gen + if gen != nil { + log.Infof("executing [%s] evasion test generator", tt.name) + require.NoError(t, gen(), tt.name) + } + } + + ntests := len(tests) + timeout := time.After(time.Duration(ntests) * time.Minute) + + for { + select { + case e := <-evs.Events(): + for _, tt := range tests { + if tt.completed { + continue + } + pred := tt.want + if pred(e) { + t.Logf("PASS: %s", tt.name) + tt.completed = true + ntests-- + } + if ntests == 0 { + return + } + } + case err := <-evs.Errors(): + t.Fatalf("FAIL: %v", err) + case <-timeout: + for _, tt := range tests { + if !tt.completed { + t.Logf("FAIL: %s", tt.name) + } + } + t.Fatal("FAIL: TestEvasionScanner") + } + } +} + var ( modadvapi32 = windows.NewLazySystemDLL("advapi32.dll") kernel32 = windows.NewLazySystemDLL("kernel32.dll") diff --git a/internal/evasion/config.go b/internal/evasion/config.go new file mode 100644 index 000000000..4e0d899a5 --- /dev/null +++ b/internal/evasion/config.go @@ -0,0 +1,49 @@ +/* + * Copyright 2021-present by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package evasion + +import ( + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +const ( + enabled = "evasion.enabled" + enableDirectSyscall = "evasion.enable-direct-syscall" +) + +// Config contains the settings that influence the behaviour of the evasion scanner. +type Config struct { + // Enabled indicates if evasion detections are enabled global-wise. + Enabled bool `json:"enabled" yaml:"enabled"` + // EnableDirectSyscall indicates if direct syscall evasion detection is enabled. + EnableDirectSyscall bool `json:"enable-direct-syscall" yaml:"enable-direct-syscall"` +} + +// InitFromViper initializes evasion config from Viper. +func (c *Config) InitFromViper(v *viper.Viper) { + c.Enabled = v.GetBool(enabled) + c.EnableDirectSyscall = v.GetBool(enableDirectSyscall) +} + +// AddFlags adds evasion config flags to the set. +func AddFlags(flags *pflag.FlagSet) { + flags.Bool(enabled, true, "Indicates if evasion detections are enabled global-wise") + flags.Bool(enableDirectSyscall, true, "Indicates if direct syscall evasion detection is enabled") +} diff --git a/internal/evasion/direct_syscall.go b/internal/evasion/direct_syscall.go new file mode 100644 index 000000000..57b52c6ad --- /dev/null +++ b/internal/evasion/direct_syscall.go @@ -0,0 +1,64 @@ +/* + * Copyright 2021-present by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package evasion + +import ( + "github.com/rabbitstack/fibratus/pkg/event" + "path/filepath" + "strings" +) + +// directSyscall direct syscall evasion refers to a technique where +// adversaries bypass traditional user-mode API monitoring and security +// hooks by invoking system calls directly, but does so in a way that +// evades detection or analysis. +// +// A direct syscall bypasses Windows API functions and calls the underlying +// system call directly using the syscall instruction, skipping the NTDLL +// stub that normally performs the transition to kernel mode. +type directSyscall struct{} + +func NewDirectSyscall() Evasion { + return &directSyscall{} +} + +func (d *directSyscall) Eval(e *event.Event) (bool, error) { + if e.Callstack.IsEmpty() { + return false, nil + } + + frame := e.Callstack.FinalUserspaceFrame() + if frame == nil { + return false, nil + } + + if frame.IsUnbacked() { + return false, nil + } + + mod := filepath.Base(strings.ToLower(frame.Module)) + + // check if the last userspace frame is originated + // from the allowed modules such as the native NTDLL + // module. If that's not the case, the process is + // invoking a direct syscall + return mod != "ntdll.dll" && mod != "win32.dll" && mod != "win32u.dll" && mod != "wow64win.dll" && mod != "wow64cpu.dll", nil +} + +func (d *directSyscall) Type() Type { return DirectSyscall } diff --git a/internal/evasion/direct_syscall_test.go b/internal/evasion/direct_syscall_test.go new file mode 100644 index 000000000..80a394b73 --- /dev/null +++ b/internal/evasion/direct_syscall_test.go @@ -0,0 +1,141 @@ +/* + * Copyright 2021-present by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package evasion + +import ( + "github.com/rabbitstack/fibratus/pkg/callstack" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" + "github.com/rabbitstack/fibratus/pkg/fs" + "github.com/stretchr/testify/require" + "testing" + "time" +) + +func TestDirectSyscall(t *testing.T) { + var tests = []struct { + evt *event.Event + matches bool + }{ + {&event.Event{ + Type: event.CreateFile, + Tid: 2484, + PID: 859, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Timestamp: time.Now(), + Category: event.File, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.Enum, Value: uint32(2), Enum: fs.FileCreateDispositions}, + }, + Callstack: callstackFromFrames( + callstack.Frame{Addr: 0xf259de, Module: "unbacked", Symbol: "?"}, + callstack.Frame{Addr: 0x7ffe4fda6e3b, Module: "C:\\Windows\\System32\\KernelBase.dll", Symbol: "SetThreadContext"}, + callstack.Frame{Addr: 0xfffff807e228c555, Module: "C:\\WINDOWS\\system32\\ntoskrnl.exe", Symbol: "setjmpex"}, + callstack.Frame{Addr: 0xfffff807e264805c, Module: "C:\\WINDOWS\\system32\\ntoskrnl.exe", Symbol: "ObOpenObjectByPointerWithTag"}), + }, true}, + {&event.Event{ + Type: event.CreateFile, + Tid: 2484, + PID: 859, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Timestamp: time.Now(), + Category: event.File, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.Enum, Value: uint32(2), Enum: fs.FileCreateDispositions}, + }, + Callstack: callstackFromFrames( + callstack.Frame{Addr: 0xf259de, Module: "unbacked", Symbol: "?"}, + callstack.Frame{Addr: 0x7ffe4fda6e3b, Module: "C:\\Windows\\System32\\KernelBase.dll", Symbol: "SetThreadContext"}, + callstack.Frame{Addr: 0x7ffe52942b24, Module: "C:\\Windows\\System32\\ntdll.dll", Symbol: "ZwSetContextThread"}, + callstack.Frame{Addr: 0xfffff807e228c555, Module: "C:\\WINDOWS\\system32\\ntoskrnl.exe", Symbol: "setjmpex"}, + callstack.Frame{Addr: 0xfffff807e264805c, Module: "C:\\WINDOWS\\system32\\ntoskrnl.exe", Symbol: "ObOpenObjectByPointerWithTag"}), + }, false}, + {&event.Event{ + Type: event.CreateFile, + Tid: 2484, + PID: 859, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Timestamp: time.Now(), + Category: event.File, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.Enum, Value: uint32(2), Enum: fs.FileCreateDispositions}, + }, + Callstack: callstackFromFrames( + callstack.Frame{Addr: 0xf259de, Module: "unbacked", Symbol: "?"}, + callstack.Frame{Addr: 0x7ffe4fda6e3b, Module: "C:\\Windows\\System32\\KernelBase.dll", Symbol: "SetThreadContext"}, + callstack.Frame{Addr: 0x7ffe52942b24, Module: "C:\\Windows\\System32\\wow64win.dll", Symbol: "SetContextThread"}, + callstack.Frame{Addr: 0xfffff807e228c555, Module: "C:\\WINDOWS\\system32\\ntoskrnl.exe", Symbol: "setjmpex"}, + callstack.Frame{Addr: 0xfffff807e264805c, Module: "C:\\WINDOWS\\system32\\ntoskrnl.exe", Symbol: "ObOpenObjectByPointerWithTag"}), + }, false}, + {&event.Event{ + Type: event.CreateFile, + Tid: 2484, + PID: 859, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Timestamp: time.Now(), + Category: event.File, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.Enum, Value: uint32(2), Enum: fs.FileCreateDispositions}, + }, + Callstack: callstackFromFrames( + callstack.Frame{Addr: 0xf259de, Module: "unbacked", Symbol: "?"}, + callstack.Frame{Addr: 0x7ffe4fda6e3b, Module: "C:\\Windows\\System32\\KernelBase.dll", Symbol: "SetThreadContext"}, + callstack.Frame{Addr: 0x7ffe52942b24, Module: "unbacked", Symbol: "?"}, + callstack.Frame{Addr: 0xfffff807e228c555, Module: "C:\\WINDOWS\\system32\\ntoskrnl.exe", Symbol: "setjmpex"}, + callstack.Frame{Addr: 0xfffff807e264805c, Module: "C:\\WINDOWS\\system32\\ntoskrnl.exe", Symbol: "ObOpenObjectByPointerWithTag"}), + }, false}, + } + + for _, tt := range tests { + t.Run(tt.evt.Name, func(t *testing.T) { + eva := NewDirectSyscall() + matches, err := eva.Eval(tt.evt) + require.NoError(t, err) + require.Equal(t, tt.matches, matches) + }) + } +} + +func callstackFromFrames(frames ...callstack.Frame) callstack.Callstack { + var c callstack.Callstack + for _, frame := range frames { + c.PushFrame(frame) + } + return c +} diff --git a/internal/evasion/scanner.go b/internal/evasion/scanner.go new file mode 100644 index 000000000..00c28a901 --- /dev/null +++ b/internal/evasion/scanner.go @@ -0,0 +1,79 @@ +/* + * Copyright 2021-present by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package evasion + +import ( + "github.com/rabbitstack/fibratus/pkg/event" + log "github.com/sirupsen/logrus" +) + +// Scanner is responsible for evaluating evasion detectors +// and decorating the event with the reported behaviours. +// Some behaviours represent strong IoCs, while other need +// careful tuning to avoid alert fatigue. Evasion behaviours +// are consumed by the rule engine through the filter fields +// that yields the evasion techniques, such as direct syscall. +type Scanner struct { + evasions []Evasion +} + +// NewScanner instantiates the new evasion scanner. +func NewScanner(config Config) *Scanner { + s := &Scanner{ + evasions: make([]Evasion, 0), + } + + if config.EnableDirectSyscall { + s.registerEvasion(NewDirectSyscall()) + } + + return s +} + +func (s *Scanner) ProcessEvent(e *event.Event) (bool, error) { + // filter out CreateFile events with the open disposition + // as they tend to be noisy and could impact performance + // when hitting evasion detectors + if e.IsOpenDisposition() { + return true, nil + } + + var enq bool + + // run registered evasion detectors + for _, eva := range s.evasions { + matches, err := eva.Eval(e) + if err != nil { + return false, err + } + if matches { + enq = true + e.AddSliceMetaOrAppend(event.EvasionsKey, eva.Type().String()) + log.Infof("detected evasion %q on event [%s] and callstack [%s]", eva.Type(), e, e.Callstack) + } + } + + return enq, nil +} + +func (s *Scanner) CanEnqueue() bool { return false } + +func (s *Scanner) registerEvasion(eva Evasion) { + s.evasions = append(s.evasions, eva) +} diff --git a/internal/evasion/scanner_test.go b/internal/evasion/scanner_test.go new file mode 100644 index 000000000..36205511b --- /dev/null +++ b/internal/evasion/scanner_test.go @@ -0,0 +1,74 @@ +/* + * Copyright 2021-present by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package evasion + +import ( + "github.com/rabbitstack/fibratus/pkg/callstack" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" + "github.com/rabbitstack/fibratus/pkg/fs" + "github.com/stretchr/testify/require" + "strings" + "testing" + "time" +) + +func TestScannerProcessEvent(t *testing.T) { + var tests = []struct { + evt *event.Event + expectedEvasions []string + }{ + {&event.Event{ + Type: event.CreateFile, + Tid: 2484, + PID: 859, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Timestamp: time.Now(), + Category: event.File, + Metadata: event.Metadata{}, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.Enum, Value: uint32(2), Enum: fs.FileCreateDispositions}, + }, + Callstack: callstackFromFrames( + callstack.Frame{Addr: 0xf259de, Module: "unbacked", Symbol: "?"}, + callstack.Frame{Addr: 0x7ffe4fda6e3b, Module: "C:\\Windows\\System32\\KernelBase.dll", Symbol: "SetThreadContext"}, + callstack.Frame{Addr: 0xfffff807e228c555, Module: "C:\\WINDOWS\\system32\\ntoskrnl.exe", Symbol: "setjmpex"}, + callstack.Frame{Addr: 0xfffff807e264805c, Module: "C:\\WINDOWS\\system32\\ntoskrnl.exe", Symbol: "ObOpenObjectByPointerWithTag"}), + }, []string{"direct_syscall"}}, + } + + s := NewScanner(Config{Enabled: true, EnableDirectSyscall: true}) + + for _, tt := range tests { + t.Run(strings.Join(tt.expectedEvasions, ","), func(t *testing.T) { + matches, err := s.ProcessEvent(tt.evt) + require.NoError(t, err) + require.True(t, matches && len(tt.expectedEvasions) > 0) + if len(tt.expectedEvasions) > 0 { + require.True(t, tt.evt.ContainsMeta(event.EvasionsKey)) + require.Equal(t, tt.expectedEvasions, tt.evt.GetMeta(event.EvasionsKey).([]string)) + } + }) + } +} diff --git a/internal/evasion/types.go b/internal/evasion/types.go new file mode 100644 index 000000000..dc06a0e81 --- /dev/null +++ b/internal/evasion/types.go @@ -0,0 +1,50 @@ +/* + * Copyright 2021-present by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package evasion + +import "github.com/rabbitstack/fibratus/pkg/event" + +// Type is the alias for the evasion technique type. +type Type uint8 + +const ( + // DirectSyscall represents the direct syscall evasion. + DirectSyscall Type = iota +) + +// String returns the evasion human-friendly name. +func (t Type) String() string { + switch t { + case DirectSyscall: + return "direct_syscall" + default: + return "unknown" + } +} + +// Evasion defines the contract that all evasion detectors need to satisfy. +type Evasion interface { + // Eval executes the evasion logic. The evasion detector usually accesses + // the callstack from the given event to determine if any evasions are + // performed on behalf of the process. If the evasion is classified, this + // method return true, or false otherwise. + Eval(*event.Event) (bool, error) + // Type returns the type of the evasion technique. + Type() Type +} diff --git a/pkg/callstack/callstack.go b/pkg/callstack/callstack.go index a60ae1b4d..934a2650b 100644 --- a/pkg/callstack/callstack.go +++ b/pkg/callstack/callstack.go @@ -196,6 +196,24 @@ func (s *Callstack) FinalUserFrame() *Frame { return nil } +// FinalUserspaceFrame returns the final userspace frame. This +// frame is typically backed by the ntdll module. +func (s *Callstack) FinalUserspaceFrame() *Frame { + if s.IsEmpty() { + return nil + } + + for n := s.Depth() - 1; n > 0; n-- { + f := (*s)[n] + if f.Addr.InSystemRange() { + continue + } + return &f + } + + return nil +} + // FinalKernelFrame returns the final kernel space frame. func (s *Callstack) FinalKernelFrame() *Frame { if s.IsEmpty() { diff --git a/pkg/callstack/callstack_test.go b/pkg/callstack/callstack_test.go index 5ac3e4b48..c9a629559 100644 --- a/pkg/callstack/callstack_test.go +++ b/pkg/callstack/callstack_test.go @@ -56,6 +56,12 @@ func TestCallstack(t *testing.T) { assert.Equal(t, "fffff8015690b644", kframe.Addr.String()) assert.Equal(t, "ObDeleteCapturedInsertInfo", kframe.Symbol) assert.Equal(t, "C:\\WINDOWS\\system32\\ntoskrnl.exe", kframe.Module) + + finalUserspaceFrame := callstack.FinalUserspaceFrame() + require.NotNil(t, finalUserspaceFrame) + assert.Equal(t, "7ffb5c1d0396", finalUserspaceFrame.Addr.String()) + assert.Equal(t, "CreateProcessW", finalUserspaceFrame.Symbol) + assert.Equal(t, "C:\\WINDOWS\\System32\\KERNELBASE.dll", finalUserspaceFrame.Module) } func TestCallstackFinalUserFrame(t *testing.T) { diff --git a/pkg/config/config.schema.json b/pkg/config/config.schema.json index 4fd9b9ae1..ffe77ef80 100644 --- a/pkg/config/config.schema.json +++ b/pkg/config/config.schema.json @@ -292,6 +292,18 @@ }, "additionalProperties": false }, + "evasion": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "enable-direct-syscall": { + "type": "boolean" + } + }, + "additionalProperties": false + }, "event": { "type": "object", "properties": { diff --git a/pkg/config/config_windows.go b/pkg/config/config_windows.go index 4f2fc40c9..59391fc97 100644 --- a/pkg/config/config_windows.go +++ b/pkg/config/config_windows.go @@ -21,6 +21,7 @@ package config import ( "encoding/json" "fmt" + "github.com/rabbitstack/fibratus/internal/evasion" "golang.org/x/sys/windows" "time" @@ -125,6 +126,9 @@ type Config struct { // Filters contains filter/rule definitions Filters *Filters `json:"filters" yaml:"filters"` + // Evasion controls the detection of evasion behaviours. + Evasion evasion.Config `json:"evasion" yaml:"evasion"` + flags *pflag.FlagSet viper *viper.Viper opts *Options @@ -236,6 +240,10 @@ func NewWithOpts(options ...Option) *Config { pe.AddFlags(flagSet) } + if opts.run { + evasion.AddFlags(flagSet) + } + c.addFlags() return c @@ -303,6 +311,11 @@ func (c *Config) Init() error { return err } } + + if c.opts.run { + c.Evasion.InitFromViper(c.viper) + } + return nil } diff --git a/pkg/event/event.go b/pkg/event/event.go index 0383f0c76..f376c6959 100644 --- a/pkg/event/event.go +++ b/pkg/event/event.go @@ -48,6 +48,8 @@ const ( // RuleSequenceOOOKey the presence of this metadata key indicates the // event in the partials list arrived out of order and requires reevaluation RuleSequenceOOOKey MetadataKey = "rule.seq.ooo" + // EvasionsKey represents the evasion behaviours detected on the event + EvasionsKey MetadataKey = "evasions" ) func (key MetadataKey) String() string { return string(key) } @@ -243,6 +245,17 @@ func (e *Event) AddMeta(k MetadataKey, v any) { e.Metadata[k] = v } +// AddSliceMetaOrAppend puts the provided string into the slice if the key +// doesn't exist or appends the string to the slice. +func (e *Event) AddSliceMetaOrAppend(k MetadataKey, s string) { + if e.ContainsMeta(k) { + v := append(e.GetMeta(k).([]string), s) + e.AddMeta(k, v) + } else { + e.AddMeta(k, []string{s}) + } +} + // RemoveMeta removes the event metadata index by given key. func (e *Event) RemoveMeta(k MetadataKey) { e.mmux.Lock() @@ -262,6 +275,13 @@ func (e *Event) GetMetaAsString(k MetadataKey) string { return "" } +// GetMeta returns the metadata for the given key. +func (e *Event) GetMeta(k MetadataKey) any { + e.mmux.RLock() + defer e.mmux.RUnlock() + return e.Metadata[k] +} + // ContainsMeta returns true if the metadata contains the specified key. func (e *Event) ContainsMeta(k MetadataKey) bool { e.mmux.RLock() From ee4a3a3a44feaaf15e36166a0ab7d36ab62f3988 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Sun, 28 Sep 2025 18:27:47 +0200 Subject: [PATCH 40/42] feat(filter): Process token filter fields Introduces the filter fields for the current, child, and parent process token integrity level, elevation type, and the elevation indicator (whether the process token was elevated or not). --- pkg/event/param_windows.go | 4 +- pkg/event/params/params_windows.go | 4 +- pkg/filter/accessor_windows.go | 59 ++++++++- pkg/filter/fields/fields_windows.go | 187 ++++++++++++++++------------ pkg/filter/filter_test.go | 70 +++++++---- pkg/filter/ql/function.go | 6 + pkg/ps/snapshotter_windows.go | 6 +- pkg/ps/snapshotter_windows_test.go | 36 +++--- 8 files changed, 241 insertions(+), 131 deletions(-) diff --git a/pkg/event/param_windows.go b/pkg/event/param_windows.go index 0130265b8..1054260b7 100644 --- a/pkg/event/param_windows.go +++ b/pkg/event/param_windows.go @@ -82,7 +82,7 @@ func (p Param) String() string { if err != nil { return "" } - if p.Name == params.ProcessIntegrityLevel { + if p.Name == params.ProcessTokenIntegrityLevel { return sys.RidToString(sid) } return sid.String() @@ -312,7 +312,7 @@ func (e *Event) produceParams(evt *etw.EventRecord) { e.AppendParam(params.ProcessFlags, params.Flags, flags, WithFlags(PsCreationFlags)) e.AppendParam(params.ProcessTokenElevationType, params.Enum, tokenElevationType, WithEnum(PsTokenElevationTypes)) e.AppendParam(params.ProcessTokenIsElevated, params.Bool, tokenIsElevated > 0) - e.AppendParam(params.ProcessIntegrityLevel, params.SID, tokenMandatoryLabel) + e.AppendParam(params.ProcessTokenIntegrityLevel, params.SID, tokenMandatoryLabel) e.AppendParam(params.Exe, params.DOSPath, exe) case OpenProcess: processID := evt.ReadUint32(0) diff --git a/pkg/event/params/params_windows.go b/pkg/event/params/params_windows.go index 1a860094d..716719496 100644 --- a/pkg/event/params/params_windows.go +++ b/pkg/event/params/params_windows.go @@ -58,8 +58,8 @@ const ( ExitStatus = "exit_status" // StartTime field denotes the process start time. StartTime = "start_time" - // ProcessIntegrityLevel field denotes the process integrity level. - ProcessIntegrityLevel = "integrity_level" + // ProcessTokenIntegrityLevel field denotes the process integrity level. + ProcessTokenIntegrityLevel = "token_integrity_level" // ProcessTokenElevationType field designates the process token elevation type. ProcessTokenElevationType = "token_elevation_type" // ProcessTokenIsElevated field designates if the process token is elevated. diff --git a/pkg/filter/accessor_windows.go b/pkg/filter/accessor_windows.go index d2ea35fd6..aa18c401d 100644 --- a/pkg/filter/accessor_windows.go +++ b/pkg/filter/accessor_windows.go @@ -430,11 +430,64 @@ func (ps *psAccessor) Get(f Field, e *event.Event) (params.Value, error) { return nil, ErrPsNil } return ps.IsProtected, nil + case fields.PsChildTokenIntegrityLevel: + if e.Category != event.Process { + return nil, nil + } + return e.GetParamAsString(params.ProcessTokenIntegrityLevel), nil + case fields.PsChildTokenIsElevated: + if e.Category != event.Process { + return nil, nil + } + return e.Params.GetBool(params.ProcessTokenIsElevated) + case fields.PsChildTokenElevationType: + if e.Category != event.Process { + return nil, nil + } + return e.GetParamAsString(params.ProcessTokenElevationType), nil + case fields.PsTokenIntegrityLevel: + ps := e.PS + if ps == nil { + return nil, ErrPsNil + } + return ps.TokenIntegrityLevel, nil + case fields.PsTokenElevationType: + ps := e.PS + if ps == nil { + return nil, ErrPsNil + } + return ps.TokenElevationType, nil + case fields.PsTokenIsElevated: + ps := e.PS + if ps == nil { + return nil, ErrPsNil + } + return ps.IsTokenElevated, nil + case fields.PsParentTokenIntegrityLevel: + ps := getParentPs(e) + if ps == nil { + return nil, ErrPsNil + } + return ps.TokenIntegrityLevel, nil + case fields.PsParentTokenElevationType: + ps := getParentPs(e) + if ps == nil { + return nil, ErrPsNil + } + return ps.TokenElevationType, nil + case fields.PsParentTokenIsElevated: + ps := getParentPs(e) + if ps == nil { + return nil, ErrPsNil + } + return ps.IsTokenElevated, nil case fields.PsAncestors: if e.PS != nil { ancestors := make([]*pstypes.PS, 0) walk := func(proc *pstypes.PS) { - ancestors = append(ancestors, proc) + if proc != nil { + ancestors = append(ancestors, proc) + } } pstypes.Walk(walk, e.PS) @@ -474,7 +527,9 @@ func (ps *psAccessor) Get(f Field, e *event.Event) (params.Value, error) { ancestors := make([]string, 0) walk := func(proc *pstypes.PS) { - ancestors = append(ancestors, proc.Name) + if proc != nil { + ancestors = append(ancestors, proc.Name) + } } pstypes.Walk(walk, e.PS) diff --git a/pkg/filter/fields/fields_windows.go b/pkg/filter/fields/fields_windows.go index 84904fe02..26517e88f 100644 --- a/pkg/filter/fields/fields_windows.go +++ b/pkg/filter/fields/fields_windows.go @@ -165,6 +165,24 @@ const ( PsChildIsPackagedField Field = "ps.child.is_packaged" // PsChildIsProtectedField represents the field that indicates if the process is to be run as a protected process PsChildIsProtectedField Field = "ps.child.is_protected" + // PsTokenIntegrityLevel represents the field that indicates the current process integrity level + PsTokenIntegrityLevel = "ps.token.integrity_level" + // PsTokenIsElevated represents the field that indicates if the current process token is elevated + PsTokenIsElevated = "ps.token.is_elevated" + // PsTokenElevationType represents the field that indicates if the current process token elevation type + PsTokenElevationType = "ps.token.elevation_type" + // PsChildTokenIntegrityLevel represents the field that indicates the created/child process integrity level + PsChildTokenIntegrityLevel = "ps.child.token.integrity_level" + // PsChildTokenIsElevated represents the field that indicates if the created/child process token is elevated + PsChildTokenIsElevated = "ps.child.token.is_elevated" + // PsChildTokenElevationType represents the field that indicates if the created/child process token elevation type + PsChildTokenElevationType = "ps.child.token.elevation_type" + // PsParentTokenIntegrityLevel represents the field that indicates the parent process integrity level + PsParentTokenIntegrityLevel = "ps.parent.token.integrity_level" + // PsParentTokenIsElevated represents the field that indicates if the parent process token is elevated + PsParentTokenIsElevated = "ps.parent.token.is_elevated" + // PsTokenElevationType represents the field that indicates if the parent process token elevation type + PsParentTokenElevationType = "ps.parent.token.elevation_type" // ThreadBasePrio is the base thread priority ThreadBasePrio Field = "thread.prio" @@ -643,15 +661,18 @@ const ( EntropySegment Segment = "entropy" MD5Segment Segment = "md5" - PIDSegment Segment = "pid" - CmdlineSegment Segment = "cmdline" - ExeSegment Segment = "exe" - ArgsSegment Segment = "args" - CwdSegment Segment = "cwd" - SIDSegment Segment = "sid" - SessionIDSegment Segment = "sessionid" - UsernameSegment Segment = "username" - DomainSegment Segment = "domain" + PIDSegment Segment = "pid" + CmdlineSegment Segment = "cmdline" + ExeSegment Segment = "exe" + ArgsSegment Segment = "args" + CwdSegment Segment = "cwd" + SIDSegment Segment = "sid" + SessionIDSegment Segment = "sessionid" + UsernameSegment Segment = "username" + DomainSegment Segment = "domain" + TokenIntegrityLevelSegment Segment = "token.integrity_level" + TokenIsElevatedSegment Segment = "token.is_elevated" + TokenElevationTypeSegment Segment = "token.elevation_type" TidSegment Segment = "tid" StartAddressSegment Segment = "start_address" @@ -692,6 +713,9 @@ var segments = map[Segment]bool{ SessionIDSegment: true, UsernameSegment: true, DomainSegment: true, + TokenIntegrityLevelSegment: true, + TokenIsElevatedSegment: true, + TokenElevationTypeSegment: true, TidSegment: true, StartAddressSegment: true, UserStackBaseSegment: true, @@ -713,7 +737,7 @@ var segments = map[Segment]bool{ } var allowedSegments = map[Field][]Segment{ - PsAncestors: {NameSegment, PIDSegment, CmdlineSegment, ExeSegment, ArgsSegment, CwdSegment, SIDSegment, SessionIDSegment, UsernameSegment, DomainSegment}, + PsAncestors: {NameSegment, PIDSegment, CmdlineSegment, ExeSegment, ArgsSegment, CwdSegment, SIDSegment, SessionIDSegment, UsernameSegment, DomainSegment, TokenIntegrityLevelSegment, TokenIsElevatedSegment, TokenElevationTypeSegment}, PsThreads: {TidSegment, StartAddressSegment, UserStackBaseSegment, UserStackLimitSegment, KernelStackBaseSegment, KernelStackLimitSegment}, PsModules: {PathSegment, NameSegment, AddressSegment, SizeSegment, ChecksumSegment}, PsMmaps: {AddressSegment, TypeSegment, AddressSegment, SizeSegment, ProtectionSegment, PathSegment}, @@ -846,73 +870,82 @@ var fields = map[Field]FieldInfo{ return true }}}, - PsPid: {PsPid, "process identifier", params.PID, []string{"ps.pid = 1024"}, nil, nil}, - PsPpid: {PsPpid, "parent process identifier", params.PID, []string{"ps.ppid = 45"}, nil, nil}, - PsName: {PsName, "process image name including the file extension", params.UnicodeString, []string{"ps.name contains 'firefox'"}, nil, nil}, - PsComm: {PsComm, "process command line", params.UnicodeString, []string{"ps.comm contains 'java'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsCmdline}}, nil}, - PsCmdline: {PsCmdline, "process command line", params.UnicodeString, []string{"ps.cmdline contains 'java'"}, nil, nil}, - PsExe: {PsExe, "full name of the process' executable", params.UnicodeString, []string{"ps.exe = 'C:\\Windows\\system32\\cmd.exe'"}, nil, nil}, - PsArgs: {PsArgs, "process command line arguments", params.Slice, []string{"ps.args in ('/cdir', '/-C')"}, nil, nil}, - PsCwd: {PsCwd, "process current working directory", params.UnicodeString, []string{"ps.cwd = 'C:\\Users\\Default'"}, nil, nil}, - PsSID: {PsSID, "security identifier under which this process is run", params.UnicodeString, []string{"ps.sid contains 'SYSTEM'"}, nil, nil}, - PsSessionID: {PsSessionID, "unique identifier for the current session", params.Int16, []string{"ps.sessionid = 1"}, nil, nil}, - PsDomain: {PsDomain, "process domain", params.UnicodeString, []string{"ps.domain contains 'SERVICE'"}, nil, nil}, - PsUsername: {PsUsername, "process username", params.UnicodeString, []string{"ps.username contains 'system'"}, nil, nil}, - PsEnvs: {PsEnvs, "process environment variables", params.Slice, []string{"ps.envs in ('SystemRoot:C:\\WINDOWS')", "ps.envs[windir] = 'C:\\WINDOWS'"}, nil, &Argument{Optional: true, ValidationFunc: func(arg string) bool { return true }}}, - PsHandleNames: {PsHandleNames, "allocated process handle names", params.Slice, []string{"ps.handles in ('\\BaseNamedObjects\\__ComCatalogCache__')"}, nil, nil}, - PsHandleTypes: {PsHandleTypes, "allocated process handle types", params.Slice, []string{"ps.handle.types in ('Key', 'Mutant', 'Section')"}, nil, nil}, - PsDTB: {PsDTB, "process directory table base address", params.Address, []string{"ps.dtb = '7ffe0000'"}, nil, nil}, - PsModuleNames: {PsModuleNames, "modules loaded by the process", params.Slice, []string{"ps.modules in ('crypt32.dll', 'xul.dll')"}, nil, nil}, - PsParentName: {PsParentName, "parent process image name including the file extension", params.UnicodeString, []string{"ps.parent.name contains 'cmd.exe'"}, nil, nil}, - PsParentPid: {PsParentPid, "parent process id", params.Uint32, []string{"ps.parent.pid = 4"}, nil, nil}, - PsParentComm: {PsParentComm, "parent process command line", params.UnicodeString, []string{"ps.parent.comm contains 'java'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsParentCmdline}}, nil}, - PsParentCmdline: {PsParentCmdline, "parent process command line", params.UnicodeString, []string{"ps.parent.cmdline contains 'java'"}, nil, nil}, - PsParentExe: {PsParentExe, "full name of the parent process' executable", params.UnicodeString, []string{"ps.parent.exe = 'C:\\Windows\\system32\\explorer.exe'"}, nil, nil}, - PsParentArgs: {PsParentArgs, "parent process command line arguments", params.Slice, []string{"ps.parent.args in ('/cdir', '/-C')"}, nil, nil}, - PsParentCwd: {PsParentCwd, "parent process current working directory", params.UnicodeString, []string{"ps.parent.cwd = 'C:\\Temp'"}, nil, nil}, - PsParentSID: {PsParentSID, "security identifier under which the parent process is run", params.UnicodeString, []string{"ps.parent.sid contains 'SYSTEM'"}, nil, nil}, - PsParentDomain: {PsParentDomain, "parent process domain", params.UnicodeString, []string{"ps.parent.domain contains 'SERVICE'"}, nil, nil}, - PsParentUsername: {PsParentUsername, "parent process username", params.UnicodeString, []string{"ps.parent.username contains 'system'"}, nil, nil}, - PsParentSessionID: {PsParentSessionID, "unique identifier for the current session of parent process", params.Int16, []string{"ps.parent.sessionid = 1"}, nil, nil}, - PsParentEnvs: {PsParentEnvs, "parent process environment variables", params.Slice, []string{"ps.parent.envs in ('MOZ_CRASHREPORTER_DATA_DIRECTORY')"}, nil, nil}, - PsParentHandles: {PsParentHandles, "allocated parent process handle names", params.Slice, []string{"ps.parent.handles in ('\\BaseNamedObjects\\__ComCatalogCache__')"}, nil, nil}, - PsParentHandleTypes: {PsParentHandleTypes, "allocated parent process handle types", params.Slice, []string{"ps.parent.handle.types in ('File', 'SymbolicLink')"}, nil, nil}, - PsParentDTB: {PsParentDTB, "parent process directory table base address", params.Address, []string{"ps.parent.dtb = '7ffe0000'"}, nil, nil}, - PsAccessMask: {PsAccessMask, "process desired access rights", params.AnsiString, []string{"ps.access.mask = '0x1400'"}, nil, nil}, - PsAccessMaskNames: {PsAccessMaskNames, "process desired access rights as a string list", params.Slice, []string{"ps.access.mask.names in ('SUSPEND_RESUME')"}, nil, nil}, - PsAccessStatus: {PsAccessStatus, "process access status", params.UnicodeString, []string{"ps.access.status = 'access is denied.'"}, nil, nil}, - PsSiblingPid: {PsSiblingPid, "created or terminated process identifier", params.PID, []string{"ps.sibling.pid = 320"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildPid}}, nil}, - PsChildPid: {PsChildPid, "created or terminated process identifier", params.PID, []string{"ps.child.pid = 320"}, nil, nil}, - PsSiblingName: {PsSiblingName, "created or terminated process name", params.UnicodeString, []string{"ps.sibling.name = 'notepad.exe'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildName}}, nil}, - PsChildName: {PsChildName, "created or terminated process name", params.UnicodeString, []string{"ps.child.name = 'notepad.exe'"}, nil, nil}, - PsSiblingComm: {PsSiblingComm, "created or terminated process command line", params.UnicodeString, []string{"ps.sibling.comm contains '\\k \\v'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildCmdline}}, nil}, - PsChildCmdline: {PsChildCmdline, "created or terminated process command line", params.UnicodeString, []string{"ps.child.cmdline contains '\\k \\v'"}, nil, nil}, - PsSiblingArgs: {PsSiblingArgs, "created process command line arguments", params.Slice, []string{"ps.sibling.args in ('/cdir', '/-C')"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildArgs}}, nil}, - PsChildArgs: {PsChildArgs, "created process command line arguments", params.Slice, []string{"ps.child.args in ('/cdir', '/-C')"}, nil, nil}, - PsSiblingExe: {PsSiblingExe, "created, terminated, or opened process id", params.UnicodeString, []string{"ps.sibling.exe contains '\\Windows\\cmd.exe'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildExe}}, nil}, - PsChildExe: {PsChildExe, "created, terminated, or opened process id", params.UnicodeString, []string{"ps.child.exe contains '\\Windows\\cmd.exe'"}, nil, nil}, - PsSiblingSID: {PsSiblingSID, "created or terminated process security identifier", params.UnicodeString, []string{"ps.sibling.sid contains 'SERVICE'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildSID}}, nil}, - PsChildSID: {PsChildSID, "created or terminated process security identifier", params.UnicodeString, []string{"ps.child.sid contains 'SERVICE'"}, nil, nil}, - PsSiblingSessionID: {PsSiblingSessionID, "created or terminated process session identifier", params.Int16, []string{"ps.sibling.sessionid == 1"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildSessionID}}, nil}, - PsChildSessionID: {PsChildSessionID, "created or terminated process session identifier", params.Int16, []string{"ps.child.sessionid == 1"}, nil, nil}, - PsSiblingDomain: {PsSiblingDomain, "created or terminated process domain", params.UnicodeString, []string{"ps.sibling.domain contains 'SERVICE'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildDomain}}, nil}, - PsChildDomain: {PsChildDomain, "created or terminated process domain", params.UnicodeString, []string{"ps.child.domain contains 'SERVICE'"}, nil, nil}, - PsSiblingUsername: {PsSiblingUsername, "created or terminated process username", params.UnicodeString, []string{"ps.sibling.username contains 'system'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildUsername}}, nil}, - PsChildUsername: {PsChildUsername, "created or terminated process username", params.UnicodeString, []string{"ps.child.username contains 'system'"}, nil, nil}, - PsUUID: {PsUUID, "unique process identifier", params.Uint64, []string{"ps.uuid > 6000054355"}, nil, nil}, - PsParentUUID: {PsParentUUID, "unique parent process identifier", params.Uint64, []string{"ps.parent.uuid > 6000054355"}, nil, nil}, - PsChildUUID: {PsChildUUID, "unique child process identifier", params.Uint64, []string{"ps.child.uuid > 6000054355"}, nil, nil}, - PsChildPeFilename: {PsChildPeFilename, "original file name of the child process executable supplied at compile-time", params.UnicodeString, []string{"ps.child.pe.file.name = 'NOTEPAD.EXE'"}, nil, nil}, - PsChildIsWOW64Field: {PsChildIsWOW64Field, "indicates if the 32-bit child process is created in 64-bit Windows system", params.Bool, []string{"ps.child.is_wow64"}, nil, nil}, - PsChildIsPackagedField: {PsChildIsPackagedField, "indicates if the child process is packaged with the MSIX technology", params.Bool, []string{"ps.child.is_packaged"}, nil, nil}, - PsChildIsProtectedField: {PsChildIsProtectedField, "indicates if the child process is a protected process", params.Bool, []string{"ps.child.is_protected"}, nil, nil}, - PsIsWOW64Field: {PsIsWOW64Field, "indicates if the process generating the event is a 32-bit process created in 64-bit Windows system", params.Bool, []string{"ps.is_wow64"}, nil, nil}, - PsIsPackagedField: {PsIsPackagedField, "indicates if the process generating the event is packaged with the MSIX technology", params.Bool, []string{"ps.is_packaged"}, nil, nil}, - PsIsProtectedField: {PsIsProtectedField, "indicates if the process generating the event is a protected process", params.Bool, []string{"ps.is_protected"}, nil, nil}, - PsParentIsWOW64Field: {PsParentIsWOW64Field, "indicates if the parent process generating the event is a 32-bit process created in 64-bit Windows system", params.Bool, []string{"ps.parent.is_wow64"}, nil, nil}, - PsParentIsPackagedField: {PsParentIsPackagedField, "indicates if the parent process generating the event is packaged with the MSIX technology", params.Bool, []string{"ps.parent.is_packaged"}, nil, nil}, - PsParentIsProtectedField: {PsParentIsProtectedField, "indicates if the the parent process generating the event is a protected process", params.Bool, []string{"ps.parent.is_protected"}, nil, nil}, - PsAncestor: {PsAncestor, "the process ancestor name", params.UnicodeString, []string{"ps.ancestor[1] = 'svchost.exe'", "ps.ancestor in ('winword.exe')"}, nil, &Argument{Optional: true, Pattern: "[0-9]+", ValidationFunc: isNumber}}, + PsPid: {PsPid, "process identifier", params.PID, []string{"ps.pid = 1024"}, nil, nil}, + PsPpid: {PsPpid, "parent process identifier", params.PID, []string{"ps.ppid = 45"}, nil, nil}, + PsName: {PsName, "process image name including the file extension", params.UnicodeString, []string{"ps.name contains 'firefox'"}, nil, nil}, + PsComm: {PsComm, "process command line", params.UnicodeString, []string{"ps.comm contains 'java'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsCmdline}}, nil}, + PsCmdline: {PsCmdline, "process command line", params.UnicodeString, []string{"ps.cmdline contains 'java'"}, nil, nil}, + PsExe: {PsExe, "full name of the process' executable", params.UnicodeString, []string{"ps.exe = 'C:\\Windows\\system32\\cmd.exe'"}, nil, nil}, + PsArgs: {PsArgs, "process command line arguments", params.Slice, []string{"ps.args in ('/cdir', '/-C')"}, nil, nil}, + PsCwd: {PsCwd, "process current working directory", params.UnicodeString, []string{"ps.cwd = 'C:\\Users\\Default'"}, nil, nil}, + PsSID: {PsSID, "security identifier under which this process is run", params.UnicodeString, []string{"ps.sid contains 'SYSTEM'"}, nil, nil}, + PsSessionID: {PsSessionID, "unique identifier for the current session", params.Int16, []string{"ps.sessionid = 1"}, nil, nil}, + PsDomain: {PsDomain, "process domain", params.UnicodeString, []string{"ps.domain contains 'SERVICE'"}, nil, nil}, + PsUsername: {PsUsername, "process username", params.UnicodeString, []string{"ps.username contains 'system'"}, nil, nil}, + PsEnvs: {PsEnvs, "process environment variables", params.Slice, []string{"ps.envs in ('SystemRoot:C:\\WINDOWS')", "ps.envs[windir] = 'C:\\WINDOWS'"}, nil, &Argument{Optional: true, ValidationFunc: func(arg string) bool { return true }}}, + PsHandleNames: {PsHandleNames, "allocated process handle names", params.Slice, []string{"ps.handles in ('\\BaseNamedObjects\\__ComCatalogCache__')"}, nil, nil}, + PsHandleTypes: {PsHandleTypes, "allocated process handle types", params.Slice, []string{"ps.handle.types in ('Key', 'Mutant', 'Section')"}, nil, nil}, + PsDTB: {PsDTB, "process directory table base address", params.Address, []string{"ps.dtb = '7ffe0000'"}, nil, nil}, + PsModuleNames: {PsModuleNames, "modules loaded by the process", params.Slice, []string{"ps.modules in ('crypt32.dll', 'xul.dll')"}, nil, nil}, + PsParentName: {PsParentName, "parent process image name including the file extension", params.UnicodeString, []string{"ps.parent.name contains 'cmd.exe'"}, nil, nil}, + PsParentPid: {PsParentPid, "parent process id", params.Uint32, []string{"ps.parent.pid = 4"}, nil, nil}, + PsParentComm: {PsParentComm, "parent process command line", params.UnicodeString, []string{"ps.parent.comm contains 'java'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsParentCmdline}}, nil}, + PsParentCmdline: {PsParentCmdline, "parent process command line", params.UnicodeString, []string{"ps.parent.cmdline contains 'java'"}, nil, nil}, + PsParentExe: {PsParentExe, "full name of the parent process' executable", params.UnicodeString, []string{"ps.parent.exe = 'C:\\Windows\\system32\\explorer.exe'"}, nil, nil}, + PsParentArgs: {PsParentArgs, "parent process command line arguments", params.Slice, []string{"ps.parent.args in ('/cdir', '/-C')"}, nil, nil}, + PsParentCwd: {PsParentCwd, "parent process current working directory", params.UnicodeString, []string{"ps.parent.cwd = 'C:\\Temp'"}, nil, nil}, + PsParentSID: {PsParentSID, "security identifier under which the parent process is run", params.UnicodeString, []string{"ps.parent.sid contains 'SYSTEM'"}, nil, nil}, + PsParentDomain: {PsParentDomain, "parent process domain", params.UnicodeString, []string{"ps.parent.domain contains 'SERVICE'"}, nil, nil}, + PsParentUsername: {PsParentUsername, "parent process username", params.UnicodeString, []string{"ps.parent.username contains 'system'"}, nil, nil}, + PsParentSessionID: {PsParentSessionID, "unique identifier for the current session of parent process", params.Int16, []string{"ps.parent.sessionid = 1"}, nil, nil}, + PsParentEnvs: {PsParentEnvs, "parent process environment variables", params.Slice, []string{"ps.parent.envs in ('MOZ_CRASHREPORTER_DATA_DIRECTORY')"}, nil, nil}, + PsParentHandles: {PsParentHandles, "allocated parent process handle names", params.Slice, []string{"ps.parent.handles in ('\\BaseNamedObjects\\__ComCatalogCache__')"}, nil, nil}, + PsParentHandleTypes: {PsParentHandleTypes, "allocated parent process handle types", params.Slice, []string{"ps.parent.handle.types in ('File', 'SymbolicLink')"}, nil, nil}, + PsParentDTB: {PsParentDTB, "parent process directory table base address", params.Address, []string{"ps.parent.dtb = '7ffe0000'"}, nil, nil}, + PsAccessMask: {PsAccessMask, "process desired access rights", params.AnsiString, []string{"ps.access.mask = '0x1400'"}, nil, nil}, + PsAccessMaskNames: {PsAccessMaskNames, "process desired access rights as a string list", params.Slice, []string{"ps.access.mask.names in ('SUSPEND_RESUME')"}, nil, nil}, + PsAccessStatus: {PsAccessStatus, "process access status", params.UnicodeString, []string{"ps.access.status = 'access is denied.'"}, nil, nil}, + PsSiblingPid: {PsSiblingPid, "created or terminated process identifier", params.PID, []string{"ps.sibling.pid = 320"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildPid}}, nil}, + PsChildPid: {PsChildPid, "created or terminated process identifier", params.PID, []string{"ps.child.pid = 320"}, nil, nil}, + PsSiblingName: {PsSiblingName, "created or terminated process name", params.UnicodeString, []string{"ps.sibling.name = 'notepad.exe'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildName}}, nil}, + PsChildName: {PsChildName, "created or terminated process name", params.UnicodeString, []string{"ps.child.name = 'notepad.exe'"}, nil, nil}, + PsSiblingComm: {PsSiblingComm, "created or terminated process command line", params.UnicodeString, []string{"ps.sibling.comm contains '\\k \\v'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildCmdline}}, nil}, + PsChildCmdline: {PsChildCmdline, "created or terminated process command line", params.UnicodeString, []string{"ps.child.cmdline contains '\\k \\v'"}, nil, nil}, + PsSiblingArgs: {PsSiblingArgs, "created process command line arguments", params.Slice, []string{"ps.sibling.args in ('/cdir', '/-C')"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildArgs}}, nil}, + PsChildArgs: {PsChildArgs, "created process command line arguments", params.Slice, []string{"ps.child.args in ('/cdir', '/-C')"}, nil, nil}, + PsSiblingExe: {PsSiblingExe, "created, terminated, or opened process id", params.UnicodeString, []string{"ps.sibling.exe contains '\\Windows\\cmd.exe'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildExe}}, nil}, + PsChildExe: {PsChildExe, "created, terminated, or opened process id", params.UnicodeString, []string{"ps.child.exe contains '\\Windows\\cmd.exe'"}, nil, nil}, + PsSiblingSID: {PsSiblingSID, "created or terminated process security identifier", params.UnicodeString, []string{"ps.sibling.sid contains 'SERVICE'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildSID}}, nil}, + PsChildSID: {PsChildSID, "created or terminated process security identifier", params.UnicodeString, []string{"ps.child.sid contains 'SERVICE'"}, nil, nil}, + PsSiblingSessionID: {PsSiblingSessionID, "created or terminated process session identifier", params.Int16, []string{"ps.sibling.sessionid == 1"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildSessionID}}, nil}, + PsChildSessionID: {PsChildSessionID, "created or terminated process session identifier", params.Int16, []string{"ps.child.sessionid == 1"}, nil, nil}, + PsSiblingDomain: {PsSiblingDomain, "created or terminated process domain", params.UnicodeString, []string{"ps.sibling.domain contains 'SERVICE'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildDomain}}, nil}, + PsChildDomain: {PsChildDomain, "created or terminated process domain", params.UnicodeString, []string{"ps.child.domain contains 'SERVICE'"}, nil, nil}, + PsSiblingUsername: {PsSiblingUsername, "created or terminated process username", params.UnicodeString, []string{"ps.sibling.username contains 'system'"}, &Deprecation{Since: "1.10.0", Fields: []Field{PsChildUsername}}, nil}, + PsChildUsername: {PsChildUsername, "created or terminated process username", params.UnicodeString, []string{"ps.child.username contains 'system'"}, nil, nil}, + PsUUID: {PsUUID, "unique process identifier", params.Uint64, []string{"ps.uuid > 6000054355"}, nil, nil}, + PsParentUUID: {PsParentUUID, "unique parent process identifier", params.Uint64, []string{"ps.parent.uuid > 6000054355"}, nil, nil}, + PsChildUUID: {PsChildUUID, "unique child process identifier", params.Uint64, []string{"ps.child.uuid > 6000054355"}, nil, nil}, + PsChildPeFilename: {PsChildPeFilename, "original file name of the child process executable supplied at compile-time", params.UnicodeString, []string{"ps.child.pe.file.name = 'NOTEPAD.EXE'"}, nil, nil}, + PsChildIsWOW64Field: {PsChildIsWOW64Field, "indicates if the 32-bit child process is created in 64-bit Windows system", params.Bool, []string{"ps.child.is_wow64"}, nil, nil}, + PsChildIsPackagedField: {PsChildIsPackagedField, "indicates if the child process is packaged with the MSIX technology", params.Bool, []string{"ps.child.is_packaged"}, nil, nil}, + PsChildIsProtectedField: {PsChildIsProtectedField, "indicates if the child process is a protected process", params.Bool, []string{"ps.child.is_protected"}, nil, nil}, + PsIsWOW64Field: {PsIsWOW64Field, "indicates if the process generating the event is a 32-bit process created in 64-bit Windows system", params.Bool, []string{"ps.is_wow64"}, nil, nil}, + PsIsPackagedField: {PsIsPackagedField, "indicates if the process generating the event is packaged with the MSIX technology", params.Bool, []string{"ps.is_packaged"}, nil, nil}, + PsIsProtectedField: {PsIsProtectedField, "indicates if the process generating the event is a protected process", params.Bool, []string{"ps.is_protected"}, nil, nil}, + PsParentIsWOW64Field: {PsParentIsWOW64Field, "indicates if the parent process generating the event is a 32-bit process created in 64-bit Windows system", params.Bool, []string{"ps.parent.is_wow64"}, nil, nil}, + PsParentIsPackagedField: {PsParentIsPackagedField, "indicates if the parent process generating the event is packaged with the MSIX technology", params.Bool, []string{"ps.parent.is_packaged"}, nil, nil}, + PsParentIsProtectedField: {PsParentIsProtectedField, "indicates if the the parent process generating the event is a protected process", params.Bool, []string{"ps.parent.is_protected"}, nil, nil}, + PsAncestor: {PsAncestor, "the process ancestor name", params.UnicodeString, []string{"ps.ancestor[1] = 'svchost.exe'", "ps.ancestor in ('winword.exe')"}, nil, &Argument{Optional: true, Pattern: "[0-9]+", ValidationFunc: isNumber}}, + PsTokenIntegrityLevel: {PsTokenIntegrityLevel, "process token integrity level", params.UnicodeString, []string{"ps.token.integrity_level = 'SYSTEM'"}, nil, nil}, + PsTokenIsElevated: {PsTokenIsElevated, "indicates if the process token is elevated", params.Bool, []string{"ps.token.is_elevated = true"}, nil, nil}, + PsTokenElevationType: {PsTokenElevationType, "process token elevation type", params.AnsiString, []string{"ps.token.elevation_type = 'LIMITED'"}, nil, nil}, + PsChildTokenIntegrityLevel: {PsChildTokenIntegrityLevel, "child process token integrity level", params.UnicodeString, []string{"ps.child.token.integrity_level = 'SYSTEM'"}, nil, nil}, + PsChildTokenIsElevated: {PsChildTokenIsElevated, "indicates if the child process token is elevated", params.Bool, []string{"ps.child.token.is_elevated = true"}, nil, nil}, + PsChildTokenElevationType: {PsChildTokenElevationType, "child process token elevation type", params.AnsiString, []string{"ps.child.token.elevation_type = 'LIMITED'"}, nil, nil}, + PsParentTokenIntegrityLevel: {PsParentTokenIntegrityLevel, "parent process token integrity level", params.UnicodeString, []string{"ps.parent.token.integrity_level = 'HIGH'"}, nil, nil}, + PsParentTokenIsElevated: {PsParentTokenIsElevated, "indicates if the parent process token is elevated", params.Bool, []string{"ps.parent.token.is_elevated = true"}, nil, nil}, + PsParentTokenElevationType: {PsParentTokenElevationType, "parent process token elevation type", params.AnsiString, []string{"ps.parent.token.elevation_type = 'LIMITED'"}, nil, nil}, ThreadBasePrio: {ThreadBasePrio, "scheduler priority of the thread", params.Int8, []string{"thread.prio = 5"}, nil, nil}, ThreadIOPrio: {ThreadIOPrio, "I/O priority hint for scheduling I/O operations", params.Int8, []string{"thread.io.prio = 4"}, nil, nil}, diff --git a/pkg/filter/filter_test.go b/pkg/filter/filter_test.go index 2b46669fc..c658da0f8 100644 --- a/pkg/filter/filter_test.go +++ b/pkg/filter/filter_test.go @@ -109,21 +109,6 @@ func TestStringFields(t *testing.T) { } func TestProcFilter(t *testing.T) { - pars := event.Params{ - params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\svchost-fake.exe -k RPCSS"}, - params.ProcessName: {Name: params.ProcessName, Type: params.AnsiString, Value: "svchost-fake.exe"}, - params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1234)}, - params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(345)}, - params.UserSID: {Name: params.UserSID, Type: params.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, - params.Username: {Name: params.Username, Type: params.UnicodeString, Value: "loki"}, - params.Domain: {Name: params.Domain, Type: params.UnicodeString, Value: "TITAN"}, - params.ProcessFlags: {Name: params.ProcessFlags, Type: params.Flags, Value: uint32(0x000000E)}, - } - - pars1 := event.Params{ - params.DesiredAccess: {Name: params.DesiredAccess, Type: params.Flags, Value: uint32(0x1400), Flags: event.PsAccessRightFlags}, - } - ps1 := &pstypes.PS{ Name: "wininit.exe", Username: "SYSTEM", @@ -138,17 +123,32 @@ func TestProcFilter(t *testing.T) { Name: "System", }, }, - IsWOW64: false, - IsProtected: true, - IsPackaged: false, + IsWOW64: false, + IsProtected: true, + IsPackaged: false, + TokenIntegrityLevel: "SYSTEM", + IsTokenElevated: false, + TokenElevationType: "DEFAULT", } evt := &event.Event{ Type: event.CreateProcess, Category: event.Process, - Params: pars, - Name: "CreateProcess", - PID: 1023, + Params: event.Params{ + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\svchost-fake.exe -k RPCSS"}, + params.ProcessName: {Name: params.ProcessName, Type: params.AnsiString, Value: "svchost-fake.exe"}, + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1234)}, + params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(345)}, + params.UserSID: {Name: params.UserSID, Type: params.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, + params.Username: {Name: params.Username, Type: params.UnicodeString, Value: "loki"}, + params.Domain: {Name: params.Domain, Type: params.UnicodeString, Value: "TITAN"}, + params.ProcessFlags: {Name: params.ProcessFlags, Type: params.Flags, Value: uint32(0x000000E)}, + params.ProcessTokenIntegrityLevel: {Name: params.ProcessTokenIntegrityLevel, Type: params.AnsiString, Value: "SYSTEM"}, + params.ProcessTokenIsElevated: {Name: params.ProcessTokenIsElevated, Type: params.Bool, Value: true}, + params.ProcessTokenElevationType: {Name: params.ProcessTokenElevationType, Type: params.AnsiString, Value: "FULL"}, + }, + Name: "CreateProcess", + PID: 1023, PS: &pstypes.PS{ Name: "svchost.exe", Cmdline: "C:\\Windows\\System32\\svchost.exe", @@ -171,9 +171,12 @@ func TestProcFilter(t *testing.T) { {Size: 34545, BaseAddress: va.Address(144229524944769), Protection: 4653056, File: "C:\\Windows\\System32\\ucrtbase.dll", Type: "IMAGE"}, //EXECUTE_READWRITE|READONLY {Size: 4096, BaseAddress: va.Address(145229445447666), Protection: 12845056, Type: "PAGEFILE"}, // READWRITE 12845056 }, - IsProtected: false, - IsPackaged: true, - IsWOW64: false, + IsProtected: false, + IsPackaged: true, + IsWOW64: false, + TokenIntegrityLevel: "SYSTEM", + IsTokenElevated: false, + TokenElevationType: "DEFAULT", }, } evt.Timestamp, _ = time.Parse(time.RFC3339, "2011-05-03T15:04:05.323Z") @@ -181,9 +184,11 @@ func TestProcFilter(t *testing.T) { evt1 := &event.Event{ Type: event.OpenProcess, Category: event.Process, - Params: pars1, - Name: "OpenProcess", - PID: 1023, + Params: event.Params{ + params.DesiredAccess: {Name: params.DesiredAccess, Type: params.Flags, Value: uint32(0x1400), Flags: event.PsAccessRightFlags}, + }, + Name: "OpenProcess", + PID: 1023, PS: &pstypes.PS{ Name: "svchost.exe", Parent: ps1, @@ -240,6 +245,16 @@ func TestProcFilter(t *testing.T) { {`ps.parent.is_wow64`, false}, {`ps.parent.is_packaged`, false}, {`ps.parent.is_protected`, true}, + {`ps.token.integrity_level = 'SYSTEM'`, true}, + {`ps.token.is_elevated = false`, true}, + {`ps.token.elevation_type = 'DEFAULT'`, true}, + {`ps.child.token.integrity_level = 'SYSTEM'`, true}, + {`ps.child.token.is_elevated = true`, true}, + {`ps.child.token.elevation_type = 'FULL'`, true}, + {`ps.parent.token.integrity_level = 'SYSTEM'`, true}, + {`ps.parent.token.is_elevated = false`, true}, + {`ps.parent.token.elevation_type = 'DEFAULT'`, true}, + {`evt.name = 'CreateProcess' and ps.name contains 'svchost'`, true}, {`ps.modules IN ('kernel32.dll')`, true}, @@ -262,6 +277,7 @@ func TestProcFilter(t *testing.T) { {`foreach(ps._ancestors, $proc, $proc.username = 'SYSTEM')`, true}, {`foreach(ps._ancestors, $proc, $proc.domain = 'NT AUTHORITY')`, true}, {`foreach(ps._ancestors, $proc, $proc.username = upper('system'))`, true}, + {`foreach(ps._ancestors, $proc, $proc.token.integrity_level = 'SYSTEM' and $proc.token.is_elevated = false and $proc.token.elevation_type = 'DEFAULT')`, true}, {`ps.args intersects ('-k', 'DcomLaunch')`, true}, {`ps.args intersects ('-w', 'DcomLaunch')`, false}, diff --git a/pkg/filter/ql/function.go b/pkg/filter/ql/function.go index e4c0c5982..fa213415e 100644 --- a/pkg/filter/ql/function.go +++ b/pkg/filter/ql/function.go @@ -487,6 +487,12 @@ func (f *Foreach) procMapValuer(segments []*BoundSegmentLiteral, proc *pstypes.P valuer[key] = proc.Username case fields.DomainSegment: valuer[key] = proc.Domain + case fields.TokenIntegrityLevelSegment: + valuer[key] = proc.TokenIntegrityLevel + case fields.TokenIsElevatedSegment: + valuer[key] = proc.IsTokenElevated + case fields.TokenElevationTypeSegment: + valuer[key] = proc.TokenElevationType } } return valuer diff --git a/pkg/ps/snapshotter_windows.go b/pkg/ps/snapshotter_windows.go index d95b475ef..68801b5ac 100644 --- a/pkg/ps/snapshotter_windows.go +++ b/pkg/ps/snapshotter_windows.go @@ -185,7 +185,7 @@ func (s *snapshotter) Write(e *event.Event) error { e.AppendParam(params.Exe, params.Path, ps.Exe) } - e.AppendParam(params.ProcessIntegrityLevel, params.AnsiString, ps.TokenIntegrityLevel) + e.AppendParam(params.ProcessTokenIntegrityLevel, params.AnsiString, ps.TokenIntegrityLevel) e.AppendParam(params.ProcessTokenElevationType, params.AnsiString, ps.TokenElevationType) e.AppendParam(params.ProcessTokenIsElevated, params.Bool, ps.IsTokenElevated) @@ -370,7 +370,7 @@ func (s *snapshotter) newProcState(pid, ppid uint32, e *event.Event) (*pstypes.P Ppid: ppid, Exe: e.GetParamAsString(params.Exe), Name: filepath.Base(e.GetParamAsString(params.Exe)), - TokenIntegrityLevel: e.GetParamAsString(params.ProcessIntegrityLevel), + TokenIntegrityLevel: e.GetParamAsString(params.ProcessTokenIntegrityLevel), TokenElevationType: e.GetParamAsString(params.ProcessTokenElevationType), IsTokenElevated: e.Params.TryGetBool(params.ProcessTokenIsElevated), Threads: make(map[uint32]pstypes.Thread), @@ -460,7 +460,7 @@ func (s *snapshotter) newProcState(pid, ppid uint32, e *event.Event) (*pstypes.P proc.TokenIntegrityLevel = sys.RidToString(tokenMandatoryLabel.Label.Sid) proc.IsTokenElevated = token.IsElevated() - e.AppendParam(params.ProcessIntegrityLevel, params.AnsiString, proc.TokenIntegrityLevel) + e.AppendParam(params.ProcessTokenIntegrityLevel, params.AnsiString, proc.TokenIntegrityLevel) e.AppendParam(params.ProcessTokenIsElevated, params.Bool, proc.IsTokenElevated) } diff --git a/pkg/ps/snapshotter_windows_test.go b/pkg/ps/snapshotter_windows_test.go index 92b1879fd..254c76d2f 100644 --- a/pkg/ps/snapshotter_windows_test.go +++ b/pkg/ps/snapshotter_windows_test.go @@ -200,12 +200,12 @@ func TestWriteInternalEventsEnrichment(t *testing.T) { { Type: event.CreateProcessInternal, Params: event.Params{ - params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1024)}, - params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(444)}, - params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: `C:\Windows\System32\svchost.exe`}, - params.ProcessIntegrityLevel: {Name: params.ProcessIntegrityLevel, Type: params.AnsiString, Value: "HIGH"}, - params.ProcessTokenIsElevated: {Name: params.ProcessTokenIsElevated, Type: params.Bool, Value: true}, - params.ProcessTokenElevationType: {Name: params.ProcessTokenElevationType, Type: params.AnsiString, Value: "FULL"}, + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1024)}, + params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(444)}, + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: `C:\Windows\System32\svchost.exe`}, + params.ProcessTokenIntegrityLevel: {Name: params.ProcessTokenIntegrityLevel, Type: params.AnsiString, Value: "HIGH"}, + params.ProcessTokenIsElevated: {Name: params.ProcessTokenIsElevated, Type: params.Bool, Value: true}, + params.ProcessTokenElevationType: {Name: params.ProcessTokenElevationType, Type: params.AnsiString, Value: "FULL"}, }, }, }, @@ -236,12 +236,12 @@ func TestWriteInternalEventsEnrichment(t *testing.T) { { Type: event.CreateProcessInternal, Params: event.Params{ - params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1024)}, - params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(444)}, - params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: `C:\Windows\System32\svchost.exe`}, - params.ProcessIntegrityLevel: {Name: params.ProcessIntegrityLevel, Type: params.AnsiString, Value: "HIGH"}, - params.ProcessTokenIsElevated: {Name: params.ProcessTokenIsElevated, Type: params.Bool, Value: true}, - params.ProcessTokenElevationType: {Name: params.ProcessTokenElevationType, Type: params.AnsiString, Value: "FULL"}, + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1024)}, + params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(444)}, + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: `C:\Windows\System32\svchost.exe`}, + params.ProcessTokenIntegrityLevel: {Name: params.ProcessTokenIntegrityLevel, Type: params.AnsiString, Value: "HIGH"}, + params.ProcessTokenIsElevated: {Name: params.ProcessTokenIsElevated, Type: params.Bool, Value: true}, + params.ProcessTokenElevationType: {Name: params.ProcessTokenElevationType, Type: params.AnsiString, Value: "FULL"}, }, }, }, @@ -262,12 +262,12 @@ func TestWriteInternalEventsEnrichment(t *testing.T) { { Type: event.CreateProcessInternal, Params: event.Params{ - params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1024)}, - params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(444)}, - params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: `C:\Windows\System32\svchost.exe`}, - params.ProcessIntegrityLevel: {Name: params.ProcessIntegrityLevel, Type: params.AnsiString, Value: "HIGH"}, - params.ProcessTokenIsElevated: {Name: params.ProcessTokenIsElevated, Type: params.Bool, Value: true}, - params.ProcessTokenElevationType: {Name: params.ProcessTokenElevationType, Type: params.AnsiString, Value: "FULL"}, + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1024)}, + params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(444)}, + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: `C:\Windows\System32\svchost.exe`}, + params.ProcessTokenIntegrityLevel: {Name: params.ProcessTokenIntegrityLevel, Type: params.AnsiString, Value: "HIGH"}, + params.ProcessTokenIsElevated: {Name: params.ProcessTokenIsElevated, Type: params.Bool, Value: true}, + params.ProcessTokenElevationType: {Name: params.ProcessTokenElevationType, Type: params.AnsiString, Value: "FULL"}, }, }, { From e68f46d2e3742a50f469c3d06b440326ccd5ff55 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Mon, 13 Oct 2025 18:48:14 +0200 Subject: [PATCH 41/42] fix(tests): Try to stabilize flaky tests --- internal/etw/source_test.go | 49 +++++++++++-------------------------- pkg/pe/parser_test.go | 7 +++--- 2 files changed, 18 insertions(+), 38 deletions(-) diff --git a/internal/etw/source_test.go b/internal/etw/source_test.go index 88b27c1ce..0447a01af 100644 --- a/internal/etw/source_test.go +++ b/internal/etw/source_test.go @@ -20,6 +20,18 @@ package etw import ( "context" "fmt" + "net" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "syscall" + "testing" + "time" + "unsafe" + "github.com/rabbitstack/fibratus/internal/evasion" "github.com/rabbitstack/fibratus/pkg/config" "github.com/rabbitstack/fibratus/pkg/event" @@ -39,17 +51,6 @@ import ( "github.com/stretchr/testify/require" "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" - "net" - "net/http" - "os" - "os/exec" - "path/filepath" - "runtime" - "strings" - "syscall" - "testing" - "time" - "unsafe" ) // MockListener receives the event and does nothing but indicating the event was processed. @@ -626,30 +627,8 @@ func TestEventSourceAllEvents(t *testing.T) { { "duplicate handle", func() error { - var si windows.StartupInfo - var pi windows.ProcessInformation - argv, err := windows.UTF16PtrFromString(filepath.Join(os.Getenv("windir"), "notepad.exe")) - if err != nil { - return err - } - err = windows.CreateProcess( - nil, - argv, - nil, - nil, - true, - 0, - nil, - nil, - &si, - &pi) - if err != nil { - return err - } - time.Sleep(time.Second) - defer windows.TerminateProcess(pi.Process, 0) hs := handle.NewSnapshotter(&config.Config{EnumerateHandles: true}, nil) - handles, err := hs.FindHandles(pi.ProcessId) + handles, err := hs.FindHandles(uint32(os.Getppid())) if err != nil { return err } @@ -660,7 +639,7 @@ func TestEventSourceAllEvents(t *testing.T) { } } assert.False(t, dupHandleID == 0) - dup, err := handle.Duplicate(dupHandleID, pi.ProcessId, windows.KEY_READ) + dup, err := handle.Duplicate(dupHandleID, uint32(os.Getppid()), 0) if err != nil { return err } diff --git a/pkg/pe/parser_test.go b/pkg/pe/parser_test.go index 9443e1384..28e3fe645 100644 --- a/pkg/pe/parser_test.go +++ b/pkg/pe/parser_test.go @@ -19,12 +19,13 @@ package pe import ( - "github.com/stretchr/testify/require" - "golang.org/x/sys/windows" "os" "path/filepath" "testing" "time" + + "github.com/stretchr/testify/require" + "golang.org/x/sys/windows" ) func TestParseFile(t *testing.T) { @@ -106,7 +107,7 @@ func TestParseMem(t *testing.T) { executable string expectedSections int }{ - {filepath.Join(os.Getenv("windir"), "notepad.exe"), 7}, + {filepath.Join(os.Getenv("windir"), "regedit.exe"), 8}, } for _, tt := range tests { From 11ab6872dac9526c0b102e8a2810f6a6af172510 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Mon, 29 Sep 2025 18:54:53 +0200 Subject: [PATCH 42/42] fix(filter): Provide accessor default value When the accessor fails or return a nil value, populate the valuer map with a default value. This is important to avoid rule matching misbehaviours. --- pkg/filter/accessor.go | 31 ++++++++++++++++++++++++++++- pkg/filter/fields/fields_windows.go | 3 +++ pkg/filter/filter.go | 13 ++++++------ pkg/filter/filter_test.go | 30 ++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 7 deletions(-) diff --git a/pkg/filter/accessor.go b/pkg/filter/accessor.go index 21e05dc72..bac4de3a8 100644 --- a/pkg/filter/accessor.go +++ b/pkg/filter/accessor.go @@ -23,7 +23,9 @@ import ( "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/filter/fields" + "net" "reflect" + "time" ) var ( @@ -62,7 +64,7 @@ func newEventAccessor() Accessor { const timeFmt = "15:04:05" const dateFmt = "2006-01-02" -func (k *evtAccessor) Get(f Field, evt *event.Event) (params.Value, error) { +func (*evtAccessor) Get(f Field, evt *event.Event) (params.Value, error) { switch f.Name { case fields.EvtSeq, fields.KevtSeq: return evt.Seq, nil @@ -238,3 +240,30 @@ func (f *filter) removeAccessor(removed Accessor) { } } } + +// defaultAccessorValue provides the default value for the field. +// This value is typically assigned when the accessor returns an +// error or nil value, but the map valuer must contain the resolved +// field name in case of filters using the not operator. +func defaultAccessorValue(field Field) any { + switch field.Name.Type() { + case params.Uint8, params.Int64, params.Int8, params.Int32, params.Int16, + params.Uint16, params.Port, params.Uint32, params.Uint64, params.PID, + params.TID, params.Flags, params.Flags64: + return 0 + case params.Float, params.Double: + return 0.0 + case params.Time: + return time.Now() + case params.Bool: + return false + case params.IP, params.IPv4, params.IPv6: + return net.IP{} + case params.Binary: + return []byte{} + case params.Slice: + return []string{} + default: + return "" + } +} diff --git a/pkg/filter/fields/fields_windows.go b/pkg/filter/fields/fields_windows.go index 26517e88f..435aa660a 100644 --- a/pkg/filter/fields/fields_windows.go +++ b/pkg/filter/fields/fields_windows.go @@ -609,6 +609,9 @@ const ( // String casts the field type to string. func (f Field) String() string { return string(f) } +// Type returns the data type that this field contains. +func (f Field) Type() params.Type { return fields[f].Type } + func (f Field) IsPsField() bool { return strings.HasPrefix(string(f), "ps.") } func (f Field) IsKevtField() bool { return strings.HasPrefix(string(f), "evt.") } func (f Field) IsThreadField() bool { return strings.HasPrefix(string(f), "thread.") } diff --git a/pkg/filter/filter.go b/pkg/filter/filter.go index ed92de1c4..09d2e7a49 100644 --- a/pkg/filter/filter.go +++ b/pkg/filter/filter.go @@ -423,14 +423,15 @@ func (f *filter) mapValuer(evt *event.Event) map[string]interface{} { continue } v, err := accessor.Get(field, evt) - if err != nil && !errs.IsParamNotFound(err) { - accessorErrors.Add(err.Error(), 1) + if v == nil || err != nil { + valuer[field.Value] = defaultAccessorValue(field) + if err != nil && !errs.IsParamNotFound(err) { + accessorErrors.Add(err.Error(), 1) + } continue } - if v != nil { - valuer[field.Value] = v - break - } + valuer[field.Value] = v + break } } return valuer diff --git a/pkg/filter/filter_test.go b/pkg/filter/filter_test.go index c658da0f8..f889b0d4f 100644 --- a/pkg/filter/filter_test.go +++ b/pkg/filter/filter_test.go @@ -201,6 +201,16 @@ func TestProcFilter(t *testing.T) { }, } + evt2 := &event.Event{ + Type: event.OpenProcess, + Category: event.Process, + Params: event.Params{ + params.DesiredAccess: {Name: params.DesiredAccess, Type: params.Flags, Value: uint32(0x1400), Flags: event.PsAccessRightFlags}, + }, + Name: "OpenProcess", + PID: 1023, + } + var tests = []struct { filter string matches bool @@ -340,6 +350,26 @@ func TestProcFilter(t *testing.T) { t.Errorf("%d. %q ps filter mismatch: exp=%t got=%t", i, tt.filter, tt.matches, matches) } } + + var tests2 = []struct { + filter string + matches bool + }{ + + {`ps.exe = ''`, true}, + } + + for i, tt := range tests2 { + f := New(tt.filter, cfg) + err := f.Compile() + if err != nil { + t.Fatal(err) + } + matches := f.Run(evt2) + if matches != tt.matches { + t.Errorf("%d. %q ps filter mismatch: exp=%t got=%t", i, tt.filter, tt.matches, matches) + } + } } func TestThreadFilter(t *testing.T) {

BwOvLXik0; zxq&C-rvgafE5M*o9us}_X`)C5Mt>I*q&MhHQh%Lu|_(=kbXLxUA>L}Cb~ie!nQ&VU>Cf`pK% zBzD(X{mCV0QK`!BWefBr|Cz)HW{-0xv1jpkk#iI?0QwbK{R zcj%bT9c!}d#hTImnMB4;Ro@HHSoF|PQGmnf8v)nKA?K+OLUjOka7?YpPD#vu8?1au zT<*6RLOK*8BYK3Q@l7!wb@v9>qteinpXjLmvzVTvzw`oS4SRS;fFSe>Jw7S#-=a89 zHU!%MX6IMi`ie<3W z|F-Mz)y6d0`=8dk;KT1SN0*)9;vpu(b`Jt~+n*e7(`FOk4g)wDPTdLp?d%OGe+RC> zfd8{-(>tOi8bo~gLdwmiCRZs=9Y|v6Cpo|Vm-{W{^fB||^;6!~{qh3)c3u4sa(sv+ z=L`1P+s%8Y4=2OV5!d!d0S2;1(!0bj|H4TsOEW z*Q5Z|d6=1*{)oVdDT$$t`1Eop6R^)esRxn3q)jc+7$9e0r5Y>TqQ80ob@T@-H(s8H&3O2*A7R$D$-Jn-U;%C=Cuu|Qe z&V#!o9U59JxlZL&vde;&b8BDMIK(fv&jmBE0v4T|P1ik*F(QQnD_B-D1~`L*#wEFv ziTf|ZTZ1TlqJsE9=K7;a{x{9(*_ij@Cr}RO_%QS{770Hl4G5TC$Q5Zl&{dHzLPMir z$f?l^R~nMbxPu|&0)gN%2armcJA*TqkFtUp3uybYT(y7St5Nl59eO@?1>MhSP73uu zds|qH6Sja)Or~{NX;I28?c>p(k027*14a0uq2A7nWOM7`vIl_-~E6<6m0E;e#MiL~<3oN$O###+1PB9*^95{=WM!0M< zy+$edHSdyL%H>a~(RsApc>_$l>ehduOzn1qrCwlYsHB-v7GrW^Ey7IVC@~)yOe`#` z566Ro1QzBq5-=DkQp^Mt2e5pl!S96AQZ#}ll`N`GUO149OzGcDhwpmSZ)>?tAZ>eC zb?;Fn@d)+*2LKyB+;<&Oh#@eC78Y=KQ&;mCmOSsyVCdc*5dn>e4-O<`nYG-7@of?`(5MW$gZ=9}KZ*5vX~uRruh&LA ziaT{Ipw5BR)wESV?S)kPoU>ea`$pG|6MNMb#MQ8%6f2HD&aFL(!V?RWi97xQy9+^3 z;h;^4hNYik7F_^$`5u0i{H;l2P3<$v0%^;O87vkThSO^^1CO^Ewij|7(x3~>A&YM7 zB{`rT`#r`_Whn|`x7PajCq+TA8yYkMLZ`<9fgDjj^hht87RM@nNn58YxVH-c^?mGP zd;Id5TS-p?9_%WHt{t;OGeSihSGV_0JS~xiNnI6A36OxN2CiYyg%K4HL04A>g@m7q z#DGGjS`vV6rMy5lXnK~cPCy(5r zHWDm5*LO2^^?1BS?~HmA_nSEh>g=HG`=?^V!WDxGIk-TGZO|fEwbE95KRT{&@;aQh zK7d_8l*tL*_zl#h%Y-E@tlAg4IQ zg)4xeObv*K6o@c%0`Sp>SmyoqoFYCzBg7$$lH-5o)4PSNJ-}!7A}E1})g(9Wood5B zFr%?7hF{_tAyz^&qJRU8JF8ai#@1M>VaLUe^u#8Jt9>a;wS**m0*j{38s2}at?>#& zhFg+|gS`uWL-GcreYD>w7g?2@R{VfBx-xQrL4XWNK4uS8P7i0*gr?)GrZqa8pi;?p z^PIqJy{p%_QSrZv;;3m2K+~Dh8bC()wFQ{VT$8T_i_yUnzOtHC)T0q=#&YPpdh-%- z(1cW{uwfA+P(zIZ|8t(~1G?x~Wb!E>cK^7pN8PxuC-uK=v=>|+@&AVuSa0d^A5s