2277 feat sysmon rewrite in golang #2660

Merged
mfreeman451 merged 23 commits from refs/pull/2660/head into staging 2026-01-14 06:47:53 +00:00
mfreeman451 commented 2026-01-14 02:33:50 +00:00 (Migrated from github.com)
Owner

Imported from GitHub pull request.

Original GitHub pull request: #2279
Original author: @mfreeman451
Original URL: https://github.com/carverauto/serviceradar/pull/2279
Original created: 2026-01-14T02:33:50Z
Original updated: 2026-01-14T06:47:55Z
Original head: carverauto/serviceradar:2277-feat-sysmon-rewrite-in-golang
Original base: staging
Original merged: 2026-01-14T06:47:53Z by @mfreeman451

User description

IMPORTANT: Please sign the Developer Certificate of Origin

Thank you for your contribution to ServiceRadar. Please note, when contributing, the developer must include
a DCO sign-off statement indicating the DCO acceptance in one commit message. Here
is an example DCO Signed-off-by line in a commit message:

Signed-off-by: J. Doe <j.doe@domain.com>

Describe your changes

Code checklist before requesting a review

  • I have signed the DCO?
  • The build completes without errors?
  • All tests are passing when running make test?

PR Type

Enhancement


Description

  • Complete sysmon monitoring system rewrite from Rust to Go with full backend and frontend integration

  • Sysmon profiles management with CRUD operations, SRQL-based device targeting, and priority-based resolution

  • Agent configuration system for sysmon with multi-stage resolution (device > SRQL > tag > default) and config compilation

  • Device linking and enrollment via agent gateway sync with device identity reconciliation and capability-based discovery

  • Web UI enhancements including sysmon profile settings, device bulk assignment, and agent status display

  • Go sysmon package implementing metric collection for CPU, memory, disk, network, and processes with configurable sampling

  • Service lifecycle management in Go agent with config refresh loop, local/cache/default precedence, and concurrent operation support

  • Protobuf definitions extended with SysmonConfig message type and config_source field for configuration tracking

  • Database migrations for sysmon profiles, assignments, and agent config source tracking

  • SRQL shared library with Rustler NIF bindings for query parsing and translation

  • Comprehensive test coverage including E2E profile assignment tests, sysmon compiler tests, service lifecycle tests, and performance benchmarks

  • Agent capabilities extended with "sysmon" status reporting via streaming for large payloads


Diagram Walkthrough

flowchart LR
  Agent["Agent<br/>Go Sysmon Service"]
  Gateway["Agent Gateway<br/>Device Creation"]
  Config["Config Server<br/>Profile Resolution"]
  Profiles["Sysmon Profiles<br/>SRQL Targeting"]
  WebUI["Web UI<br/>Profile Management"]
  
  Agent -->|"heartbeat +<br/>metrics"| Gateway
  Gateway -->|"ensure_device"| Config
  Config -->|"resolve profile"| Profiles
  Profiles -->|"apply config"| Agent
  WebUI -->|"create/edit"| Profiles
  WebUI -->|"bulk assign"| Gateway

File Walkthrough

Relevant files
Enhancement
39 files
index.ex
Sysmon profiles management LiveView with query builder     

web-ng/lib/serviceradar_web_ng_web/live/settings/sysmon_profiles_live/index.ex

  • New LiveView module for managing sysmon profiles with full CRUD
    operations
  • Implements visual query builder for device targeting using SRQL
    filters
  • Provides profile compilation preview and device count estimation
  • Supports profile enable/disable toggling, default profile assignment,
    and deletion
+1183/-0
monitoring.pb.ex
Protobuf definitions update with sysmon config support     

elixir/serviceradar_core/lib/serviceradar/proto/monitoring.pb.ex

  • Updated protobuf version from 0.15.0 to 0.16.0 across all message
    definitions
  • Added full_name field to all Protobuf module declarations for clarity
  • Added new SysmonConfig message type with fields for monitoring
    configuration
  • Extended AgentConfigResponse and GatewayStatusRequest with
    config_source field
+133/-60
index.ex
Device list bulk sysmon profile assignment feature             

web-ng/lib/serviceradar_web_ng_web/live/device_live/index.ex

  • Added bulk sysmon profile assignment modal and event handlers
  • Integrated sysmon profile display in device table with direct/default
    indicators
  • Added helper functions to load and apply sysmon profiles to selected
    devices
  • Implemented profile resolution showing which profile applies to each
    device
+322/-2 
show.ex
Device detail view sysmon configuration management             

web-ng/lib/serviceradar_web_ng_web/live/device_live/show.ex

  • Added sysmon configuration section to device detail view
  • Implemented profile assignment UI with dropdown selector for direct
    device assignments
  • Added helper functions to load available profiles and manage device
    assignments
  • Displays effective profile source (device/tag/default) with visual
    indicators
+268/-0 
sysmon_profile.ex
SysmonProfile Ash resource with targeting and policies     

elixir/serviceradar_core/lib/serviceradar/sysmon_profiles/sysmon_profile.ex

  • New Ash resource for managing system monitoring profiles with
    multitenancy support
  • Defines attributes for collection intervals, metric types, disk paths,
    and alert thresholds
  • Implements SRQL-based device targeting with priority-based profile
    resolution
  • Includes authorization policies restricting profile management to
    admin users
+326/-0 
sysmon_compiler.ex
Sysmon configuration compiler with profile resolution       

elixir/serviceradar_core/lib/serviceradar/agent_config/compilers/sysmon_compiler.ex

  • New compiler module implementing the Compiler behaviour for sysmon
    configuration
  • Resolves sysmon profiles using multi-stage resolution (device, SRQL,
    tag, default)
  • Compiles profiles to agent-consumable config format with thresholds
    and collection settings
  • Includes validation and default config generation
+279/-0 
srql_target_resolver.ex
SRQL-based device targeting for sysmon profiles                   

elixir/serviceradar_core/lib/serviceradar/sysmon_profiles/srql_target_resolver.ex

  • New module for resolving sysmon profiles using SRQL query targeting
  • Loads targeting profiles ordered by priority and evaluates SRQL
    queries against devices
  • Parses SRQL AST and applies filters to device queries
  • Supports tag-based filtering via JSONB containment
+224/-0 
sysmon_profile_assignment.ex
Sysmon profile assignment resource for flexible targeting

elixir/serviceradar_core/lib/serviceradar/sysmon_profiles/sysmon_profile_assignment.ex

  • New Ash resource for assigning sysmon profiles to devices or tags
  • Supports device-specific and tag-based assignment types with priority
    ordering
  • Includes validation to enforce required fields per assignment type
  • Defines unique constraints and read actions for profile resolution
+233/-0 
sysmon_profile_seeder.ex
Default sysmon profile seeder for tenants                               

elixir/serviceradar_core/lib/serviceradar/sysmon_profiles/sysmon_profile_seeder.ex

  • New seeder module that creates default sysmon profile for each tenant
  • Ensures idempotent profile creation with standard thresholds
  • Called during tenant initialization via InitializeTenantInfrastructure
+100/-0 
validate_srql_query.ex
SRQL query validation for sysmon profile targeting             

elixir/serviceradar_core/lib/serviceradar/sysmon_profiles/changes/validate_srql_query.ex

  • New Ash change validator for target_query attribute on sysmon profiles
  • Validates SRQL query syntax via NIF and normalizes queries to start
    with in:devices
  • Treats empty strings as nil (no targeting)
+47/-0   
sysmon_profiles.ex
Sysmon profiles domain and resource registry                         

elixir/serviceradar_core/lib/serviceradar/sysmon_profiles.ex

  • New domain module for sysmon profile management
  • Registers SysmonProfile and SysmonProfileAssignment resources
  • Documents profile resolution order and integration with agents
+44/-0   
agent_gateway_sync.ex
Device creation and linking for agent enrollment                 

elixir/serviceradar_core/lib/serviceradar/edge/agent_gateway_sync.ex

  • New ensure_device_for_agent/3 function to create/update device records
    for agents
  • Uses DIRE (Device Identity Reconciliation Engine) to resolve device
    identity
  • Builds device metadata from agent attributes (hostname, OS, arch,
    capabilities)
  • Links agents to devices and tracks discovery sources based on
    capabilities
+239/-1 
agent_config_generator.ex
Sysmon config integration into agent configuration generation

elixir/serviceradar_core/lib/serviceradar/edge/agent_config_generator.ex

  • Integrates sysmon config loading via ConfigServer.get_config/4
  • Builds proto-compatible Monitoring.SysmonConfig struct for agent
    delivery
  • Includes sysmon config in version hash computation for config
    versioning
  • Falls back to default sysmon config when none defined
+56/-7   
agent_gateway_server.ex
Agent gateway device creation and config source tracking 

elixir/serviceradar_agent_gateway/lib/serviceradar_agent_gateway/agent_gateway_server.ex

  • Calls ensure_device_for_agent/5 during agent registration to create
    device records
  • Extracts config_source from agent hello requests and heartbeats
  • Builds device attributes from agent request metadata
  • Passes config source to agent heartbeat tracking
+64/-3   
agent.ex
Agent resource extended with device linking and config source

elixir/serviceradar_core/lib/serviceradar/infrastructure/agent.ex

  • Adds device_uid and config_source attributes to Agent resource
  • Updates gateway_sync action to accept device_uid and config_source
  • Updates heartbeat action to accept config_source
  • config_source tracks origin: remote, local, cached, or default
+8/-2     
compiler.ex
Compiler registry updated with sysmon compiler                     

elixir/serviceradar_core/lib/serviceradar/agent_config/compiler.ex

  • Adds :sysmon to config_type union type
  • Registers SysmonCompiler in the compiler registry
+3/-2     
initialize_tenant_infrastructure.ex
Tenant initialization extended with sysmon profile seeding

elixir/serviceradar_core/lib/serviceradar/identity/changes/initialize_tenant_infrastructure.ex

  • Calls SysmonProfileSeeder.seed_for_tenant/1 during tenant
    initialization
  • Logs successful seeding of default sysmon profile
+4/-0     
native.ex
SRQL native Rust NIF bindings                                                       

elixir/serviceradar_srql/lib/serviceradar_srql/native.ex

  • New Rustler NIF binding module for SRQL parsing and translation
  • Defines translate/5 and parse_ast/1 functions
  • Loads Rust NIF from srql_nif crate
+35/-0   
native.ex
SRQL native module refactored to use shared library           

web-ng/lib/serviceradar_web_ng/srql/native.ex

  • Converts from direct Rustler binding to compatibility alias
  • Delegates to ServiceRadarSRQL.Native for backwards compatibility
+7/-26   
router.ex
Web router extended with sysmon profile routes                     

web-ng/lib/serviceradar_web_ng_web/router.ex

  • Adds routes for sysmon profile settings pages
  • Routes for index, new profile, and edit profile views
+5/-0     
settings_components.ex
Settings navigation extended with sysmon option                   

web-ng/lib/serviceradar_web_ng_web/components/settings_components.ex

  • Adds sysmon settings navigation item to settings menu
+5/-0     
index.ex
Agent live view extended with sysmon status display           

web-ng/lib/serviceradar_web_ng_web/live/agent_live/index.ex

  • Adds sysmon status badge column to agent tables (live and offline)
  • Implements sysmon_status_badge/1 component showing sysmon capability
  • Updates colspan counts to account for new column
+36/-2   
monitoring.pb.go
Protobuf definitions extended with sysmon configuration   

proto/monitoring.pb.go

  • Adds SysmonConfig message type with monitoring configuration fields
  • Adds config_source field to GatewayStatusRequest, GatewayStatusChunk,
    and AgentHelloRequest
  • Adds sysmon_config field to AgentConfigResponse
  • Updates protobuf version comments
+263/-52
process.go
Go sysmon package for process metric collection                   

pkg/sysmon/process.go

  • New Go package for collecting process metrics
  • Implements CollectProcesses/1 to gather CPU, memory, and status for
    all processes
  • Sorts processes by CPU usage (descending) for convenient ordering
+129/-0 
sysmon_service.go
Core sysmon service implementation with config management

pkg/agent/sysmon_service.go

  • New file implementing SysmonService wrapper for system metrics
    collection
  • Handles service lifecycle (Start/Stop), configuration loading with
    local/cache/default precedence
  • Implements config refresh loop with jitter to detect and apply
    configuration changes
  • Provides status reporting via GetStatus() with JSON payload containing
    metrics
+539/-0 
collector.go
System metrics collector with periodic sampling                   

pkg/sysmon/collector.go

  • New DefaultCollector implementation of the Collector interface
  • Manages periodic metric collection with configurable sample intervals
  • Supports runtime reconfiguration without service restart
  • Implements collection loop with CPU frequency sampling integration
+299/-0 
metrics.go
Metric data structures and sample definitions                       

pkg/sysmon/metrics.go

  • Defines metric data structures (MetricSample, CPUMetric, DiskMetric,
    etc.)
  • Includes CPU cluster metrics for big.LITTLE architectures
  • Provides JSON serialization with RFC3339 timestamps
  • Compatible with existing Rust sysmon output format
+180/-0 
config.go
Configuration management and validation                                   

pkg/sysmon/config.go

  • Configuration structures with validation and parsing logic
  • Supports sample interval clamping (50ms-5min range)
  • Implements config file loading and merging with defaults
  • Provides ParsedConfig with validated, usable values
+190/-0 
cpu.go
CPU metrics collection with frequency support                       

pkg/sysmon/cpu.go

  • CPU metrics collection with frequency sampling support
  • Handles CPU cluster aggregation for heterogeneous architectures
  • Falls back to basic collection without frequency data if unavailable
  • Integrates with cpufreq package for frequency information
+134/-0 
host.go
Host identification and network interface detection           

pkg/sysmon/host.go

  • Host identification via hostname and local IP detection
  • Filters Docker and virtual network interfaces
  • Implements fallback mechanisms for IP discovery
  • Skips Docker CIDR ranges (172.17-19.0.0/16)
+125/-0 
disk.go
Disk usage metrics collection                                                       

pkg/sysmon/disk.go

  • Disk usage collection for specified mount points or all filesystems
  • Filters pseudo filesystems (proc, sysfs, tmpfs, cgroup, etc.)
  • Handles inaccessible partitions gracefully
  • Provides mount point and capacity metrics
+109/-0 
network.go
Network interface metrics collection                                         

pkg/sysmon/network.go

  • Network interface statistics collection (bytes, packets, errors,
    drops)
  • Filters loopback and virtual interfaces (Docker, bridge, veth, etc.)
  • Per-interface metrics with transmit/receive counters
  • Skips interfaces that shouldn't be monitored
+85/-0   
memory.go
Memory and swap metrics collection                                             

pkg/sysmon/memory.go

  • System memory and swap statistics collection
  • Graceful handling of systems without swap
  • Returns used/total bytes for both memory and swap
  • Non-fatal error handling for swap collection
+47/-0   
push_loop.go
Integration of sysmon metrics into push loop and config application

pkg/agent/push_loop.go

  • Separates sysmon status from regular service statuses in collection
  • Implements pushSysmonStatus() using StreamStatus for large payloads
  • Adds applySysmonConfig() to apply gateway-provided sysmon
    configuration
  • Implements protoToSysmonConfig() for proto-to-internal config
    conversion
  • Adds "sysmon" to agent capabilities list
+178/-9 
server.go
Server integration of sysmon service lifecycle                     

pkg/agent/server.go

  • Initializes embedded SysmonService during server startup
  • Implements initSysmonService() to create and start the service
  • Adds GetSysmonStatus() method for retrieving current metrics
  • Stops sysmon service during server shutdown
  • Uses defaultPartition constant for partition configuration
+50/-1   
types.go
Server type definition update for sysmon service                 

pkg/agent/types.go

  • Adds sysmonService field to Server struct
  • Enables server to manage embedded sysmon service instance
+1/-0     
syn_scanner_stub.go
SYN scanner statistics structure for non-Linux platforms 

pkg/scan/syn_scanner_stub.go

  • Adds ScannerStats struct for performance and diagnostic counters
  • Includes packet, ring buffer, retry, port allocation, and rate
    limiting statistics
  • Implements GetStats() stub returning empty stats on non-Linux
    platforms
+31/-0   
agents.rs
Agent query support for config source filtering                   

rust/srql/src/query/agents.rs

  • Adds config_source field to agent query filtering
  • Implements text filter support for config_source field
  • Adds test cases for config_source equality and IN filters
+50/-3   
models.rs
Agent model update with config source field                           

rust/srql/src/models.rs

  • Adds config_source optional field to AgentRow struct
  • Includes config_source in JSON serialization output
+2/-0     
Tests
8 files
sysmon_profile_assignment_test.exs
End-to-end tests for sysmon profile assignment resolution

elixir/serviceradar_core/test/serviceradar/sysmon_profiles/sysmon_profile_assignment_test.exs

  • Comprehensive E2E tests for sysmon profile assignment and resolution
    workflow
  • Tests profile matching by tags, device-specific assignments, and
    priority resolution
  • Validates fallback to default profile and agent config generation
    integration
  • Covers task 5.1 from sysmon-consolidation specification
+477/-0 
agent_gateway_sync_test.exs
Agent gateway sync integration tests                                         

elixir/serviceradar_core/test/serviceradar/edge/agent_gateway_sync_test.exs

  • Integration tests for agent enrollment and device creation workflow
  • Tests device creation from agent attributes and capability-based
    discovery source assignment
  • Validates agent upsert operations and heartbeat handling with
    config_source tracking
  • Covers agent-device synchronization and platform tenant info retrieval
+309/-0 
sysmon_compiler_test.exs
Unit and integration tests for sysmon compiler                     

elixir/serviceradar_core/test/serviceradar/sysmon_profiles/sysmon_compiler_test.exs

  • Tests module structure and behaviour implementation
  • Tests default config generation and validation
  • Integration tests for config compilation with and without profiles
  • Tests profile-to-config conversion
+167/-0 
sysmon_profile_seeder_test.exs
Unit and integration tests for sysmon profile seeder         

elixir/serviceradar_core/test/serviceradar/sysmon_profiles/sysmon_profile_seeder_test.exs

  • Tests module structure and function definitions
  • Integration tests for default profile creation during seeding
  • Tests idempotency and threshold configuration
+118/-0 
agent_config_generator_test.exs
Agent config generator tests extended with sysmon config 

elixir/serviceradar_core/test/serviceradar/edge/agent_config_generator_test.exs

  • Changes test tag from :database to :integration
  • Adds tests for sysmon config inclusion in generated agent config
  • Tests default sysmon config and version hash impact
  • Verifies proto-compatible struct fields
+52/-1   
sysmon_service_test.go
Extensive test coverage for sysmon service functionality 

pkg/agent/sysmon_service_test.go

  • Comprehensive test suite with 590 lines covering service lifecycle and
    configuration
  • Tests for idempotent Start/Stop, config loading precedence (local >
    cache > default)
  • Validates config refresh, hashing, and caching mechanisms
  • Includes tests for concurrent operations and configuration override
    behavior
+590/-0 
sysmon_config_bench_test.go
Performance benchmarks for sysmon configuration operations

pkg/agent/sysmon_config_bench_test.go

  • Benchmark tests for config hash computation, parsing, and file loading
  • Performance tests for concurrent config loading with 1K-10K agents
  • Validates latency requirements (avg <100-200ms, max <500-2000ms)
  • Includes throughput measurements for scalability validation
+343/-0 
collector_test.go
Unit tests for metrics collection and configuration           

pkg/sysmon/collector_test.go

  • Tests for config parsing, default values, and interval clamping
  • Validates individual metric collectors (CPU, memory, disk, network,
    processes)
  • Tests collector lifecycle (Start/Stop) and JSON serialization
    compatibility
  • Verifies metric sample structure matches expected format
+331/-0 
Configuration changes
7 files
config.exs
Configuration update for sysmon profiles domain                   

elixir/serviceradar_core/config/config.exs

  • Added ServiceRadar.SysmonProfiles domain to the list of configured
    domains
+2/-1     
20260113212456_add_sysmon_profiles.exs
Database migration for sysmon profiles and assignments     

elixir/serviceradar_core/priv/repo/migrations/20260113212456_add_sysmon_profiles.exs

  • Creates sysmon_profiles table with profile configuration fields
  • Creates sysmon_profile_assignments table for device/tag assignments
  • Adds unique indexes for device and tag assignments per profile
  • Adds lookup indexes for efficient assignment queries
+99/-0   
20260114000000_add_sysmon_profile_srql_targeting.exs
Database migration for SRQL targeting on sysmon profiles 

elixir/serviceradar_core/priv/repo/migrations/20260114000000_add_sysmon_profile_srql_targeting.exs

  • Adds target_query and priority columns to sysmon_profiles
  • Creates index for efficient lookup of targeting profiles
+35/-0   
20260114001354_add_agent_config_source.exs
Tenant migration for agent config source tracking               

elixir/serviceradar_core/priv/repo/tenant_migrations/20260114001354_add_agent_config_source.exs

  • Adds config_source column to ocsf_agents table in tenant schemas
  • Creates index on config_source for query efficiency
+25/-0   
test.exs
Test configuration updated with sysmon profiles domain     

elixir/serviceradar_core/config/test.exs

  • Adds ServiceRadar.SysmonProfiles to the list of domains for testing
+2/-1     
mix.exs
SRQL shared library Mix project configuration                       

elixir/serviceradar_srql/mix.exs

  • New Mix project for serviceradar_srql shared library
  • Configures Rustler for NIF compilation
  • Includes Jason dependency for JSON parsing
+43/-0   
schema.rs
Database schema update for agent config source tracking   

rust/srql/src/schema.rs

  • Adds config_source nullable text column to ocsf_agents table schema
+1/-0     
Dependencies
2 files
mix.exs
Mix dependencies updated with SRQL library                             

elixir/serviceradar_core/mix.exs

  • Adds dependency on serviceradar_srql shared library
+3/-0     
mix.exs
Mix dependencies updated with SRQL library                             

web-ng/mix.exs

  • Adds dependency on serviceradar_srql shared library
+3/-1     
Miscellaneous
17 files
client.ex
Compiler directive for gRPC stub warning suppression         

elixir/serviceradar_core/lib/serviceradar/sync/client.ex

  • Adds compiler directive to suppress undefined warning for gRPC stub
+3/-0     
agent_process.ex
Compiler directive for gRPC stub warning suppression         

elixir/serviceradar_core/lib/serviceradar/edge/agent_process.ex

  • Adds compiler directive to suppress undefined warning for gRPC stub
+3/-0     
identity_map.pb.go
Protobuf version comment updates                                                 

proto/identitymap/v1/identity_map.pb.go

  • Updates protobuf version comments (protoc-gen-go and protoc versions)
+2/-2     
rperf.pb.go
Protobuf code regeneration with version updates                   

proto/rperf/rperf.pb.go

  • Regenerated protobuf code with updated protoc version (v6.32.1)
  • Version bump from protoc-gen-go v1.36.10 to v1.36.9
+2/-2     
flow.pb.go
Protobuf code regeneration with version updates                   

proto/flow/flow.pb.go

  • Regenerated protobuf code with updated protoc version (v6.32.1)
  • Version bump from protoc-gen-go v1.36.10 to v1.36.9
+2/-2     
discovery.pb.go
Protobuf code regeneration with version updates                   

proto/discovery/discovery.pb.go

  • Regenerated protobuf code with updated protoc version (v6.32.1)
  • Version bump from protoc-gen-go v1.36.10 to v1.36.9
+2/-2     
nats_account.pb.go
Protobuf code regeneration with version updates                   

proto/nats_account.pb.go

  • Regenerated protobuf code with updated protoc version (v6.32.1)
  • Version bump from protoc-gen-go v1.36.10 to v1.36.9
+2/-2     
data_service.pb.go
Protobuf code regeneration with version updates                   

proto/data_service.pb.go

  • Regenerated protobuf code with updated protoc version (v6.32.1)
  • Version bump from protoc-gen-go v1.36.10 to v1.36.9
+2/-2     
core_service.pb.go
Protobuf code regeneration with version updates                   

proto/core_service.pb.go

  • Regenerated protobuf code with updated protoc version (v6.32.1)
  • Version bump from protoc-gen-go v1.36.10 to v1.36.9
+2/-2     
kv.pb.go
Protobuf code regeneration with version updates                   

proto/kv.pb.go

  • Regenerated protobuf code with updated protoc version (v6.32.1)
  • Version bump from protoc-gen-go v1.36.10 to v1.36.9
+2/-2     
rperf_grpc.pb.go
gRPC protobuf code regeneration with version updates         

proto/rperf/rperf_grpc.pb.go

  • Regenerated gRPC protobuf code with updated protoc version (v6.32.1)
  • Version bump from protoc v3.14.0 to v6.32.1
+1/-1     
discovery_grpc.pb.go
gRPC protobuf code regeneration with version updates         

proto/discovery/discovery_grpc.pb.go

  • Regenerated gRPC protobuf code with updated protoc version (v6.32.1)
  • Version bump from protoc v3.14.0 to v6.32.1
+1/-1     
nats_account_grpc.pb.go
gRPC protobuf code regeneration with version updates         

proto/nats_account_grpc.pb.go

  • Regenerated gRPC protobuf code with updated protoc version (v6.32.1)
  • Version bump from protoc v3.14.0 to v6.32.1
+1/-1     
core_service_grpc.pb.go
gRPC protobuf code regeneration with version updates         

proto/core_service_grpc.pb.go

  • Regenerated gRPC protobuf code with updated protoc version (v6.32.1)
  • Version bump from protoc v3.14.0 to v6.32.1
+1/-1     
data_service_grpc.pb.go
gRPC protobuf code regeneration with version updates         

proto/data_service_grpc.pb.go

  • Regenerated gRPC protobuf code with updated protoc version (v6.32.1)
  • Version bump from protoc v3.14.0 to v6.32.1
+1/-1     
monitoring_grpc.pb.go
gRPC protobuf code regeneration with version updates         

proto/monitoring_grpc.pb.go

  • Regenerated gRPC protobuf code with updated protoc version (v6.32.1)
  • Version bump from protoc v3.14.0 to v6.32.1
+1/-1     
kv_grpc.pb.go
gRPC protobuf code regeneration with version updates         

proto/kv_grpc.pb.go

  • Regenerated gRPC protobuf code with updated protoc version (v6.32.1)
  • Version bump from protoc v3.14.0 to v6.32.1
+1/-1     
Documentation
2 files
serviceradar_srql.ex
SRQL shared library module documentation                                 

elixir/serviceradar_srql/lib/serviceradar_srql.ex

  • New shared library module documenting SRQL NIF bindings
  • Explains usage of parse and translate functions
  • Documents architecture across serviceradar_srql, web-ng, and
    serviceradar_core
+27/-0   
behaviour.ex
SRQL behaviour module documentation improvement                   

web-ng/lib/serviceradar_web_ng/srql/behaviour.ex

  • Improves moduledoc from false to descriptive documentation
+3/-1     
Formatting
1 files
.formatter.exs
Code formatter configuration for SRQL library                       

elixir/serviceradar_srql/.formatter.exs

  • Formatter configuration for SRQL library
+3/-0     
Additional files
22 files
openspec-apply.md +20/-0   
openspec-archive.md +24/-0   
openspec-proposal.md +25/-0   
BUILD.bazel +6/-0     
README.md +20/-0   
README.md +18/-0   
installation.md +37/-0   
sysmon-migration.md +305/-0 
sysmon-local-config.md +363/-0 
sysmon-profiles.md +235/-0 
Cargo.toml +16/-0   
lib.rs +66/-0   
design.md +249/-0 
proposal.md +138/-0 
spec.md +235/-0 
spec.md +221/-0 
spec.md +145/-0 
task.md +13/-0   
tasks.md +125/-0 
BUILD.bazel +4/-0     
BUILD.bazel +38/-0   
monitoring.proto +40/-0   

Imported from GitHub pull request. Original GitHub pull request: #2279 Original author: @mfreeman451 Original URL: https://github.com/carverauto/serviceradar/pull/2279 Original created: 2026-01-14T02:33:50Z Original updated: 2026-01-14T06:47:55Z Original head: carverauto/serviceradar:2277-feat-sysmon-rewrite-in-golang Original base: staging Original merged: 2026-01-14T06:47:53Z by @mfreeman451 --- ### **User description** ## IMPORTANT: Please sign the Developer Certificate of Origin Thank you for your contribution to ServiceRadar. Please note, when contributing, the developer must include a [DCO sign-off statement]( https://developercertificate.org/) indicating the DCO acceptance in one commit message. Here is an example DCO Signed-off-by line in a commit message: ``` Signed-off-by: J. Doe <j.doe@domain.com> ``` ## Describe your changes ## Issue ticket number and link ## Code checklist before requesting a review - [ ] I have signed the DCO? - [ ] The build completes without errors? - [ ] All tests are passing when running make test? ___ ### **PR Type** Enhancement ___ ### **Description** - **Complete sysmon monitoring system rewrite from Rust to Go** with full backend and frontend integration - **Sysmon profiles management** with CRUD operations, SRQL-based device targeting, and priority-based resolution - **Agent configuration system** for sysmon with multi-stage resolution (device > SRQL > tag > default) and config compilation - **Device linking and enrollment** via agent gateway sync with device identity reconciliation and capability-based discovery - **Web UI enhancements** including sysmon profile settings, device bulk assignment, and agent status display - **Go sysmon package** implementing metric collection for CPU, memory, disk, network, and processes with configurable sampling - **Service lifecycle management** in Go agent with config refresh loop, local/cache/default precedence, and concurrent operation support - **Protobuf definitions** extended with `SysmonConfig` message type and `config_source` field for configuration tracking - **Database migrations** for sysmon profiles, assignments, and agent config source tracking - **SRQL shared library** with Rustler NIF bindings for query parsing and translation - **Comprehensive test coverage** including E2E profile assignment tests, sysmon compiler tests, service lifecycle tests, and performance benchmarks - **Agent capabilities** extended with "sysmon" status reporting via streaming for large payloads ___ ### Diagram Walkthrough ```mermaid flowchart LR Agent["Agent<br/>Go Sysmon Service"] Gateway["Agent Gateway<br/>Device Creation"] Config["Config Server<br/>Profile Resolution"] Profiles["Sysmon Profiles<br/>SRQL Targeting"] WebUI["Web UI<br/>Profile Management"] Agent -->|"heartbeat +<br/>metrics"| Gateway Gateway -->|"ensure_device"| Config Config -->|"resolve profile"| Profiles Profiles -->|"apply config"| Agent WebUI -->|"create/edit"| Profiles WebUI -->|"bulk assign"| Gateway ``` <details><summary><h3>File Walkthrough</h3></summary> <table><thead><tr><th></th><th align="left">Relevant files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><details><summary>39 files</summary><table> <tr> <td> <details> <summary><strong>index.ex</strong><dd><code>Sysmon profiles management LiveView with query builder</code>&nbsp; &nbsp; &nbsp; </dd></summary> <hr> web-ng/lib/serviceradar_web_ng_web/live/settings/sysmon_profiles_live/index.ex <ul><li>New LiveView module for managing sysmon profiles with full CRUD <br>operations<br> <li> Implements visual query builder for device targeting using SRQL <br>filters<br> <li> Provides profile compilation preview and device count estimation<br> <li> Supports profile enable/disable toggling, default profile assignment, <br>and deletion</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-8849d05a7171d718f17babecdf46709110ad58d7dbf378ed29429f2a4d50a5c5">+1183/-0</a></td> </tr> <tr> <td> <details> <summary><strong>monitoring.pb.ex</strong><dd><code>Protobuf definitions update with sysmon config support</code>&nbsp; &nbsp; &nbsp; </dd></summary> <hr> elixir/serviceradar_core/lib/serviceradar/proto/monitoring.pb.ex <ul><li>Updated protobuf version from 0.15.0 to 0.16.0 across all message <br>definitions<br> <li> Added <code>full_name</code> field to all Protobuf module declarations for clarity<br> <li> Added new <code>SysmonConfig</code> message type with fields for monitoring <br>configuration<br> <li> Extended <code>AgentConfigResponse</code> and <code>GatewayStatusRequest</code> with <br><code>config_source</code> field</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-c4072f56f73e61992b2401a13598f9edcf11b255ba343c79cc629782b0b0dca0">+133/-60</a></td> </tr> <tr> <td> <details> <summary><strong>index.ex</strong><dd><code>Device list bulk sysmon profile assignment feature</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> web-ng/lib/serviceradar_web_ng_web/live/device_live/index.ex <ul><li>Added bulk sysmon profile assignment modal and event handlers<br> <li> Integrated sysmon profile display in device table with direct/default <br>indicators<br> <li> Added helper functions to load and apply sysmon profiles to selected <br>devices<br> <li> Implemented profile resolution showing which profile applies to each <br>device</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-261a01f4876e5984e1d9e9b38a3540675dfb0272abc30e6bdb2a4fa610353cc7">+322/-2</a>&nbsp; </td> </tr> <tr> <td> <details> <summary><strong>show.ex</strong><dd><code>Device detail view sysmon configuration management</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> web-ng/lib/serviceradar_web_ng_web/live/device_live/show.ex <ul><li>Added sysmon configuration section to device detail view<br> <li> Implemented profile assignment UI with dropdown selector for direct <br>device assignments<br> <li> Added helper functions to load available profiles and manage device <br>assignments<br> <li> Displays effective profile source (device/tag/default) with visual <br>indicators</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-92d8e0af57d2f65dfb8562e896dbea17ef9f47074ffd14f58261decc76f80c24">+268/-0</a>&nbsp; </td> </tr> <tr> <td> <details> <summary><strong>sysmon_profile.ex</strong><dd><code>SysmonProfile Ash resource with targeting and policies</code>&nbsp; &nbsp; &nbsp; </dd></summary> <hr> elixir/serviceradar_core/lib/serviceradar/sysmon_profiles/sysmon_profile.ex <ul><li>New Ash resource for managing system monitoring profiles with <br>multitenancy support<br> <li> Defines attributes for collection intervals, metric types, disk paths, <br>and alert thresholds<br> <li> Implements SRQL-based device targeting with priority-based profile <br>resolution<br> <li> Includes authorization policies restricting profile management to <br>admin users</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-81bed24a71209f091c692710727cbfb6eec5386891e7dd2323fad7fc21763f17">+326/-0</a>&nbsp; </td> </tr> <tr> <td> <details> <summary><strong>sysmon_compiler.ex</strong><dd><code>Sysmon configuration compiler with profile resolution</code>&nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> elixir/serviceradar_core/lib/serviceradar/agent_config/compilers/sysmon_compiler.ex <ul><li>New compiler module implementing the <code>Compiler</code> behaviour for sysmon <br>configuration<br> <li> Resolves sysmon profiles using multi-stage resolution (device, SRQL, <br>tag, default)<br> <li> Compiles profiles to agent-consumable config format with thresholds <br>and collection settings<br> <li> Includes validation and default config generation</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-0112a6c9ad8e3f4e689216d15a910efca1bf1a9c1ae89f5dc992ee685c417b51">+279/-0</a>&nbsp; </td> </tr> <tr> <td> <details> <summary><strong>srql_target_resolver.ex</strong><dd><code>SRQL-based device targeting for sysmon profiles</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> elixir/serviceradar_core/lib/serviceradar/sysmon_profiles/srql_target_resolver.ex <ul><li>New module for resolving sysmon profiles using SRQL query targeting<br> <li> Loads targeting profiles ordered by priority and evaluates SRQL <br>queries against devices<br> <li> Parses SRQL AST and applies filters to device queries<br> <li> Supports tag-based filtering via JSONB containment</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-92fc55ddb645aa284c8bded705d5750290d8058cce77ca339f97d5d77fe5b9d2">+224/-0</a>&nbsp; </td> </tr> <tr> <td> <details> <summary><strong>sysmon_profile_assignment.ex</strong><dd><code>Sysmon profile assignment resource for flexible targeting</code></dd></summary> <hr> elixir/serviceradar_core/lib/serviceradar/sysmon_profiles/sysmon_profile_assignment.ex <ul><li>New Ash resource for assigning sysmon profiles to devices or tags<br> <li> Supports device-specific and tag-based assignment types with priority <br>ordering<br> <li> Includes validation to enforce required fields per assignment type<br> <li> Defines unique constraints and read actions for profile resolution</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-a2f8d3c7fdc5e0a92286b1aebf95371402f0715db4a8af31d49a04035207243b">+233/-0</a>&nbsp; </td> </tr> <tr> <td> <details> <summary><strong>sysmon_profile_seeder.ex</strong><dd><code>Default sysmon profile seeder for tenants</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> elixir/serviceradar_core/lib/serviceradar/sysmon_profiles/sysmon_profile_seeder.ex <ul><li>New seeder module that creates default sysmon profile for each tenant<br> <li> Ensures idempotent profile creation with standard thresholds<br> <li> Called during tenant initialization via <code>InitializeTenantInfrastructure</code></ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-86bf80567ac09a8ccee51b88c5a8976766cd6bed6edf6f35fe50aa35603d2a77">+100/-0</a>&nbsp; </td> </tr> <tr> <td> <details> <summary><strong>validate_srql_query.ex</strong><dd><code>SRQL query validation for sysmon profile targeting</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> elixir/serviceradar_core/lib/serviceradar/sysmon_profiles/changes/validate_srql_query.ex <ul><li>New Ash change validator for <code>target_query</code> attribute on sysmon profiles<br> <li> Validates SRQL query syntax via NIF and normalizes queries to start <br>with <code>in:devices</code><br> <li> Treats empty strings as nil (no targeting)</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-515a4dabbf1d3cfdb94ec3d96dbc04fdc8260ac36fed19cc4c170d411bdfc6f2">+47/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>sysmon_profiles.ex</strong><dd><code>Sysmon profiles domain and resource registry</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> elixir/serviceradar_core/lib/serviceradar/sysmon_profiles.ex <ul><li>New domain module for sysmon profile management<br> <li> Registers <code>SysmonProfile</code> and <code>SysmonProfileAssignment</code> resources<br> <li> Documents profile resolution order and integration with agents</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-5f1c964a2d758cd13b6cf37db5289e8e700cc58bc1b58bad5ce3531e3529170b">+44/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>agent_gateway_sync.ex</strong><dd><code>Device creation and linking for agent enrollment</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> elixir/serviceradar_core/lib/serviceradar/edge/agent_gateway_sync.ex <ul><li>New <code>ensure_device_for_agent/3</code> function to create/update device records <br>for agents<br> <li> Uses DIRE (Device Identity Reconciliation Engine) to resolve device <br>identity<br> <li> Builds device metadata from agent attributes (hostname, OS, arch, <br>capabilities)<br> <li> Links agents to devices and tracks discovery sources based on <br>capabilities</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-9b32fb2972aa43999e1afb261429b23bcba6a8868eab704270158bb12e1825be">+239/-1</a>&nbsp; </td> </tr> <tr> <td> <details> <summary><strong>agent_config_generator.ex</strong><dd><code>Sysmon config integration into agent configuration generation</code></dd></summary> <hr> elixir/serviceradar_core/lib/serviceradar/edge/agent_config_generator.ex <ul><li>Integrates sysmon config loading via <code>ConfigServer.get_config/4</code><br> <li> Builds proto-compatible <code>Monitoring.SysmonConfig</code> struct for agent <br>delivery<br> <li> Includes sysmon config in version hash computation for config <br>versioning<br> <li> Falls back to default sysmon config when none defined</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-f368b9b41fa062759f00ff6fcae314cc5a42bb1caca82a9069103a803df1f9d7">+56/-7</a>&nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>agent_gateway_server.ex</strong><dd><code>Agent gateway device creation and config source tracking</code>&nbsp; </dd></summary> <hr> elixir/serviceradar_agent_gateway/lib/serviceradar_agent_gateway/agent_gateway_server.ex <ul><li>Calls <code>ensure_device_for_agent/5</code> during agent registration to create <br>device records<br> <li> Extracts <code>config_source</code> from agent hello requests and heartbeats<br> <li> Builds device attributes from agent request metadata<br> <li> Passes config source to agent heartbeat tracking</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-369a368073dc8ec1140bcea699005a1ce97a90cd59629df0bd18c71c7ffaae9f">+64/-3</a>&nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>agent.ex</strong><dd><code>Agent resource extended with device linking and config source</code></dd></summary> <hr> elixir/serviceradar_core/lib/serviceradar/infrastructure/agent.ex <ul><li>Adds <code>device_uid</code> and <code>config_source</code> attributes to Agent resource<br> <li> Updates <code>gateway_sync</code> action to accept <code>device_uid</code> and <code>config_source</code><br> <li> Updates <code>heartbeat</code> action to accept <code>config_source</code><br> <li> <code>config_source</code> tracks origin: remote, local, cached, or default</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-c56f92b6ce744cab3f2dc00dde92e2017cffdd12ad4618f7fa720252f2a6843a">+8/-2</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>compiler.ex</strong><dd><code>Compiler registry updated with sysmon compiler</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> elixir/serviceradar_core/lib/serviceradar/agent_config/compiler.ex <ul><li>Adds <code>:sysmon</code> to <code>config_type</code> union type<br> <li> Registers <code>SysmonCompiler</code> in the compiler registry</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-c853b548702e07ffa78e0a371869cd58f00b8ec13836220866d34298e047cbcd">+3/-2</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>initialize_tenant_infrastructure.ex</strong><dd><code>Tenant initialization extended with sysmon profile seeding</code></dd></summary> <hr> elixir/serviceradar_core/lib/serviceradar/identity/changes/initialize_tenant_infrastructure.ex <ul><li>Calls <code>SysmonProfileSeeder.seed_for_tenant/1</code> during tenant <br>initialization<br> <li> Logs successful seeding of default sysmon profile</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-a9b0e6035eab1514edae7dc2f9bf0744176688cb167e1cc293d6687c72e88086">+4/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>native.ex</strong><dd><code>SRQL native Rust NIF bindings</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> elixir/serviceradar_srql/lib/serviceradar_srql/native.ex <ul><li>New Rustler NIF binding module for SRQL parsing and translation<br> <li> Defines <code>translate/5</code> and <code>parse_ast/1</code> functions<br> <li> Loads Rust NIF from <code>srql_nif</code> crate</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-3acaa08fbc89bab46d583a5ce80be65b6883b66cb3ddb06aeb0e60c73ca21936">+35/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>native.ex</strong><dd><code>SRQL native module refactored to use shared library</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> web-ng/lib/serviceradar_web_ng/srql/native.ex <ul><li>Converts from direct Rustler binding to compatibility alias<br> <li> Delegates to <code>ServiceRadarSRQL.Native</code> for backwards compatibility</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-194d2427c0dc1f26ae2c2b75a23336a60159bbc6b70625ec49afe98d0a1003bf">+7/-26</a>&nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>router.ex</strong><dd><code>Web router extended with sysmon profile routes</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> web-ng/lib/serviceradar_web_ng_web/router.ex <ul><li>Adds routes for sysmon profile settings pages<br> <li> Routes for index, new profile, and edit profile views</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-df516cd33165cd85914c1ccb3ff6511d3fe688d4a66498b99807958998c5d07c">+5/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>settings_components.ex</strong><dd><code>Settings navigation extended with sysmon option</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> web-ng/lib/serviceradar_web_ng_web/components/settings_components.ex - Adds sysmon settings navigation item to settings menu </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-540f72cd405a3221e6f350afd04e644db83f0a504938a8907e94c417d070601e">+5/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>index.ex</strong><dd><code>Agent live view extended with sysmon status display</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> web-ng/lib/serviceradar_web_ng_web/live/agent_live/index.ex <ul><li>Adds sysmon status badge column to agent tables (live and offline)<br> <li> Implements <code>sysmon_status_badge/1</code> component showing sysmon capability<br> <li> Updates colspan counts to account for new column</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-e99a0a946ff70e2367ade800c0f33d273c8a3f20fcfbb9a26245c3f350f151d7">+36/-2</a>&nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>monitoring.pb.go</strong><dd><code>Protobuf definitions extended with sysmon configuration</code>&nbsp; &nbsp; </dd></summary> <hr> proto/monitoring.pb.go <ul><li>Adds <code>SysmonConfig</code> message type with monitoring configuration fields<br> <li> Adds <code>config_source</code> field to <code>GatewayStatusRequest</code>, <code>GatewayStatusChunk</code>, <br>and <code>AgentHelloRequest</code><br> <li> Adds <code>sysmon_config</code> field to <code>AgentConfigResponse</code><br> <li> Updates protobuf version comments</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-4f7e955b42854cc9cf3fb063b95e58a04f36271e6a0c1cb42ea6d7953dd96cc4">+263/-52</a></td> </tr> <tr> <td> <details> <summary><strong>process.go</strong><dd><code>Go sysmon package for process metric collection</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> pkg/sysmon/process.go <ul><li>New Go package for collecting process metrics<br> <li> Implements <code>CollectProcesses/1</code> to gather CPU, memory, and status for <br>all processes<br> <li> Sorts processes by CPU usage (descending) for convenient ordering</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-d9aa436ec5d8fd2058cbe8c46a0ed5fd2fbf3b615343c3ccf65ca252e87a270f">+129/-0</a>&nbsp; </td> </tr> <tr> <td> <details> <summary><strong>sysmon_service.go</strong><dd><code>Core sysmon service implementation with config management</code></dd></summary> <hr> pkg/agent/sysmon_service.go <ul><li>New file implementing <code>SysmonService</code> wrapper for system metrics <br>collection<br> <li> Handles service lifecycle (Start/Stop), configuration loading with <br>local/cache/default precedence<br> <li> Implements config refresh loop with jitter to detect and apply <br>configuration changes<br> <li> Provides status reporting via <code>GetStatus()</code> with JSON payload containing <br>metrics</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-8c00578b299aa029ef81f77ebe8959d20b7122299ee24e525c345f7c91c28d48">+539/-0</a>&nbsp; </td> </tr> <tr> <td> <details> <summary><strong>collector.go</strong><dd><code>System metrics collector with periodic sampling</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> pkg/sysmon/collector.go <ul><li>New <code>DefaultCollector</code> implementation of the <code>Collector</code> interface<br> <li> Manages periodic metric collection with configurable sample intervals<br> <li> Supports runtime reconfiguration without service restart<br> <li> Implements collection loop with CPU frequency sampling integration</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-3a79fac65ccb4853cd11d17be1f54977dd324dba1cf359c8535d4555a80aa568">+299/-0</a>&nbsp; </td> </tr> <tr> <td> <details> <summary><strong>metrics.go</strong><dd><code>Metric data structures and sample definitions</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> pkg/sysmon/metrics.go <ul><li>Defines metric data structures (<code>MetricSample</code>, <code>CPUMetric</code>, <code>DiskMetric</code>, <br>etc.)<br> <li> Includes CPU cluster metrics for big.LITTLE architectures<br> <li> Provides JSON serialization with RFC3339 timestamps<br> <li> Compatible with existing Rust sysmon output format</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-b3e642a1263d7b5a5a557db0f8bc8aa92dbe56e368ba39eef9db3ff0bb50f94e">+180/-0</a>&nbsp; </td> </tr> <tr> <td> <details> <summary><strong>config.go</strong><dd><code>Configuration management and validation</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> pkg/sysmon/config.go <ul><li>Configuration structures with validation and parsing logic<br> <li> Supports sample interval clamping (50ms-5min range)<br> <li> Implements config file loading and merging with defaults<br> <li> Provides <code>ParsedConfig</code> with validated, usable values</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-9b194947ce21f76ecc5305d6b6a92b6007dfd394ede78dfb271a12774e2aa0f2">+190/-0</a>&nbsp; </td> </tr> <tr> <td> <details> <summary><strong>cpu.go</strong><dd><code>CPU metrics collection with frequency support</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> pkg/sysmon/cpu.go <ul><li>CPU metrics collection with frequency sampling support<br> <li> Handles CPU cluster aggregation for heterogeneous architectures<br> <li> Falls back to basic collection without frequency data if unavailable<br> <li> Integrates with <code>cpufreq</code> package for frequency information</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-270a967b64d0099dd0e0f2a9be76e225d92e8ee24441d9fa737ba4d33df717f8">+134/-0</a>&nbsp; </td> </tr> <tr> <td> <details> <summary><strong>host.go</strong><dd><code>Host identification and network interface detection</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> pkg/sysmon/host.go <ul><li>Host identification via hostname and local IP detection<br> <li> Filters Docker and virtual network interfaces<br> <li> Implements fallback mechanisms for IP discovery<br> <li> Skips Docker CIDR ranges (172.17-19.0.0/16)</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-f8ea9f4c8ff33fcafda8ac509fb6bfb91cedc29e756047a36a9496521c761f28">+125/-0</a>&nbsp; </td> </tr> <tr> <td> <details> <summary><strong>disk.go</strong><dd><code>Disk usage metrics collection</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> pkg/sysmon/disk.go <ul><li>Disk usage collection for specified mount points or all filesystems<br> <li> Filters pseudo filesystems (proc, sysfs, tmpfs, cgroup, etc.)<br> <li> Handles inaccessible partitions gracefully<br> <li> Provides mount point and capacity metrics</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-d0cf5236d59374c3dc08521ba6c13ca3dc247a7d08bb23077599320e14943c29">+109/-0</a>&nbsp; </td> </tr> <tr> <td> <details> <summary><strong>network.go</strong><dd><code>Network interface metrics collection</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> pkg/sysmon/network.go <ul><li>Network interface statistics collection (bytes, packets, errors, <br>drops)<br> <li> Filters loopback and virtual interfaces (Docker, bridge, veth, etc.)<br> <li> Per-interface metrics with transmit/receive counters<br> <li> Skips interfaces that shouldn't be monitored</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-93a0767795ee1a1d1f54d7c62e310106672c47a00f0bb29b79a26d699c56db14">+85/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>memory.go</strong><dd><code>Memory and swap metrics collection</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> pkg/sysmon/memory.go <ul><li>System memory and swap statistics collection<br> <li> Graceful handling of systems without swap<br> <li> Returns used/total bytes for both memory and swap<br> <li> Non-fatal error handling for swap collection</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-f031e75d2a971fdc8dd4e69e4d41e814faa82f09f1e97a365ce03dc49ef885b6">+47/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>push_loop.go</strong><dd><code>Integration of sysmon metrics into push loop and config application</code></dd></summary> <hr> pkg/agent/push_loop.go <ul><li>Separates sysmon status from regular service statuses in collection<br> <li> Implements <code>pushSysmonStatus()</code> using <code>StreamStatus</code> for large payloads<br> <li> Adds <code>applySysmonConfig()</code> to apply gateway-provided sysmon <br>configuration<br> <li> Implements <code>protoToSysmonConfig()</code> for proto-to-internal config <br>conversion<br> <li> Adds "sysmon" to agent capabilities list</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-5f0d59be34ef26b449d7f5fd2b198a29b277936b9708a699f7487415ed6c2785">+178/-9</a>&nbsp; </td> </tr> <tr> <td> <details> <summary><strong>server.go</strong><dd><code>Server integration of sysmon service lifecycle</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> pkg/agent/server.go <ul><li>Initializes embedded <code>SysmonService</code> during server startup<br> <li> Implements <code>initSysmonService()</code> to create and start the service<br> <li> Adds <code>GetSysmonStatus()</code> method for retrieving current metrics<br> <li> Stops sysmon service during server shutdown<br> <li> Uses <code>defaultPartition</code> constant for partition configuration</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-3c51e5356a25859a3300ab62ed2494462feb2567ef69e6db3aa2bbc1032c1c5d">+50/-1</a>&nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>types.go</strong><dd><code>Server type definition update for sysmon service</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> pkg/agent/types.go <ul><li>Adds <code>sysmonService</code> field to <code>Server</code> struct<br> <li> Enables server to manage embedded sysmon service instance</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-e3a3767c816cdb568a387a32243f7348046f1f3445549cc06368870c914496cd">+1/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>syn_scanner_stub.go</strong><dd><code>SYN scanner statistics structure for non-Linux platforms</code>&nbsp; </dd></summary> <hr> pkg/scan/syn_scanner_stub.go <ul><li>Adds <code>ScannerStats</code> struct for performance and diagnostic counters<br> <li> Includes packet, ring buffer, retry, port allocation, and rate <br>limiting statistics<br> <li> Implements <code>GetStats()</code> stub returning empty stats on non-Linux <br>platforms</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-315626c989233a04e3d970ba49e20d807fbb7c10956858bf1b68b57609faf792">+31/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>agents.rs</strong><dd><code>Agent query support for config source filtering</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> rust/srql/src/query/agents.rs <ul><li>Adds <code>config_source</code> field to agent query filtering<br> <li> Implements text filter support for <code>config_source</code> field<br> <li> Adds test cases for <code>config_source</code> equality and IN filters</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-4c64666b2ea0a610ccbb59e89a560d62a06643335e19dab2185557b60728a82a">+50/-3</a>&nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>models.rs</strong><dd><code>Agent model update with config source field</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> rust/srql/src/models.rs <ul><li>Adds <code>config_source</code> optional field to <code>AgentRow</code> struct<br> <li> Includes <code>config_source</code> in JSON serialization output</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-57c82b01f92e9bd40063d0b0178c12d452771ac133f2121fb0ac008b167da367">+2/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> </table></details></td></tr><tr><td><strong>Tests</strong></td><td><details><summary>8 files</summary><table> <tr> <td> <details> <summary><strong>sysmon_profile_assignment_test.exs</strong><dd><code>End-to-end tests for sysmon profile assignment resolution</code></dd></summary> <hr> elixir/serviceradar_core/test/serviceradar/sysmon_profiles/sysmon_profile_assignment_test.exs <ul><li>Comprehensive E2E tests for sysmon profile assignment and resolution <br>workflow<br> <li> Tests profile matching by tags, device-specific assignments, and <br>priority resolution<br> <li> Validates fallback to default profile and agent config generation <br>integration<br> <li> Covers task 5.1 from sysmon-consolidation specification</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-d152ebd5cf257cc8417f0210a086b373aaf435cf0c041adad12d44ef8d1b2254">+477/-0</a>&nbsp; </td> </tr> <tr> <td> <details> <summary><strong>agent_gateway_sync_test.exs</strong><dd><code>Agent gateway sync integration tests</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> elixir/serviceradar_core/test/serviceradar/edge/agent_gateway_sync_test.exs <ul><li>Integration tests for agent enrollment and device creation workflow<br> <li> Tests device creation from agent attributes and capability-based <br>discovery source assignment<br> <li> Validates agent upsert operations and heartbeat handling with <br>config_source tracking<br> <li> Covers agent-device synchronization and platform tenant info retrieval</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-e4e30b3bed204d84faa9b86414059c44c85d962646e3c62d61b93773514916af">+309/-0</a>&nbsp; </td> </tr> <tr> <td> <details> <summary><strong>sysmon_compiler_test.exs</strong><dd><code>Unit and integration tests for sysmon compiler</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> elixir/serviceradar_core/test/serviceradar/sysmon_profiles/sysmon_compiler_test.exs <ul><li>Tests module structure and behaviour implementation<br> <li> Tests default config generation and validation<br> <li> Integration tests for config compilation with and without profiles<br> <li> Tests profile-to-config conversion</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-fe290f37f408a89f2441af2a6559cb992b86e5af5632bfd1cce946353f98f0fd">+167/-0</a>&nbsp; </td> </tr> <tr> <td> <details> <summary><strong>sysmon_profile_seeder_test.exs</strong><dd><code>Unit and integration tests for sysmon profile seeder</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> elixir/serviceradar_core/test/serviceradar/sysmon_profiles/sysmon_profile_seeder_test.exs <ul><li>Tests module structure and function definitions<br> <li> Integration tests for default profile creation during seeding<br> <li> Tests idempotency and threshold configuration</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-13298ba7ab4ba617cca920a15c77eaae8fa088ca72885f94541c611ee435102b">+118/-0</a>&nbsp; </td> </tr> <tr> <td> <details> <summary><strong>agent_config_generator_test.exs</strong><dd><code>Agent config generator tests extended with sysmon config</code>&nbsp; </dd></summary> <hr> elixir/serviceradar_core/test/serviceradar/edge/agent_config_generator_test.exs <ul><li>Changes test tag from <code>:database</code> to <code>:integration</code><br> <li> Adds tests for sysmon config inclusion in generated agent config<br> <li> Tests default sysmon config and version hash impact<br> <li> Verifies proto-compatible struct fields</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-521bf6cc2cbd0dd5e954a9ebad2dc776c91f5339ff28e157e506aaf511e243f1">+52/-1</a>&nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>sysmon_service_test.go</strong><dd><code>Extensive test coverage for sysmon service functionality</code>&nbsp; </dd></summary> <hr> pkg/agent/sysmon_service_test.go <ul><li>Comprehensive test suite with 590 lines covering service lifecycle and <br>configuration<br> <li> Tests for idempotent Start/Stop, config loading precedence (local > <br>cache > default)<br> <li> Validates config refresh, hashing, and caching mechanisms<br> <li> Includes tests for concurrent operations and configuration override <br>behavior</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-b6ec9458ea56578c153d977ce596bc117488fa02af69e1e3b4f4da4d61f3ee2a">+590/-0</a>&nbsp; </td> </tr> <tr> <td> <details> <summary><strong>sysmon_config_bench_test.go</strong><dd><code>Performance benchmarks for sysmon configuration operations</code></dd></summary> <hr> pkg/agent/sysmon_config_bench_test.go <ul><li>Benchmark tests for config hash computation, parsing, and file loading<br> <li> Performance tests for concurrent config loading with 1K-10K agents<br> <li> Validates latency requirements (avg <100-200ms, max <500-2000ms)<br> <li> Includes throughput measurements for scalability validation</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-f37e70f95fa979455f7ac847266a074a922e58909b301743169ade2441cb217a">+343/-0</a>&nbsp; </td> </tr> <tr> <td> <details> <summary><strong>collector_test.go</strong><dd><code>Unit tests for metrics collection and configuration</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> pkg/sysmon/collector_test.go <ul><li>Tests for config parsing, default values, and interval clamping<br> <li> Validates individual metric collectors (CPU, memory, disk, network, <br>processes)<br> <li> Tests collector lifecycle (Start/Stop) and JSON serialization <br>compatibility<br> <li> Verifies metric sample structure matches expected format</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-868f596f85a0bccf256ce90f18d851344622e2b6949e196afdcd103ac5dacb7f">+331/-0</a>&nbsp; </td> </tr> </table></details></td></tr><tr><td><strong>Configuration changes</strong></td><td><details><summary>7 files</summary><table> <tr> <td> <details> <summary><strong>config.exs</strong><dd><code>Configuration update for sysmon profiles domain</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> elixir/serviceradar_core/config/config.exs <ul><li>Added <code>ServiceRadar.SysmonProfiles</code> domain to the list of configured <br>domains</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-42b3888cc53d9dcf4ebc45ec7fffb2c672b129bffe763b6c76de58e4678a13a8">+2/-1</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>20260113212456_add_sysmon_profiles.exs</strong><dd><code>Database migration for sysmon profiles and assignments</code>&nbsp; &nbsp; &nbsp; </dd></summary> <hr> elixir/serviceradar_core/priv/repo/migrations/20260113212456_add_sysmon_profiles.exs <ul><li>Creates <code>sysmon_profiles</code> table with profile configuration fields<br> <li> Creates <code>sysmon_profile_assignments</code> table for device/tag assignments<br> <li> Adds unique indexes for device and tag assignments per profile<br> <li> Adds lookup indexes for efficient assignment queries</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-6b00c7a2058da9207d299eeefeecf5e9e0c60a94dba0a71cf6b2278b762cd543">+99/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>20260114000000_add_sysmon_profile_srql_targeting.exs</strong><dd><code>Database migration for SRQL targeting on sysmon profiles</code>&nbsp; </dd></summary> <hr> elixir/serviceradar_core/priv/repo/migrations/20260114000000_add_sysmon_profile_srql_targeting.exs <ul><li>Adds <code>target_query</code> and <code>priority</code> columns to <code>sysmon_profiles</code><br> <li> Creates index for efficient lookup of targeting profiles</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-af1164a133616b49ae3d0f4d36ac99bcca963327d63143710ed6f9e51a94b942">+35/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>20260114001354_add_agent_config_source.exs</strong><dd><code>Tenant migration for agent config source tracking</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> elixir/serviceradar_core/priv/repo/tenant_migrations/20260114001354_add_agent_config_source.exs <ul><li>Adds <code>config_source</code> column to <code>ocsf_agents</code> table in tenant schemas<br> <li> Creates index on <code>config_source</code> for query efficiency</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-c5edcc2d17ecafaf5cd7434ce608253cd72a286299dba6eeece4295e186319ed">+25/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>test.exs</strong><dd><code>Test configuration updated with sysmon profiles domain</code>&nbsp; &nbsp; &nbsp; </dd></summary> <hr> elixir/serviceradar_core/config/test.exs <ul><li>Adds <code>ServiceRadar.SysmonProfiles</code> to the list of domains for testing</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-d8fffec56a9bc7305c02bbdd424e21031ecbbb2c2e6ba07ba626e6043540e360">+2/-1</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>mix.exs</strong><dd><code>SRQL shared library Mix project configuration</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> elixir/serviceradar_srql/mix.exs <ul><li>New Mix project for serviceradar_srql shared library<br> <li> Configures Rustler for NIF compilation<br> <li> Includes Jason dependency for JSON parsing</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-717dd0732e40f57903a21d4310cacca84865e454b21a11185960f161e0698c26">+43/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>schema.rs</strong><dd><code>Database schema update for agent config source tracking</code>&nbsp; &nbsp; </dd></summary> <hr> rust/srql/src/schema.rs <ul><li>Adds <code>config_source</code> nullable text column to <code>ocsf_agents</code> table schema</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-2fe4906f42b52f96b7bc2d3431885b996b6701cfb88416905ae130b472d536ea">+1/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> </table></details></td></tr><tr><td><strong>Dependencies</strong></td><td><details><summary>2 files</summary><table> <tr> <td> <details> <summary><strong>mix.exs</strong><dd><code>Mix dependencies updated with SRQL library</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> elixir/serviceradar_core/mix.exs - Adds dependency on `serviceradar_srql` shared library </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-5805f547542c34c7f870fa4c31a5185456d31a70a0ea8eb4fce79306f7edeead">+3/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>mix.exs</strong><dd><code>Mix dependencies updated with SRQL library</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> web-ng/mix.exs - Adds dependency on `serviceradar_srql` shared library </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-cd109ab3082c4d4fd2a746f47eb49bda3f5dfcaf0f440812594765299905bb2d">+3/-1</a>&nbsp; &nbsp; &nbsp; </td> </tr> </table></details></td></tr><tr><td><strong>Miscellaneous</strong></td><td><details><summary>17 files</summary><table> <tr> <td> <details> <summary><strong>client.ex</strong><dd><code>Compiler directive for gRPC stub warning suppression</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> elixir/serviceradar_core/lib/serviceradar/sync/client.ex - Adds compiler directive to suppress undefined warning for gRPC stub </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-e83ca7f2697968eb50d0c8bb1063567d7fa71bb432a0ef48ca99e6fa96a75e7f">+3/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>agent_process.ex</strong><dd><code>Compiler directive for gRPC stub warning suppression</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> elixir/serviceradar_core/lib/serviceradar/edge/agent_process.ex - Adds compiler directive to suppress undefined warning for gRPC stub </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-b1a0ba4695f84f3faee480a7488345eedbf5668d6fda1f635b6ec70aa990a688">+3/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>identity_map.pb.go</strong><dd><code>Protobuf version comment updates</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> proto/identitymap/v1/identity_map.pb.go <ul><li>Updates protobuf version comments (protoc-gen-go and protoc versions)</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-213d71e5b7705dcc894d7958480bdc99663849ed868e1c800fea3219ac2beb00">+2/-2</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>rperf.pb.go</strong><dd><code>Protobuf code regeneration with version updates</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> proto/rperf/rperf.pb.go <ul><li>Regenerated protobuf code with updated protoc version (v6.32.1)<br> <li> Version bump from protoc-gen-go v1.36.10 to v1.36.9</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-be3702a09a0148b0ff48fe980d93569cf22a9b418a5c0e692e758d2707f7d4e9">+2/-2</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>flow.pb.go</strong><dd><code>Protobuf code regeneration with version updates</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> proto/flow/flow.pb.go <ul><li>Regenerated protobuf code with updated protoc version (v6.32.1)<br> <li> Version bump from protoc-gen-go v1.36.10 to v1.36.9</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-53c6f9e43be80c0583498c2755aa0f2a3bbde3411c3aae09384eca3045ff7417">+2/-2</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>discovery.pb.go</strong><dd><code>Protobuf code regeneration with version updates</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> proto/discovery/discovery.pb.go <ul><li>Regenerated protobuf code with updated protoc version (v6.32.1)<br> <li> Version bump from protoc-gen-go v1.36.10 to v1.36.9</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-97e9f674edade324cc295c179e327dcee7805e7f4c7dc5c5bf5235d811c7431c">+2/-2</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>nats_account.pb.go</strong><dd><code>Protobuf code regeneration with version updates</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> proto/nats_account.pb.go <ul><li>Regenerated protobuf code with updated protoc version (v6.32.1)<br> <li> Version bump from protoc-gen-go v1.36.10 to v1.36.9</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-49eb93c28e2d86d8dcf4ee78fd24bed498cf3fcfaa8e49849a36e70980420087">+2/-2</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>data_service.pb.go</strong><dd><code>Protobuf code regeneration with version updates</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> proto/data_service.pb.go <ul><li>Regenerated protobuf code with updated protoc version (v6.32.1)<br> <li> Version bump from protoc-gen-go v1.36.10 to v1.36.9</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-57ec731cb27f4bbf99d12324dea2f070e237998aac1e39916a40624aa264a0ec">+2/-2</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>core_service.pb.go</strong><dd><code>Protobuf code regeneration with version updates</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> proto/core_service.pb.go <ul><li>Regenerated protobuf code with updated protoc version (v6.32.1)<br> <li> Version bump from protoc-gen-go v1.36.10 to v1.36.9</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-826a054f544b8a45992f73c7477738b83c54b6e5569c5f4241d1f15eadfe1b32">+2/-2</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>kv.pb.go</strong><dd><code>Protobuf code regeneration with version updates</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> proto/kv.pb.go <ul><li>Regenerated protobuf code with updated protoc version (v6.32.1)<br> <li> Version bump from protoc-gen-go v1.36.10 to v1.36.9</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-0b9bbdd1cf0f65de86dd5ee0c824e5d35326bb4b8edcf1c97b61864493f4477f">+2/-2</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>rperf_grpc.pb.go</strong><dd><code>gRPC protobuf code regeneration with version updates</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> proto/rperf/rperf_grpc.pb.go <ul><li>Regenerated gRPC protobuf code with updated protoc version (v6.32.1)<br> <li> Version bump from protoc v3.14.0 to v6.32.1</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-201283133c4381a626cba52236fd63e85ce6afc6d45187f7a3d3dee23494b516">+1/-1</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>discovery_grpc.pb.go</strong><dd><code>gRPC protobuf code regeneration with version updates</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> proto/discovery/discovery_grpc.pb.go <ul><li>Regenerated gRPC protobuf code with updated protoc version (v6.32.1)<br> <li> Version bump from protoc v3.14.0 to v6.32.1</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-4c661f2724b4c586094dd400c03281137e3944f2bb06ca88df4ada1dd6dfff16">+1/-1</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>nats_account_grpc.pb.go</strong><dd><code>gRPC protobuf code regeneration with version updates</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> proto/nats_account_grpc.pb.go <ul><li>Regenerated gRPC protobuf code with updated protoc version (v6.32.1)<br> <li> Version bump from protoc v3.14.0 to v6.32.1</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-af98ae5f073d25a2ea0a044c996542fc65d215f6a70f14a9fba0447d87b34e11">+1/-1</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>core_service_grpc.pb.go</strong><dd><code>gRPC protobuf code regeneration with version updates</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> proto/core_service_grpc.pb.go <ul><li>Regenerated gRPC protobuf code with updated protoc version (v6.32.1)<br> <li> Version bump from protoc v3.14.0 to v6.32.1</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-887b9a6e989afb445503a63449b1b0571af7e2e8b907557bcd6995a236a199c7">+1/-1</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>data_service_grpc.pb.go</strong><dd><code>gRPC protobuf code regeneration with version updates</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> proto/data_service_grpc.pb.go <ul><li>Regenerated gRPC protobuf code with updated protoc version (v6.32.1)<br> <li> Version bump from protoc v3.14.0 to v6.32.1</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-3f001ab7addcba1d7c8a984b562124a4b96f7e524f154fc6a40dcbdcc53c6d72">+1/-1</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>monitoring_grpc.pb.go</strong><dd><code>gRPC protobuf code regeneration with version updates</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> proto/monitoring_grpc.pb.go <ul><li>Regenerated gRPC protobuf code with updated protoc version (v6.32.1)<br> <li> Version bump from protoc v3.14.0 to v6.32.1</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-4cc43cae31fd1906aa866d16b67cd0812e2a433eeb29b9070947b1693ad0146f">+1/-1</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>kv_grpc.pb.go</strong><dd><code>gRPC protobuf code regeneration with version updates</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> proto/kv_grpc.pb.go <ul><li>Regenerated gRPC protobuf code with updated protoc version (v6.32.1)<br> <li> Version bump from protoc v3.14.0 to v6.32.1</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-6daa12c34cd941284969fc2b1098fff1f7a96a17c57d99c7ba9267d494ebddd5">+1/-1</a>&nbsp; &nbsp; &nbsp; </td> </tr> </table></details></td></tr><tr><td><strong>Documentation</strong></td><td><details><summary>2 files</summary><table> <tr> <td> <details> <summary><strong>serviceradar_srql.ex</strong><dd><code>SRQL shared library module documentation</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> elixir/serviceradar_srql/lib/serviceradar_srql.ex <ul><li>New shared library module documenting SRQL NIF bindings<br> <li> Explains usage of parse and translate functions<br> <li> Documents architecture across serviceradar_srql, web-ng, and <br>serviceradar_core</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-49c2335c5b53fc992d91e4128d570934a3e256c5c71871c10c08e71be3a25e53">+27/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>behaviour.ex</strong><dd><code>SRQL behaviour module documentation improvement</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> web-ng/lib/serviceradar_web_ng/srql/behaviour.ex - Improves moduledoc from `false` to descriptive documentation </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-fac2f59bc5bfeb6802901d6ea2260d4c2c0a8fb52772052109c4b83e8534fbba">+3/-1</a>&nbsp; &nbsp; &nbsp; </td> </tr> </table></details></td></tr><tr><td><strong>Formatting</strong></td><td><details><summary>1 files</summary><table> <tr> <td> <details> <summary><strong>.formatter.exs</strong><dd><code>Code formatter configuration for SRQL library</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> elixir/serviceradar_srql/.formatter.exs - Formatter configuration for SRQL library </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-a6faa57461289d432274b2d1d86a107969682e9d8553c34f8a096955c273fe1a">+3/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> </table></details></td></tr><tr><td><strong>Additional files</strong></td><td><details><summary>22 files</summary><table> <tr> <td><strong>openspec-apply.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-b594aa97c0e368e32cd6db6dabd21125dc2bac7da9b9834c4caf346265a352d0">+20/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>openspec-archive.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-5c4eb50fd669c6ec07996461deef52a0a5c2d8c6c701f075d0fdd8fdd7ca9216">+24/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>openspec-proposal.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-3368586c5764447af7451492e7ce3c407400c4735e31c89eed722c2fd1849d1a">+25/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>BUILD.bazel</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-884fa9353a5226345e44fbabea3300efc7a87dfbcde0b6a42521ca51823f1b68">+6/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>README.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-2e9751b437fa61442aac074c7a4a912d0ac50ac3ea156ac8aedd8478d21c6bdb">+20/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>README.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-57ebcc9bd94d5a042ca261b795e1d851fddb9f3051461c2d82747287eba49bca">+18/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>installation.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-19fd876fb56da88a1becb80817b52db1a40e8491641d547913b93bb7ecda3d22">+37/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>sysmon-migration.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-998a0d753f344731fe8bc489182618b9de5f5b131828a1beb223f64829c3c65b">+305/-0</a>&nbsp; </td> </tr> <tr> <td><strong>sysmon-local-config.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-4e7f36d63dcf7d9980f92835debff8fd2c3db1dd036284421d1db0bf829f0ddf">+363/-0</a>&nbsp; </td> </tr> <tr> <td><strong>sysmon-profiles.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-f94c578aa88ee9970c758d561665d5e1fc467a617ab165b9583569120ab93be9">+235/-0</a>&nbsp; </td> </tr> <tr> <td><strong>Cargo.toml</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-75b57836acc54e880d76d41d0f6d0501b6649fdc0adbc9f358e61ac6780a6a47">+16/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>lib.rs</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-a29702aa40de7108c5636d2fcdf0e76b38193d392104c817f328f7d2a22a05ce">+66/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>design.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-5a151dbc820b6444eb1f32360da645c282662bee25e112c6fb60b2521df29892">+249/-0</a>&nbsp; </td> </tr> <tr> <td><strong>proposal.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-1943bb4a589b6432c3f21e0c69b08c8bad96bc123497a0f2fd0e3a24a52e9773">+138/-0</a>&nbsp; </td> </tr> <tr> <td><strong>spec.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-3fa7191589a558d1b5753c6f362990e5a405b2aec2f11930dd9330e8964534c6">+235/-0</a>&nbsp; </td> </tr> <tr> <td><strong>spec.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-62031b39b981f525a3f3aa7eed40e5f838c364dba09ccdcc3280492a965449a6">+221/-0</a>&nbsp; </td> </tr> <tr> <td><strong>spec.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-8658ce55c9f8a62bd0e2e837055ea1af8ab0833a3bbbc47344bbf332cf1ccf0b">+145/-0</a>&nbsp; </td> </tr> <tr> <td><strong>task.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-873051d429ec8d1f09a069b16791badfdad8fc93ceed3f9d2a487956df6076f0">+13/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>tasks.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-7f30aa025400a990cb044a1a64c4ecbb3d6cc416e389804ce83cfe7c0f92e128">+125/-0</a>&nbsp; </td> </tr> <tr> <td><strong>BUILD.bazel</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-9763df8132fa8d8919489fdfaf2434921b436714eb2aa276dca0ea4f92c02ec5">+4/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>BUILD.bazel</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-6b52a79d9a3f480a1760f8fcc9d191d57c5e5fb6e58799b5af820ae117c63d8a">+38/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>monitoring.proto</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-c4fc4e9693e4d2cf45697113ccca65b8c5ff18d2037e31ade411473533b36c2b">+40/-0</a>&nbsp; &nbsp; </td> </tr> </table></details></td></tr></tbody></table> </details> ___
qodo-code-review[bot] commented 2026-01-14 02:35:12 +00:00 (Migrated from github.com)
Author
Owner

Imported GitHub PR comment.

Original author: @qodo-code-review[bot]
Original URL: https://github.com/carverauto/serviceradar/pull/2279#issuecomment-3747417071
Original created: 2026-01-14T02:35:12Z

PR Compliance Guide 🔍

(Compliance updated until commit github.com/carverauto/serviceradar@7bf551f03e)

Below is a summary of compliance checks for this PR:

Security Compliance
Resource exhaustion risk

Description: User-controlled SRQL input is parsed and can trigger database counting on each form
validation/event (e.g., count_target_devices/2 called from
handle_event("validate_profile", ...)), which could be abused to cause resource exhaustion
via repeated expensive parses/count queries.
index.ex [111-963]

Referred Code
def handle_event("validate_profile", %{"form" => params}, socket) do
  scope = socket.assigns.current_scope
  target_query = Map.get(params, "target_query")
  device_count = count_target_devices(scope, target_query)
  ash_form = socket.assigns.ash_form |> Form.validate(params)

  {:noreply,
   socket
   |> assign(:ash_form, ash_form)
   |> assign(:form, to_form(ash_form))
   |> assign(:target_device_count, device_count)}
end

def handle_event("save_profile", %{"form" => params}, socket) do
  ash_form = socket.assigns.ash_form |> Form.validate(params)
  scope = socket.assigns.current_scope

  case Form.submit(ash_form, params: params) do
    {:ok, _profile} ->
      action = if socket.assigns.show_form == :new_profile, do: "created", else: "updated"




 ... (clipped 832 lines)
Identity spoofing risk

Description: Device identity and inventory fields (notably IP/hostname from attrs in
ensure_device_for_agent/3) appear to trust agent-supplied values, which could enable
spoofing/poisoning of inventory associations if agent enrollment is not strongly
authenticated and source IP is not verified server-side.
agent_gateway_sync.ex [115-316]

Referred Code
@doc """
Ensure a device record exists for the agent's host.

When an agent enrolls, we create or update a device record representing
the host machine. This enables the agent's sysmon metrics to be associated
with a device in the inventory.

The device identity is resolved using DIRE (Device Identity and Reconciliation Engine)
based on the agent's hostname and source IP.
"""
@spec ensure_device_for_agent(String.t(), String.t(), map()) ::
        {:ok, String.t()} | {:error, term()}
def ensure_device_for_agent(agent_id, tenant_id, attrs) do
  actor = SystemActor.for_tenant(tenant_id, :gateway_sync)
  tenant_schema = TenantSchemas.schema_for_tenant(tenant_id)

  # Build device update from agent metadata
  device_update = build_device_update_from_agent(attrs)

  # Resolve device ID using DIRE
  case IdentityReconciler.resolve_device_id(device_update, actor: actor) do


 ... (clipped 181 lines)
Ticket Compliance
🟡
🎫 #2277
🟢 Integrate the new sysmon library into `serviceradar-agent`.
Replace the split sysmon implementations (Go on macOS, Rust elsewhere) with a unified
sysmon in Go.
Create a new pure-Go /pkg/sysmon library incorporating the existing sysmon macOS behavior.

Use data structures compatible with the existing Rust serviceradar-sysmon package to ease
reintegration into ServiceRadar.
Leverage an established Go system metrics library (suggested: github.com/shirou/gopsutil)
where appropriate.
Support consolidation of Go-based checkers into a single install package for end users.
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Errors silently swallowed: Multiple rescue _ -> nil / generic error branches drop parsing/DB/count/compile errors
without logging or actionable context, making failures hard to diagnose.

Referred Code
defp compile_profile_preview(profile) do
  config = SysmonCompiler.compile_profile(profile)
  Jason.encode!(config, pretty: true)
rescue
  _ -> "{\"error\": \"Failed to compile config\"}"
end

defp count_target_devices(_scope, nil), do: nil
defp count_target_devices(_scope, ""), do: nil

defp count_target_devices(scope, target_query) when is_binary(target_query) do
  # Parse the SRQL query and count matching devices
  case ServiceRadarSRQL.Native.parse_ast(target_query) do
    {:ok, ast_json} ->
      case Jason.decode(ast_json) do
        {:ok, ast} ->
          count_devices_from_ast(scope, ast)

        {:error, _} ->
          nil
      end



 ... (clipped 23 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Missing audit logging: Sysmon profile create/update/delete/enable/default operations appear to only use UI
flashes without an explicit audit trail including acting user, action, and outcome.

Referred Code
def handle_event("save_profile", %{"form" => params}, socket) do
  ash_form = socket.assigns.ash_form |> Form.validate(params)
  scope = socket.assigns.current_scope

  case Form.submit(ash_form, params: params) do
    {:ok, _profile} ->
      action = if socket.assigns.show_form == :new_profile, do: "created", else: "updated"

      {:noreply,
       socket
       |> assign(:profiles, load_profiles(scope))
       |> put_flash(:info, "Profile #{action} successfully")
       |> push_navigate(to: ~p"/settings/sysmon")}

    {:error, ash_form} ->
      {:noreply,
       socket
       |> assign(:ash_form, ash_form)
       |> assign(:form, to_form(ash_form))}
  end
end



 ... (clipped 69 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Unvetted error payloads: Logging uses inspect(reason) for device/identity failures which may include sensitive
internal details depending on upstream error content and log sink exposure.

Referred Code
      Logger.warning(
        "Failed to upsert device for agent #{agent_id}: #{inspect(reason)}"
      )

      {:error, reason}
  end

{:error, reason} ->
  Logger.warning(
    "Failed to resolve device ID for agent #{agent_id}: #{inspect(reason)}"
  )

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Weak input validation: Agent-supplied hostname/os/arch/capabilities and derived source_ip are forwarded into core
device creation without visible validation/sanitization in this diff, relying on
downstream checks not shown.

Referred Code
defp ensure_device_for_agent(tenant_info, agent_id, partition_id, request, source_ip) do
  attrs = device_attrs_from_request(partition_id, request, source_ip)

  case core_call(AgentGatewaySync, :ensure_device_for_agent, [
         agent_id,
         tenant_info.tenant_id,
         attrs
       ]) do
    {:ok, {:ok, device_uid}} ->
      Logger.debug("Agent #{agent_id} linked to device #{device_uid}")
      :ok

    {:ok, {:error, reason}} ->
      # Device creation failure is non-fatal - agent can still operate
      Logger.warning("Failed to create device for agent #{agent_id}: #{inspect(reason)}")
      :ok

    {:error, :core_unavailable} ->
      # Non-fatal - device will be created on next hello
      Logger.warning("Core unavailable while creating device for agent #{agent_id}")
      :ok


 ... (clipped 15 lines)

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
- Requires Further Human Verification
🏷️ - Compliance label

Previous compliance checks

Compliance check up to commit f5c81a9
Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status: 🏷️
Silent error swallowing: Multiple new code paths swallow exceptions and/or parsing failures by returning nil or a
generic fallback without logging or returning actionable context, making production
debugging and correctness verification difficult.

Referred Code
defp compile_profile_preview(profile) do
  config = SysmonCompiler.compile_profile(profile)
  Jason.encode!(config, pretty: true)
rescue
  _ -> "{\"error\": \"Failed to compile config\"}"
end

defp count_target_devices(_scope, nil), do: nil
defp count_target_devices(_scope, ""), do: nil

defp count_target_devices(scope, target_query) when is_binary(target_query) do
  # Parse the SRQL query and count matching devices
  case ServiceRadarSRQL.Native.parse_ast(target_query) do
    {:ok, ast_json} ->
      case Jason.decode(ast_json) do
        {:ok, ast} ->
          count_devices_from_ast(scope, ast)

        {:error, _} ->
          nil
      end



 ... (clipped 68 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: 🏷️
No audit logging: New sysmon profile CRUD actions appear to be executed without emitting audit-log entries
that include acting user context and outcome, but this may be handled elsewhere in the
stack (e.g., Ash notifiers/telemetry).

Referred Code
def handle_event("save_profile", %{"form" => params}, socket) do
  ash_form = socket.assigns.ash_form |> Form.validate(params)
  scope = socket.assigns.current_scope

  case Form.submit(ash_form, params: params) do
    {:ok, _profile} ->
      action = if socket.assigns.show_form == :new_profile, do: "created", else: "updated"

      {:noreply,
       socket
       |> assign(:profiles, load_profiles(scope))
       |> put_flash(:info, "Profile #{action} successfully")
       |> push_navigate(to: ~p"/settings/sysmon")}

    {:error, ash_form} ->
      {:noreply,
       socket
       |> assign(:ash_form, ash_form)
       |> assign(:form, to_form(ash_form))}
  end
end



 ... (clipped 69 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: 🏷️
Logs may leak data: New logging statements include inspect(reason) on failures and include identifiers such as
agent_id/device_uid, which could leak sensitive/internal details depending on what reason
contains and the organization’s data classification.

Referred Code
def ensure_device_for_agent(agent_id, tenant_id, attrs) do
  actor = SystemActor.for_tenant(tenant_id, :gateway_sync)
  tenant_schema = TenantSchemas.schema_for_tenant(tenant_id)

  # Build device update from agent metadata
  device_update = build_device_update_from_agent(attrs)

  # Resolve device ID using DIRE
  case IdentityReconciler.resolve_device_id(device_update, actor: actor) do
    {:ok, device_uid} ->
      # Create or update the device record
      case upsert_device_for_agent(device_uid, agent_id, attrs, tenant_schema, actor) do
        :ok ->
          # Link the agent to the device
          link_agent_to_device(agent_id, device_uid, tenant_schema, actor)
          {:ok, device_uid}

        {:error, reason} ->
          Logger.warning(
            "Failed to upsert device for agent #{agent_id}: #{inspect(reason)}"
          )


 ... (clipped 9 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: 🏷️
External attrs unvalidated: New device creation/update logic consumes agent-provided attributes (e.g., hostname,
source_ip, capabilities) without explicit validation/sanitization in this diff, relying on
downstream layers whose guarantees are not visible here.

Referred Code
def ensure_device_for_agent(agent_id, tenant_id, attrs) do
  actor = SystemActor.for_tenant(tenant_id, :gateway_sync)
  tenant_schema = TenantSchemas.schema_for_tenant(tenant_id)

  # Build device update from agent metadata
  device_update = build_device_update_from_agent(attrs)

  # Resolve device ID using DIRE
  case IdentityReconciler.resolve_device_id(device_update, actor: actor) do
    {:ok, device_uid} ->
      # Create or update the device record
      case upsert_device_for_agent(device_uid, agent_id, attrs, tenant_schema, actor) do
        :ok ->
          # Link the agent to the device
          link_agent_to_device(agent_id, device_uid, tenant_schema, actor)
          {:ok, device_uid}

        {:error, reason} ->
          Logger.warning(
            "Failed to upsert device for agent #{agent_id}: #{inspect(reason)}"
          )


 ... (clipped 139 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Compliance check up to commit ab4b174
Security Compliance
Resource exhaustion

Description: User-controlled SRQL input triggers repeated parsing and database counting (e.g.,
handle_event("validate_profile", ...) calling count_target_devices/2 which calls
Ash.count/2) on every form change, which could be abused to generate excessive DB load/DoS
if not rate-limited/debounced. index.ex [111-1246]

Referred Code
def handle_event("validate_profile", %{"form" => params}, socket) do
  scope = socket.assigns.current_scope
  target_query = Map.get(params, "target_query")
  device_count = count_target_devices(scope, target_query)
  ash_form = socket.assigns.ash_form |> Form.validate(params)

  {:noreply,
   socket
   |> assign(:ash_form, ash_form)
   |> assign(:form, to_form(ash_form))
   |> assign(:target_device_count, device_count)}
end

def handle_event("save_profile", %{"form" => params}, socket) do
  ash_form = socket.assigns.ash_form |> Form.validate(params)
  scope = socket.assigns.current_scope

  case Form.submit(ash_form, params: params) do
    {:ok, _profile} ->
      action = if socket.assigns.show_form == :new_profile, do: "created", else: "updated"




 ... (clipped 1115 lines)
Ticket Compliance
🟡
🎫 #2277
🟢 Create a new pure-Go library at pkg/sysmon using Go libraries (e.g., gopsutil) to collect
system metrics.
Integrate the new sysmon library into `serviceradar-agent`.
Rewrite sysmon so there is a single implementation (replace Rust + sysinfo with a Go
implementation; incorporate existing Go sysmon-osx).
Keep data structures compatible with the existing Rust serviceradar-sysmon package to ease
integration back into ServiceRadar.
Support consolidation so Go-based checkers/components can be shipped as one installation
package.
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: 🏷️
Missing audit logs: Admin CRUD actions for sysmon profiles (create/update/delete/toggle/set default) are
performed without emitting audit log entries including actor identity and outcome.

Referred Code
def handle_event("save_profile", %{"form" => params}, socket) do
  ash_form = socket.assigns.ash_form |> Form.validate(params)
  scope = socket.assigns.current_scope

  case Form.submit(ash_form, params: params) do
    {:ok, _profile} ->
      action = if socket.assigns.show_form == :new_profile, do: "created", else: "updated"

      {:noreply,
       socket
       |> assign(:profiles, load_profiles(scope))
       |> put_flash(:info, "Profile #{action} successfully")
       |> push_navigate(to: ~p"/settings/sysmon")}

    {:error, ash_form} ->
      {:noreply,
       socket
       |> assign(:ash_form, ash_form)
       |> assign(:form, to_form(ash_form))}
  end
end



 ... (clipped 69 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status: 🏷️
Swallowed parse errors: SRQL parsing/JSON decoding/counting errors are swallowed via broad rescue and {:error, _}
-> nil branches without logging/context, making production diagnosis difficult.

Referred Code
defp compile_profile_preview(profile) do
  config = SysmonCompiler.compile_profile(profile)
  Jason.encode!(config, pretty: true)
rescue
  _ -> "{\"error\": \"Failed to compile config\"}"
end

defp count_target_devices(_scope, nil), do: nil
defp count_target_devices(_scope, ""), do: nil

defp count_target_devices(scope, target_query) when is_binary(target_query) do
  # Parse the SRQL query and count matching devices
  case ServiceRadarSRQL.Native.parse_ast(target_query) do
    {:ok, ast_json} ->
      case Jason.decode(ast_json) do
        {:ok, ast} ->
          count_devices_from_ast(scope, ast)

        {:error, _} ->
          nil
      end



 ... (clipped 23 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: 🏷️
Unvetted error logging: Logging inspect(reason) may expose sensitive internal/DB details depending on error
contents, and the diff does not show whether logs are structured/redacted.

Referred Code
      Logger.warning(
        "Failed to upsert device for agent #{agent_id}: #{inspect(reason)}"
      )

      {:error, reason}
  end

{:error, reason} ->
  Logger.warning(
    "Failed to resolve device ID for agent #{agent_id}: #{inspect(reason)}"
  )

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: 🏷️
Weak input validation: Device attributes derived from agent requests (e.g., hostname, os, arch, source_ip, and
capabilities) are forwarded into core/device creation flows without clear
validation/sanitization in the visible diff.

Referred Code
defp ensure_device_for_agent(tenant_info, agent_id, partition_id, request, source_ip) do
  attrs = device_attrs_from_request(partition_id, request, source_ip)

  case core_call(AgentGatewaySync, :ensure_device_for_agent, [
         agent_id,
         tenant_info.tenant_id,
         attrs
       ]) do
    {:ok, {:ok, device_uid}} ->
      Logger.debug("Agent #{agent_id} linked to device #{device_uid}")
      :ok

    {:ok, {:error, reason}} ->
      # Device creation failure is non-fatal - agent can still operate
      Logger.warning("Failed to create device for agent #{agent_id}: #{inspect(reason)}")
      :ok

    {:error, :core_unavailable} ->
      # Non-fatal - device will be created on next hello
      Logger.warning("Core unavailable while creating device for agent #{agent_id}")
      :ok


 ... (clipped 15 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Compliance check up to commit 7ab36f9
Security Compliance
SRQL injection risk

Description: The code builds combined_query = "#{target_query} uid:#{device_uid}" without
escaping/quoting device_uid, so a crafted device UID containing SRQL syntax/whitespace
could alter query semantics and potentially bypass/expand targeting behavior. srql_target_resolver.ex [104-138]

Referred Code
# Check if a profile's target_query matches a device
defp matches_device?(profile, tenant_schema, device_uid, actor) do
  target_query = profile.target_query

  if is_nil(target_query) or target_query == "" do
    {:ok, false}
  else
    # Build the combined query: original query + device UID filter
    combined_query = "#{target_query} uid:#{device_uid}"

    case execute_srql_match(combined_query, tenant_schema, actor) do
      {:ok, matched} -> {:ok, matched}
      {:error, reason} -> {:error, reason}
    end
  end
end

# Execute an SRQL query and check if it returns results
defp execute_srql_match(query_string, tenant_schema, actor) do
  # Parse the SRQL query to get filters
  case ServiceRadarSRQL.Native.parse_ast(query_string) do


 ... (clipped 14 lines)
Ticket Compliance
🟡
🎫 #2277
🟢 Integrate sysmon configuration into the agent/backend contract (e.g., introduce
SysmonConfig in protobufs and related config plumbing).
Replace the non-OSX Rust sysinfo-based sysmon with a unified implementation in Go so there
is a single sysmon codebase across platforms.
Create a new pure-Go library at pkg/sysmon (incorporating prior sysmon-osx functionality)
and implement system metric collection (CPU/memory/disk/network/processes), leveraging Go
libraries (e.g., github.com/shirou/gopsutil).
Keep data structures compatible with the existing Rust serviceradar-sysmon package to ease
integration back into ServiceRadar.
Integrate the new sysmon library into serviceradar-agent as part of consolidation of
Go-based components into one installation package.
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Missing audit logging: Critical sysmon profile actions (create/update/delete/toggle/set default) are performed
without recording an audit log containing actor/user context and outcome.

Referred Code
def handle_event("save_profile", %{"form" => params}, socket) do
  ash_form = socket.assigns.ash_form |> Form.validate(params)
  scope = socket.assigns.current_scope

  case Form.submit(ash_form, params: params) do
    {:ok, _profile} ->
      action = if socket.assigns.show_form == :new_profile, do: "created", else: "updated"

      {:noreply,
       socket
       |> assign(:profiles, load_profiles(scope))
       |> put_flash(:info, "Profile #{action} successfully")
       |> push_navigate(to: ~p"/settings/sysmon")}

    {:error, ash_form} ->
      {:noreply,
       socket
       |> assign(:ash_form, ash_form)
       |> assign(:form, to_form(ash_form))}
  end
end



 ... (clipped 69 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Unsafe destructive ops: Bulk assignment deletes existing assignments using Ash.destroy! inside Enum.each, which
can raise and crash the LiveView and is not wrapped in a transaction/rollback for partial
failures.

Referred Code
defp create_sysmon_assignments(scope, device_uids, profile_id) do
  # First, remove any existing device-specific assignments for these devices
  existing_query =
    SysmonProfileAssignment
    |> Ash.Query.for_read(:read, %{})
    |> Ash.Query.filter(device_uid in ^device_uids)

  # Delete existing assignments
  case Ash.read(existing_query, scope: scope) do
    {:ok, existing} ->
      Enum.each(existing, fn assignment ->
        Ash.destroy!(assignment, scope: scope)
      end)

    _ ->
      :ok
  end

  # Create new assignments for each device
  results =
    Enum.map(device_uids, fn device_uid ->



 ... (clipped 8 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status:
Leaky user error: A user-facing flash message interpolates the underlying failure reason, which may expose
internal details (e.g., database/validation errors) to the end-user.

Referred Code
  {:error, reason} ->
    {:noreply, put_flash(socket, :error, "Failed to assign profile: #{reason}")}
end

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Detailed reason logged: Warning logs include inspect(reason) which could unintentionally log sensitive internal
data depending on what upstream errors contain, requiring verification of logged error
contents.

Referred Code
  {:error, reason} ->
    Logger.warning(
      "Failed to load sysmon config for agent #{agent_id}: #{inspect(reason)}"
    )

    ServiceRadar.AgentConfig.Compilers.SysmonCompiler.default_config()
end

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Authorization not visible: The LiveView bulk sysmon profile assignment path accepts a client-provided profile_id and
performs destructive/creation operations without visible authorization checks in the diff,
requiring verification that access control is enforced elsewhere.

Referred Code
def handle_event("apply_bulk_sysmon_profile", _params, socket) do
  profile_id = socket.assigns.bulk_sysmon_profile_id

  if is_nil(profile_id) or profile_id == "" do
    {:noreply, put_flash(socket, :error, "Please select a profile")}
  else
    case apply_sysmon_profile_to_devices(socket, profile_id) do
      {:ok, count} ->
        {:noreply,
         socket
         |> assign(:show_bulk_sysmon_modal, false)
         |> assign(:bulk_sysmon_profile_id, nil)
         |> assign(:selected_devices, MapSet.new())
         |> assign(:select_all_matching, false)
         |> assign(:total_matching_count, nil)
         |> put_flash(:info, "Assigned sysmon profile to #{count} device(s)")}

      {:error, reason} ->
        {:noreply, put_flash(socket, :error, "Failed to assign profile: #{reason}")}
    end
  end



 ... (clipped 1 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Imported GitHub PR comment. Original author: @qodo-code-review[bot] Original URL: https://github.com/carverauto/serviceradar/pull/2279#issuecomment-3747417071 Original created: 2026-01-14T02:35:12Z --- ## PR Compliance Guide 🔍 <!-- https://github.com/carverauto/serviceradar/commit/7bf551f03e0733051882cc6902da9a63e54da742 --> #### (Compliance updated until commit https://github.com/carverauto/serviceradar/commit/7bf551f03e0733051882cc6902da9a63e54da742) Below is a summary of compliance checks for this PR:<br> <table><tbody><tr><td colspan='2'><strong>Security Compliance</strong></td></tr> <tr><td rowspan=2>⚪</td> <td><details><summary><strong>Resource exhaustion risk </strong></summary><br> <b>Description:</b> User-controlled SRQL input is parsed and can trigger database counting on each form <br>validation/event (e.g., <code>count_target_devices/2</code> called from <br><code>handle_event("validate_profile", ...)</code>), which could be abused to cause resource exhaustion <br>via repeated expensive parses/count queries.<br> <strong><a href='https://github.com/carverauto/serviceradar/pull/2279/files#diff-8849d05a7171d718f17babecdf46709110ad58d7dbf378ed29429f2a4d50a5c5R111-R963'>index.ex [111-963]</a></strong><br> <details open><summary>Referred Code</summary> ```elixir def handle_event("validate_profile", %{"form" => params}, socket) do scope = socket.assigns.current_scope target_query = Map.get(params, "target_query") device_count = count_target_devices(scope, target_query) ash_form = socket.assigns.ash_form |> Form.validate(params) {:noreply, socket |> assign(:ash_form, ash_form) |> assign(:form, to_form(ash_form)) |> assign(:target_device_count, device_count)} end def handle_event("save_profile", %{"form" => params}, socket) do ash_form = socket.assigns.ash_form |> Form.validate(params) scope = socket.assigns.current_scope case Form.submit(ash_form, params: params) do {:ok, _profile} -> action = if socket.assigns.show_form == :new_profile, do: "created", else: "updated" ... (clipped 832 lines) ``` </details></details></td></tr> <tr><td><details><summary><strong>Identity spoofing risk</strong></summary><br> <b>Description:</b> Device identity and inventory fields (notably IP/hostname from <code>attrs</code> in <br><code>ensure_device_for_agent/3</code>) appear to trust agent-supplied values, which could enable <br>spoofing/poisoning of inventory associations if agent enrollment is not strongly <br>authenticated and source IP is not verified server-side.<br> <strong><a href='https://github.com/carverauto/serviceradar/pull/2279/files#diff-9b32fb2972aa43999e1afb261429b23bcba6a8868eab704270158bb12e1825beR115-R316'>agent_gateway_sync.ex [115-316]</a></strong><br> <details open><summary>Referred Code</summary> ```elixir @doc """ Ensure a device record exists for the agent's host. When an agent enrolls, we create or update a device record representing the host machine. This enables the agent's sysmon metrics to be associated with a device in the inventory. The device identity is resolved using DIRE (Device Identity and Reconciliation Engine) based on the agent's hostname and source IP. """ @spec ensure_device_for_agent(String.t(), String.t(), map()) :: {:ok, String.t()} | {:error, term()} def ensure_device_for_agent(agent_id, tenant_id, attrs) do actor = SystemActor.for_tenant(tenant_id, :gateway_sync) tenant_schema = TenantSchemas.schema_for_tenant(tenant_id) # Build device update from agent metadata device_update = build_device_update_from_agent(attrs) # Resolve device ID using DIRE case IdentityReconciler.resolve_device_id(device_update, actor: actor) do ... (clipped 181 lines) ``` </details></details></td></tr> <tr><td colspan='2'><strong>Ticket Compliance</strong></td></tr> <tr><td>🟡</td> <td> <details> <summary>🎫 <a href=https://github.com/carverauto/serviceradar/issues/2277>#2277</a></summary> <table width='100%'><tbody> <tr><td rowspan=1>🟢</td> <td>Integrate the new sysmon library into `serviceradar-agent`. </td></tr> <tr><td rowspan=5>⚪</td> <td>Replace the split sysmon implementations (Go on macOS, Rust elsewhere) with a unified <br>sysmon in Go.<br></td></tr> <tr><td>Create a new pure-Go <code>/pkg/sysmon</code> library incorporating the existing sysmon macOS behavior.<br> <br></td></tr> <tr><td>Use data structures compatible with the existing Rust <code>serviceradar-sysmon</code> package to ease <br>reintegration into ServiceRadar.<br></td></tr> <tr><td>Leverage an established Go system metrics library (suggested: <code>github.com/shirou/gopsutil</code>) <br>where appropriate.<br></td></tr> <tr><td>Support consolidation of Go-based checkers into a single install package for end users. </td></tr> </tbody></table> </details> </td></tr> <tr><td colspan='2'><strong>Codebase Duplication Compliance</strong></td></tr> <tr><td>⚪</td><td><details><summary><strong>Codebase context is not defined </strong></summary> Follow the <a href='https://qodo-merge-docs.qodo.ai/core-abilities/rag_context_enrichment/'>guide</a> to enable codebase context checks. </details></td></tr> <tr><td colspan='2'><strong>Custom Compliance</strong></td></tr> <tr><td rowspan=2>🟢</td><td> <details><summary><strong>Generic: Meaningful Naming and Self-Documenting Code</strong></summary><br> **Objective:** Ensure all identifiers clearly express their purpose and intent, making code <br>self-documenting<br> **Status:** Passed<br> > Learn more about managing compliance <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#configuration-options'>generic rules</a> or creating your own <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#custom-compliance'>custom rules</a> </details></td></tr> <tr><td> <details><summary><strong>Generic: Secure Error Handling</strong></summary><br> **Objective:** To prevent the leakage of sensitive system information through error messages while <br>providing sufficient detail for internal debugging.<br> **Status:** Passed<br> > Learn more about managing compliance <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#configuration-options'>generic rules</a> or creating your own <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#custom-compliance'>custom rules</a> </details></td></tr> <tr><td rowspan=1>🔴</td> <td><details> <summary><strong>Generic: Robust Error Handling and Edge Case Management</strong></summary><br> **Objective:** Ensure comprehensive error handling that provides meaningful context and graceful <br>degradation<br> **Status:** <br><a href='https://github.com/carverauto/serviceradar/pull/2279/files#diff-8849d05a7171d718f17babecdf46709110ad58d7dbf378ed29429f2a4d50a5c5R920-R963'><strong>Errors silently swallowed</strong></a>: Multiple <code>rescue _ -&gt; nil</code> / generic error branches drop parsing/DB/count/compile errors <br>without logging or actionable context, making failures hard to diagnose.<br> <details open><summary>Referred Code</summary> ```elixir defp compile_profile_preview(profile) do config = SysmonCompiler.compile_profile(profile) Jason.encode!(config, pretty: true) rescue _ -> "{\"error\": \"Failed to compile config\"}" end defp count_target_devices(_scope, nil), do: nil defp count_target_devices(_scope, ""), do: nil defp count_target_devices(scope, target_query) when is_binary(target_query) do # Parse the SRQL query and count matching devices case ServiceRadarSRQL.Native.parse_ast(target_query) do {:ok, ast_json} -> case Jason.decode(ast_json) do {:ok, ast} -> count_devices_from_ast(scope, ast) {:error, _} -> nil end ... (clipped 23 lines) ``` </details> > Learn more about managing compliance <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#configuration-options'>generic rules</a> or creating your own <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#custom-compliance'>custom rules</a> </details></td></tr> <tr><td rowspan=3>⚪</td> <td><details> <summary><strong>Generic: Comprehensive Audit Trails</strong></summary><br> **Objective:** To create a detailed and reliable record of critical system actions for security analysis <br>and compliance.<br> **Status:** <br><a href='https://github.com/carverauto/serviceradar/pull/2279/files#diff-8849d05a7171d718f17babecdf46709110ad58d7dbf378ed29429f2a4d50a5c5R124-R213'><strong>Missing audit logging</strong></a>: Sysmon profile create/update/delete/enable/default operations appear to only use UI <br>flashes without an explicit audit trail including acting user, action, and outcome.<br> <details open><summary>Referred Code</summary> ```elixir def handle_event("save_profile", %{"form" => params}, socket) do ash_form = socket.assigns.ash_form |> Form.validate(params) scope = socket.assigns.current_scope case Form.submit(ash_form, params: params) do {:ok, _profile} -> action = if socket.assigns.show_form == :new_profile, do: "created", else: "updated" {:noreply, socket |> assign(:profiles, load_profiles(scope)) |> put_flash(:info, "Profile #{action} successfully") |> push_navigate(to: ~p"/settings/sysmon")} {:error, ash_form} -> {:noreply, socket |> assign(:ash_form, ash_form) |> assign(:form, to_form(ash_form))} end end ... (clipped 69 lines) ``` </details> > Learn more about managing compliance <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#configuration-options'>generic rules</a> or creating your own <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#custom-compliance'>custom rules</a> </details></td></tr> <tr><td><details> <summary><strong>Generic: Secure Logging Practices</strong></summary><br> **Objective:** To ensure logs are useful for debugging and auditing without exposing sensitive <br>information like PII, PHI, or cardholder data.<br> **Status:** <br><a href='https://github.com/carverauto/serviceradar/pull/2279/files#diff-9b32fb2972aa43999e1afb261429b23bcba6a8868eab704270158bb12e1825beR145-R156'><strong>Unvetted error payloads</strong></a>: Logging uses <code>inspect(reason)</code> for device/identity failures which may include sensitive <br>internal details depending on upstream error content and log sink exposure.<br> <details open><summary>Referred Code</summary> ```elixir Logger.warning( "Failed to upsert device for agent #{agent_id}: #{inspect(reason)}" ) {:error, reason} end {:error, reason} -> Logger.warning( "Failed to resolve device ID for agent #{agent_id}: #{inspect(reason)}" ) ``` </details> > Learn more about managing compliance <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#configuration-options'>generic rules</a> or creating your own <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#custom-compliance'>custom rules</a> </details></td></tr> <tr><td><details> <summary><strong>Generic: Security-First Input Validation and Data Handling</strong></summary><br> **Objective:** Ensure all data inputs are validated, sanitized, and handled securely to prevent <br>vulnerabilities<br> **Status:** <br><a href='https://github.com/carverauto/serviceradar/pull/2279/files#diff-369a368073dc8ec1140bcea699005a1ce97a90cd59629df0bd18c71c7ffaae9fR794-R829'><strong>Weak input validation</strong></a>: Agent-supplied <code>hostname</code>/<code>os</code>/<code>arch</code>/<code>capabilities</code> and derived <code>source_ip</code> are forwarded into core <br>device creation without visible validation/sanitization in this diff, relying on <br>downstream checks not shown.<br> <details open><summary>Referred Code</summary> ```elixir defp ensure_device_for_agent(tenant_info, agent_id, partition_id, request, source_ip) do attrs = device_attrs_from_request(partition_id, request, source_ip) case core_call(AgentGatewaySync, :ensure_device_for_agent, [ agent_id, tenant_info.tenant_id, attrs ]) do {:ok, {:ok, device_uid}} -> Logger.debug("Agent #{agent_id} linked to device #{device_uid}") :ok {:ok, {:error, reason}} -> # Device creation failure is non-fatal - agent can still operate Logger.warning("Failed to create device for agent #{agent_id}: #{inspect(reason)}") :ok {:error, :core_unavailable} -> # Non-fatal - device will be created on next hello Logger.warning("Core unavailable while creating device for agent #{agent_id}") :ok ... (clipped 15 lines) ``` </details> > Learn more about managing compliance <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#configuration-options'>generic rules</a> or creating your own <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#custom-compliance'>custom rules</a> </details></td></tr> <tr><td align="center" colspan="2"> - [ ] Update <!-- /compliance --update_compliance=true --> </td></tr></tbody></table> <details><summary>Compliance status legend</summary> 🟢 - Fully Compliant<br> 🟡 - Partial Compliant<br> 🔴 - Not Compliant<br> ⚪ - Requires Further Human Verification<br> 🏷️ - Compliance label<br> </details> ___ #### Previous compliance checks <details> <summary>Compliance check up to commit <a href='https://github.com/carverauto/serviceradar/commit/f5c81a9946ece052236da007dd0e331a32c82a7a'>f5c81a9</a></summary><br> <table><tbody><tr><td colspan='2'><strong>Security Compliance</strong></td></tr> <tr><td>🟢</td><td><details><summary><strong>No security concerns identified</strong></summary> No security vulnerabilities detected by AI analysis. Human verification advised for critical code. </details></td></tr> <tr><td colspan='2'><strong>Ticket Compliance</strong></td></tr> <tr><td>⚪</td><td><details><summary>🎫 <strong>No ticket provided </strong></summary> - [ ] Create ticket/issue <!-- /create_ticket --create_ticket=true --> </details></td></tr> <tr><td colspan='2'><strong>Codebase Duplication Compliance</strong></td></tr> <tr><td>⚪</td><td><details><summary><strong>Codebase context is not defined </strong></summary> Follow the <a href='https://qodo-merge-docs.qodo.ai/core-abilities/rag_context_enrichment/'>guide</a> to enable codebase context checks. </details></td></tr> <tr><td colspan='2'><strong>Custom Compliance</strong></td></tr> <tr><td rowspan=2>🟢</td><td> <details><summary><strong>Generic: Meaningful Naming and Self-Documenting Code</strong></summary><br> **Objective:** Ensure all identifiers clearly express their purpose and intent, making code <br>self-documenting<br> **Status:** Passed<br> > Learn more about managing compliance <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#configuration-options'>generic rules</a> or creating your own <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#custom-compliance'>custom rules</a> </details></td></tr> <tr><td> <details><summary><strong>Generic: Secure Error Handling</strong></summary><br> **Objective:** To prevent the leakage of sensitive system information through error messages while <br>providing sufficient detail for internal debugging.<br> **Status:** Passed<br> > Learn more about managing compliance <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#configuration-options'>generic rules</a> or creating your own <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#custom-compliance'>custom rules</a> </details></td></tr> <tr><td rowspan=1>🔴</td> <td><details> <summary><strong>Generic: Robust Error Handling and Edge Case Management</strong></summary><br> **Objective:** Ensure comprehensive error handling that provides meaningful context and graceful <br>degradation<br> **Status:** 🏷️<br><a href='https://github.com/carverauto/serviceradar/pull/2279/files#diff-8849d05a7171d718f17babecdf46709110ad58d7dbf378ed29429f2a4d50a5c5R914-R1002'><strong>Silent error swallowing</strong></a>: Multiple new code paths swallow exceptions and/or parsing failures by returning <code>nil</code> or a <br>generic fallback without logging or returning actionable context, making production <br>debugging and correctness verification difficult.<br> <details open><summary>Referred Code</summary> ```elixir defp compile_profile_preview(profile) do config = SysmonCompiler.compile_profile(profile) Jason.encode!(config, pretty: true) rescue _ -> "{\"error\": \"Failed to compile config\"}" end defp count_target_devices(_scope, nil), do: nil defp count_target_devices(_scope, ""), do: nil defp count_target_devices(scope, target_query) when is_binary(target_query) do # Parse the SRQL query and count matching devices case ServiceRadarSRQL.Native.parse_ast(target_query) do {:ok, ast_json} -> case Jason.decode(ast_json) do {:ok, ast} -> count_devices_from_ast(scope, ast) {:error, _} -> nil end ... (clipped 68 lines) ``` </details> > Learn more about managing compliance <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#configuration-options'>generic rules</a> or creating your own <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#custom-compliance'>custom rules</a> </details></td></tr> <tr><td rowspan=3>⚪</td> <td><details> <summary><strong>Generic: Comprehensive Audit Trails</strong></summary><br> **Objective:** To create a detailed and reliable record of critical system actions for security analysis <br>and compliance.<br> **Status:** 🏷️<br><a href='https://github.com/carverauto/serviceradar/pull/2279/files#diff-8849d05a7171d718f17babecdf46709110ad58d7dbf378ed29429f2a4d50a5c5R124-R213'><strong>No audit logging</strong></a>: New sysmon profile CRUD actions appear to be executed without emitting audit-log entries <br>that include acting user context and outcome, but this may be handled elsewhere in the <br>stack (e.g., Ash notifiers/telemetry).<br> <details open><summary>Referred Code</summary> ```elixir def handle_event("save_profile", %{"form" => params}, socket) do ash_form = socket.assigns.ash_form |> Form.validate(params) scope = socket.assigns.current_scope case Form.submit(ash_form, params: params) do {:ok, _profile} -> action = if socket.assigns.show_form == :new_profile, do: "created", else: "updated" {:noreply, socket |> assign(:profiles, load_profiles(scope)) |> put_flash(:info, "Profile #{action} successfully") |> push_navigate(to: ~p"/settings/sysmon")} {:error, ash_form} -> {:noreply, socket |> assign(:ash_form, ash_form) |> assign(:form, to_form(ash_form))} end end ... (clipped 69 lines) ``` </details> > Learn more about managing compliance <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#configuration-options'>generic rules</a> or creating your own <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#custom-compliance'>custom rules</a> </details></td></tr> <tr><td><details> <summary><strong>Generic: Secure Logging Practices</strong></summary><br> **Objective:** To ensure logs are useful for debugging and auditing without exposing sensitive <br>information like PII, PHI, or cardholder data.<br> **Status:** 🏷️<br><a href='https://github.com/carverauto/serviceradar/pull/2279/files#diff-9b32fb2972aa43999e1afb261429b23bcba6a8868eab704270158bb12e1825beR127-R156'><strong>Logs may leak data</strong></a>: New logging statements include <code>inspect(reason)</code> on failures and include identifiers such as <br><code>agent_id</code>/<code>device_uid</code>, which could leak sensitive/internal details depending on what <code>reason</code> <br>contains and the organization’s data classification.<br> <details open><summary>Referred Code</summary> ```elixir def ensure_device_for_agent(agent_id, tenant_id, attrs) do actor = SystemActor.for_tenant(tenant_id, :gateway_sync) tenant_schema = TenantSchemas.schema_for_tenant(tenant_id) # Build device update from agent metadata device_update = build_device_update_from_agent(attrs) # Resolve device ID using DIRE case IdentityReconciler.resolve_device_id(device_update, actor: actor) do {:ok, device_uid} -> # Create or update the device record case upsert_device_for_agent(device_uid, agent_id, attrs, tenant_schema, actor) do :ok -> # Link the agent to the device link_agent_to_device(agent_id, device_uid, tenant_schema, actor) {:ok, device_uid} {:error, reason} -> Logger.warning( "Failed to upsert device for agent #{agent_id}: #{inspect(reason)}" ) ... (clipped 9 lines) ``` </details> > Learn more about managing compliance <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#configuration-options'>generic rules</a> or creating your own <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#custom-compliance'>custom rules</a> </details></td></tr> <tr><td><details> <summary><strong>Generic: Security-First Input Validation and Data Handling</strong></summary><br> **Objective:** Ensure all data inputs are validated, sanitized, and handled securely to prevent <br>vulnerabilities<br> **Status:** 🏷️<br><a href='https://github.com/carverauto/serviceradar/pull/2279/files#diff-9b32fb2972aa43999e1afb261429b23bcba6a8868eab704270158bb12e1825beR127-R286'><strong>External attrs unvalidated</strong></a>: New device creation/update logic consumes agent-provided attributes (e.g., <code>hostname</code>, <br><code>source_ip</code>, <code>capabilities</code>) without explicit validation/sanitization in this diff, relying on <br>downstream layers whose guarantees are not visible here.<br> <details open><summary>Referred Code</summary> ```elixir def ensure_device_for_agent(agent_id, tenant_id, attrs) do actor = SystemActor.for_tenant(tenant_id, :gateway_sync) tenant_schema = TenantSchemas.schema_for_tenant(tenant_id) # Build device update from agent metadata device_update = build_device_update_from_agent(attrs) # Resolve device ID using DIRE case IdentityReconciler.resolve_device_id(device_update, actor: actor) do {:ok, device_uid} -> # Create or update the device record case upsert_device_for_agent(device_uid, agent_id, attrs, tenant_schema, actor) do :ok -> # Link the agent to the device link_agent_to_device(agent_id, device_uid, tenant_schema, actor) {:ok, device_uid} {:error, reason} -> Logger.warning( "Failed to upsert device for agent #{agent_id}: #{inspect(reason)}" ) ... (clipped 139 lines) ``` </details> > Learn more about managing compliance <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#configuration-options'>generic rules</a> or creating your own <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#custom-compliance'>custom rules</a> </details></td></tr> <tr><td align="center" colspan="2"> <!-- /compliance --update_compliance=true --> </td></tr></tbody></table> </details> <details> <summary>Compliance check up to commit <a href='https://github.com/carverauto/serviceradar/commit/ab4b174e4c5b5e4d562dae1075cf1b09852d729c'>ab4b174</a></summary><br> <table><tbody><tr><td colspan='2'><strong>Security Compliance</strong></td></tr> <tr><td rowspan=1>⚪</td> <td><details><summary><strong>Resource exhaustion </strong></summary><br> <b>Description:</b> User-controlled SRQL input triggers repeated parsing and database counting (e.g., <br><code>handle_event("validate_profile", ...)</code> calling <code>count_target_devices/2</code> which calls <br><code>Ash.count/2</code>) on every form change, which could be abused to generate excessive DB load/DoS <br>if not rate-limited/debounced. <strong><a href='https://github.com/carverauto/serviceradar/pull/2279/files#diff-8849d05a7171d718f17babecdf46709110ad58d7dbf378ed29429f2a4d50a5c5R111-R1246'>index.ex [111-1246]</a></strong><br> <details open><summary>Referred Code</summary> ```elixir def handle_event("validate_profile", %{"form" => params}, socket) do scope = socket.assigns.current_scope target_query = Map.get(params, "target_query") device_count = count_target_devices(scope, target_query) ash_form = socket.assigns.ash_form |> Form.validate(params) {:noreply, socket |> assign(:ash_form, ash_form) |> assign(:form, to_form(ash_form)) |> assign(:target_device_count, device_count)} end def handle_event("save_profile", %{"form" => params}, socket) do ash_form = socket.assigns.ash_form |> Form.validate(params) scope = socket.assigns.current_scope case Form.submit(ash_form, params: params) do {:ok, _profile} -> action = if socket.assigns.show_form == :new_profile, do: "created", else: "updated" ... (clipped 1115 lines) ``` </details></details></td></tr> <tr><td colspan='2'><strong>Ticket Compliance</strong></td></tr> <tr><td>🟡</td> <td> <details> <summary>🎫 <a href=https://github.com/carverauto/serviceradar/issues/2277>#2277</a></summary> <table width='100%'><tbody> <tr><td rowspan=2>🟢</td> <td>Create a new pure-Go library at <code>pkg/sysmon</code> using Go libraries (e.g., <code>gopsutil</code>) to collect <br>system metrics.</td></tr> <tr><td>Integrate the new sysmon library into `serviceradar-agent`.</td></tr> <tr><td rowspan=3>⚪</td> <td>Rewrite sysmon so there is a single implementation (replace Rust + sysinfo with a Go <br>implementation; incorporate existing Go sysmon-osx).</td></tr> <tr><td>Keep data structures compatible with the existing Rust <code>serviceradar-sysmon</code> package to ease <br>integration back into ServiceRadar.</td></tr> <tr><td>Support consolidation so Go-based checkers/components can be shipped as one installation <br>package.</td></tr> </tbody></table> </details> </td></tr> <tr><td colspan='2'><strong>Codebase Duplication Compliance</strong></td></tr> <tr><td>⚪</td><td><details><summary><strong>Codebase context is not defined </strong></summary> Follow the <a href='https://qodo-merge-docs.qodo.ai/core-abilities/rag_context_enrichment/'>guide</a> to enable codebase context checks. </details></td></tr> <tr><td colspan='2'><strong>Custom Compliance</strong></td></tr> <tr><td rowspan=2>🟢</td><td> <details><summary><strong>Generic: Meaningful Naming and Self-Documenting Code</strong></summary><br> **Objective:** Ensure all identifiers clearly express their purpose and intent, making code <br>self-documenting<br> **Status:** Passed<br> > Learn more about managing compliance <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#configuration-options'>generic rules</a> or creating your own <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#custom-compliance'>custom rules</a> </details></td></tr> <tr><td> <details><summary><strong>Generic: Secure Error Handling</strong></summary><br> **Objective:** To prevent the leakage of sensitive system information through error messages while <br>providing sufficient detail for internal debugging.<br> **Status:** Passed<br> > Learn more about managing compliance <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#configuration-options'>generic rules</a> or creating your own <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#custom-compliance'>custom rules</a> </details></td></tr> <tr><td rowspan=2>🔴</td> <td><details> <summary><strong>Generic: Comprehensive Audit Trails</strong></summary><br> **Objective:** To create a detailed and reliable record of critical system actions for security analysis <br>and compliance.<br> **Status:** 🏷️<br><a href='https://github.com/carverauto/serviceradar/pull/2279/files#diff-8849d05a7171d718f17babecdf46709110ad58d7dbf378ed29429f2a4d50a5c5R124-R213'><strong>Missing audit logs</strong></a>: Admin CRUD actions for sysmon profiles (create/update/delete/toggle/set default) are <br>performed without emitting audit log entries including actor identity and outcome.<br> <details open><summary>Referred Code</summary> ```elixir def handle_event("save_profile", %{"form" => params}, socket) do ash_form = socket.assigns.ash_form |> Form.validate(params) scope = socket.assigns.current_scope case Form.submit(ash_form, params: params) do {:ok, _profile} -> action = if socket.assigns.show_form == :new_profile, do: "created", else: "updated" {:noreply, socket |> assign(:profiles, load_profiles(scope)) |> put_flash(:info, "Profile #{action} successfully") |> push_navigate(to: ~p"/settings/sysmon")} {:error, ash_form} -> {:noreply, socket |> assign(:ash_form, ash_form) |> assign(:form, to_form(ash_form))} end end ... (clipped 69 lines) ``` </details> > Learn more about managing compliance <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#configuration-options'>generic rules</a> or creating your own <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#custom-compliance'>custom rules</a> </details></td></tr> <tr><td><details> <summary><strong>Generic: Robust Error Handling and Edge Case Management</strong></summary><br> **Objective:** Ensure comprehensive error handling that provides meaningful context and graceful <br>degradation<br> **Status:** 🏷️<br><a href='https://github.com/carverauto/serviceradar/pull/2279/files#diff-8849d05a7171d718f17babecdf46709110ad58d7dbf378ed29429f2a4d50a5c5R914-R957'><strong>Swallowed parse errors</strong></a>: SRQL parsing/JSON decoding/counting errors are swallowed via broad <code>rescue</code> and <code>{:error, _} </code><br><code>-&gt; nil</code> branches without logging/context, making production diagnosis difficult.<br> <details open><summary>Referred Code</summary> ```elixir defp compile_profile_preview(profile) do config = SysmonCompiler.compile_profile(profile) Jason.encode!(config, pretty: true) rescue _ -> "{\"error\": \"Failed to compile config\"}" end defp count_target_devices(_scope, nil), do: nil defp count_target_devices(_scope, ""), do: nil defp count_target_devices(scope, target_query) when is_binary(target_query) do # Parse the SRQL query and count matching devices case ServiceRadarSRQL.Native.parse_ast(target_query) do {:ok, ast_json} -> case Jason.decode(ast_json) do {:ok, ast} -> count_devices_from_ast(scope, ast) {:error, _} -> nil end ... (clipped 23 lines) ``` </details> > Learn more about managing compliance <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#configuration-options'>generic rules</a> or creating your own <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#custom-compliance'>custom rules</a> </details></td></tr> <tr><td rowspan=2>⚪</td> <td><details> <summary><strong>Generic: Secure Logging Practices</strong></summary><br> **Objective:** To ensure logs are useful for debugging and auditing without exposing sensitive <br>information like PII, PHI, or cardholder data.<br> **Status:** 🏷️<br><a href='https://github.com/carverauto/serviceradar/pull/2279/files#diff-9b32fb2972aa43999e1afb261429b23bcba6a8868eab704270158bb12e1825beR145-R156'><strong>Unvetted error logging</strong></a>: Logging <code>inspect(reason)</code> may expose sensitive internal/DB details depending on error <br>contents, and the diff does not show whether logs are structured/redacted.<br> <details open><summary>Referred Code</summary> ```elixir Logger.warning( "Failed to upsert device for agent #{agent_id}: #{inspect(reason)}" ) {:error, reason} end {:error, reason} -> Logger.warning( "Failed to resolve device ID for agent #{agent_id}: #{inspect(reason)}" ) ``` </details> > Learn more about managing compliance <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#configuration-options'>generic rules</a> or creating your own <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#custom-compliance'>custom rules</a> </details></td></tr> <tr><td><details> <summary><strong>Generic: Security-First Input Validation and Data Handling</strong></summary><br> **Objective:** Ensure all data inputs are validated, sanitized, and handled securely to prevent <br>vulnerabilities<br> **Status:** 🏷️<br><a href='https://github.com/carverauto/serviceradar/pull/2279/files#diff-369a368073dc8ec1140bcea699005a1ce97a90cd59629df0bd18c71c7ffaae9fR794-R829'><strong>Weak input validation</strong></a>: Device attributes derived from agent requests (e.g., <code>hostname</code>, <code>os</code>, <code>arch</code>, <code>source_ip</code>, and <br><code>capabilities</code>) are forwarded into core/device creation flows without clear <br>validation/sanitization in the visible diff.<br> <details open><summary>Referred Code</summary> ```elixir defp ensure_device_for_agent(tenant_info, agent_id, partition_id, request, source_ip) do attrs = device_attrs_from_request(partition_id, request, source_ip) case core_call(AgentGatewaySync, :ensure_device_for_agent, [ agent_id, tenant_info.tenant_id, attrs ]) do {:ok, {:ok, device_uid}} -> Logger.debug("Agent #{agent_id} linked to device #{device_uid}") :ok {:ok, {:error, reason}} -> # Device creation failure is non-fatal - agent can still operate Logger.warning("Failed to create device for agent #{agent_id}: #{inspect(reason)}") :ok {:error, :core_unavailable} -> # Non-fatal - device will be created on next hello Logger.warning("Core unavailable while creating device for agent #{agent_id}") :ok ... (clipped 15 lines) ``` </details> > Learn more about managing compliance <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#configuration-options'>generic rules</a> or creating your own <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#custom-compliance'>custom rules</a> </details></td></tr> <tr><td align="center" colspan="2"> <!-- /compliance --update_compliance=true --> </td></tr></tbody></table> </details> <details> <summary>Compliance check up to commit <a href='https://github.com/carverauto/serviceradar/commit/7ab36f95264ce578fb7b8a34dad9a974f9363a04'>7ab36f9</a></summary><br> <table><tbody><tr><td colspan='2'><strong>Security Compliance</strong></td></tr> <tr><td rowspan=1>⚪</td> <td><details><summary><strong>SRQL injection risk </strong></summary><br> <b>Description:</b> The code builds <code>combined_query = "#{target_query} uid:#{device_uid}"</code> without <br>escaping/quoting <code>device_uid</code>, so a crafted device UID containing SRQL syntax/whitespace <br>could alter query semantics and potentially bypass/expand targeting behavior. <strong><a href='https://github.com/carverauto/serviceradar/pull/2279/files#diff-92fc55ddb645aa284c8bded705d5750290d8058cce77ca339f97d5d77fe5b9d2R104-R138'>srql_target_resolver.ex [104-138]</a></strong><br> <details open><summary>Referred Code</summary> ```elixir # Check if a profile's target_query matches a device defp matches_device?(profile, tenant_schema, device_uid, actor) do target_query = profile.target_query if is_nil(target_query) or target_query == "" do {:ok, false} else # Build the combined query: original query + device UID filter combined_query = "#{target_query} uid:#{device_uid}" case execute_srql_match(combined_query, tenant_schema, actor) do {:ok, matched} -> {:ok, matched} {:error, reason} -> {:error, reason} end end end # Execute an SRQL query and check if it returns results defp execute_srql_match(query_string, tenant_schema, actor) do # Parse the SRQL query to get filters case ServiceRadarSRQL.Native.parse_ast(query_string) do ... (clipped 14 lines) ``` </details></details></td></tr> <tr><td colspan='2'><strong>Ticket Compliance</strong></td></tr> <tr><td>🟡</td> <td> <details> <summary>🎫 <a href=https://github.com/carverauto/serviceradar/issues/2277>#2277</a></summary> <table width='100%'><tbody> <tr><td rowspan=1>🟢</td> <td>Integrate sysmon configuration into the agent/backend contract (e.g., introduce <br><code>SysmonConfig</code> in protobufs and related config plumbing).<br></td></tr> <tr><td rowspan=4>⚪</td> <td>Replace the non-OSX Rust <code>sysinfo</code>-based sysmon with a unified implementation in Go so there <br>is a single sysmon codebase across platforms.<br></td></tr> <tr><td>Create a new pure-Go library at <code>pkg/sysmon</code> (incorporating prior sysmon-osx functionality) <br>and implement system metric collection (CPU/memory/disk/network/processes), leveraging Go <br>libraries (e.g., <code>github.com/shirou/gopsutil</code>).<br></td></tr> <tr><td>Keep data structures compatible with the existing Rust <code>serviceradar-sysmon</code> package to ease <br>integration back into ServiceRadar.<br></td></tr> <tr><td>Integrate the new sysmon library into <code>serviceradar-agent</code> as part of consolidation of <br>Go-based components into one installation package.<br></td></tr> </tbody></table> </details> </td></tr> <tr><td colspan='2'><strong>Codebase Duplication Compliance</strong></td></tr> <tr><td>⚪</td><td><details><summary><strong>Codebase context is not defined </strong></summary> Follow the <a href='https://qodo-merge-docs.qodo.ai/core-abilities/rag_context_enrichment/'>guide</a> to enable codebase context checks. </details></td></tr> <tr><td colspan='2'><strong>Custom Compliance</strong></td></tr> <tr><td rowspan=1>🟢</td><td> <details><summary><strong>Generic: Meaningful Naming and Self-Documenting Code</strong></summary><br> **Objective:** Ensure all identifiers clearly express their purpose and intent, making code <br>self-documenting<br> **Status:** Passed<br> > Learn more about managing compliance <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#configuration-options'>generic rules</a> or creating your own <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#custom-compliance'>custom rules</a> </details></td></tr> <tr><td rowspan=3>🔴</td> <td><details> <summary><strong>Generic: Comprehensive Audit Trails</strong></summary><br> **Objective:** To create a detailed and reliable record of critical system actions for security analysis <br>and compliance.<br> **Status:** <br><a href='https://github.com/carverauto/serviceradar/pull/2279/files#diff-8849d05a7171d718f17babecdf46709110ad58d7dbf378ed29429f2a4d50a5c5R119-R208'><strong>Missing audit logging</strong></a>: Critical sysmon profile actions (create/update/delete/toggle/set default) are performed <br>without recording an audit log containing actor/user context and outcome.<br> <details open><summary>Referred Code</summary> ```elixir def handle_event("save_profile", %{"form" => params}, socket) do ash_form = socket.assigns.ash_form |> Form.validate(params) scope = socket.assigns.current_scope case Form.submit(ash_form, params: params) do {:ok, _profile} -> action = if socket.assigns.show_form == :new_profile, do: "created", else: "updated" {:noreply, socket |> assign(:profiles, load_profiles(scope)) |> put_flash(:info, "Profile #{action} successfully") |> push_navigate(to: ~p"/settings/sysmon")} {:error, ash_form} -> {:noreply, socket |> assign(:ash_form, ash_form) |> assign(:form, to_form(ash_form))} end end ... (clipped 69 lines) ``` </details> > Learn more about managing compliance <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#configuration-options'>generic rules</a> or creating your own <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#custom-compliance'>custom rules</a> </details></td></tr> <tr><td><details> <summary><strong>Generic: Robust Error Handling and Edge Case Management</strong></summary><br> **Objective:** Ensure comprehensive error handling that provides meaningful context and graceful <br>degradation<br> **Status:** <br><a href='https://github.com/carverauto/serviceradar/pull/2279/files#diff-261a01f4876e5984e1d9e9b38a3540675dfb0272abc30e6bdb2a4fa610353cc7R1511-R1539'><strong>Unsafe destructive ops</strong></a>: Bulk assignment deletes existing assignments using <code>Ash.destroy!</code> inside <code>Enum.each</code>, which <br>can raise and crash the LiveView and is not wrapped in a transaction/rollback for partial <br>failures.<br> <details open><summary>Referred Code</summary> ```elixir defp create_sysmon_assignments(scope, device_uids, profile_id) do # First, remove any existing device-specific assignments for these devices existing_query = SysmonProfileAssignment |> Ash.Query.for_read(:read, %{}) |> Ash.Query.filter(device_uid in ^device_uids) # Delete existing assignments case Ash.read(existing_query, scope: scope) do {:ok, existing} -> Enum.each(existing, fn assignment -> Ash.destroy!(assignment, scope: scope) end) _ -> :ok end # Create new assignments for each device results = Enum.map(device_uids, fn device_uid -> ... (clipped 8 lines) ``` </details> > Learn more about managing compliance <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#configuration-options'>generic rules</a> or creating your own <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#custom-compliance'>custom rules</a> </details></td></tr> <tr><td><details> <summary><strong>Generic: Secure Error Handling</strong></summary><br> **Objective:** To prevent the leakage of sensitive system information through error messages while <br>providing sufficient detail for internal debugging.<br> **Status:** <br><a href='https://github.com/carverauto/serviceradar/pull/2279/files#diff-261a01f4876e5984e1d9e9b38a3540675dfb0272abc30e6bdb2a4fa610353cc7R280-R282'><strong>Leaky user error</strong></a>: A user-facing flash message interpolates the underlying failure <code>reason</code>, which may expose <br>internal details (e.g., database/validation errors) to the end-user.<br> <details open><summary>Referred Code</summary> ```elixir {:error, reason} -> {:noreply, put_flash(socket, :error, "Failed to assign profile: #{reason}")} end ``` </details> > Learn more about managing compliance <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#configuration-options'>generic rules</a> or creating your own <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#custom-compliance'>custom rules</a> </details></td></tr> <tr><td rowspan=2>⚪</td> <td><details> <summary><strong>Generic: Secure Logging Practices</strong></summary><br> **Objective:** To ensure logs are useful for debugging and auditing without exposing sensitive <br>information like PII, PHI, or cardholder data.<br> **Status:** <br><a href='https://github.com/carverauto/serviceradar/pull/2279/files#diff-f368b9b41fa062759f00ff6fcae314cc5a42bb1caca82a9069103a803df1f9d7R331-R337'><strong>Detailed reason logged</strong></a>: Warning logs include <code>inspect(reason)</code> which could unintentionally log sensitive internal <br>data depending on what upstream errors contain, requiring verification of logged error <br>contents.<br> <details open><summary>Referred Code</summary> ```elixir {:error, reason} -> Logger.warning( "Failed to load sysmon config for agent #{agent_id}: #{inspect(reason)}" ) ServiceRadar.AgentConfig.Compilers.SysmonCompiler.default_config() end ``` </details> > Learn more about managing compliance <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#configuration-options'>generic rules</a> or creating your own <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#custom-compliance'>custom rules</a> </details></td></tr> <tr><td><details> <summary><strong>Generic: Security-First Input Validation and Data Handling</strong></summary><br> **Objective:** Ensure all data inputs are validated, sanitized, and handled securely to prevent <br>vulnerabilities<br> **Status:** <br><a href='https://github.com/carverauto/serviceradar/pull/2279/files#diff-261a01f4876e5984e1d9e9b38a3540675dfb0272abc30e6bdb2a4fa610353cc7R263-R284'><strong>Authorization not visible</strong></a>: The LiveView bulk sysmon profile assignment path accepts a client-provided <code>profile_id</code> and <br>performs destructive/creation operations without visible authorization checks in the diff, <br>requiring verification that access control is enforced elsewhere.<br> <details open><summary>Referred Code</summary> ```elixir def handle_event("apply_bulk_sysmon_profile", _params, socket) do profile_id = socket.assigns.bulk_sysmon_profile_id if is_nil(profile_id) or profile_id == "" do {:noreply, put_flash(socket, :error, "Please select a profile")} else case apply_sysmon_profile_to_devices(socket, profile_id) do {:ok, count} -> {:noreply, socket |> assign(:show_bulk_sysmon_modal, false) |> assign(:bulk_sysmon_profile_id, nil) |> assign(:selected_devices, MapSet.new()) |> assign(:select_all_matching, false) |> assign(:total_matching_count, nil) |> put_flash(:info, "Assigned sysmon profile to #{count} device(s)")} {:error, reason} -> {:noreply, put_flash(socket, :error, "Failed to assign profile: #{reason}")} end end ... (clipped 1 lines) ``` </details> > Learn more about managing compliance <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#configuration-options'>generic rules</a> or creating your own <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#custom-compliance'>custom rules</a> </details></td></tr> <tr><td align="center" colspan="2"> <!-- /compliance --update_compliance=true --> </td></tr></tbody></table> </details>
qodo-code-review[bot] commented 2026-01-14 02:36:41 +00:00 (Migrated from github.com)
Author
Owner

Imported GitHub PR comment.

Original author: @qodo-code-review[bot]
Original URL: https://github.com/carverauto/serviceradar/pull/2279#issuecomment-3747419811
Original created: 2026-01-14T02:36:41Z

PR Code Suggestions

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Consolidate sysmon profile targeting mechanisms
Suggestion Impact:The compiler was simplified to resolve profiles only via SRQL targeting (then default), removed all device/tag assignment resolution logic and references, updated documentation/comments accordingly, and deleted the SysmonProfileAssignment resource file (effectively consolidating targeting into SRQL).

code diff:

-  Transforms SysmonProfile and SysmonProfileAssignment Ash resources into
-  agent-consumable sysmon configuration format.
+  Transforms SysmonProfile Ash resources into agent-consumable sysmon
+  configuration format using SRQL-based targeting.
 
   ## Resolution Order
 
   When resolving which profile applies to a device:
-  1. Device-specific assignment (legacy, for backwards compatibility)
-  2. SRQL targeting profiles (ordered by priority, highest first)
-  3. Tag-based assignments (legacy, for backwards compatibility)
-  4. Default tenant profile (fallback)
+  1. SRQL targeting profiles (ordered by priority, highest first)
+  2. Default tenant profile (fallback)
 
-  SRQL targeting is the preferred method. Device and tag assignments are
-  maintained for backwards compatibility during the migration period.
+  Profiles use `target_query` (SRQL) to define which devices they apply to.
+  Example: `target_query: "in:devices tags.role:database"` matches all devices
+  with the tag `role=database`.
 
   ## Output Format
 
@@ -46,16 +45,15 @@
 
   alias ServiceRadar.Actors.SystemActor
   alias ServiceRadar.Cluster.TenantSchemas
-  alias ServiceRadar.Inventory.Device
   alias ServiceRadar.SysmonProfiles.SrqlTargetResolver
-  alias ServiceRadar.SysmonProfiles.{SysmonProfile, SysmonProfileAssignment}
+  alias ServiceRadar.SysmonProfiles.SysmonProfile
 
   @impl true
   def config_type, do: :sysmon
 
   @impl true
   def source_resources do
-    [SysmonProfile, SysmonProfileAssignment]
+    [SysmonProfile]
   end
 
   @impl true
@@ -68,7 +66,7 @@
     profile = resolve_profile(tenant_schema, device_uid, agent_id, actor)
 
     if profile do
-      config = compile_profile(profile, tenant_schema, actor)
+      config = compile_profile(profile)
       {:ok, config}
     else
       # Return default config if no profile found
@@ -95,28 +93,18 @@
   end
 
   @doc """
-  Resolves the sysmon profile for a device.
+  Resolves the sysmon profile for a device using SRQL targeting.
 
   Resolution order:
-  1. Device-specific assignment (legacy, for backwards compatibility)
-  2. SRQL targeting profiles (ordered by priority, highest first)
-  3. Tag-based assignment (legacy, for backwards compatibility)
-  4. Default profile for tenant
+  1. SRQL targeting profiles (ordered by priority, highest first)
+  2. Default profile for tenant
 
-  Returns `{profile, config_source}` tuple where config_source indicates
-  how the profile was resolved ("device", "srql", "tag", or "default").
+  Returns the matching SysmonProfile or nil if no profile matches.
   """
   @spec resolve_profile(String.t(), String.t() | nil, String.t() | nil, map()) ::
           SysmonProfile.t() | nil
   def resolve_profile(tenant_schema, device_uid, _agent_id, actor) do
-    # Resolution order:
-    # 1. Device-specific assignment (legacy)
-    # 2. SRQL targeting profiles
-    # 3. Tag-based assignment (legacy)
-    # 4. Default profile
-    try_device_assignment(tenant_schema, device_uid, actor) ||
-      try_srql_targeting(tenant_schema, device_uid, actor) ||
-      try_tag_assignment(tenant_schema, device_uid, actor) ||
+    try_srql_targeting(tenant_schema, device_uid, actor) ||
       get_default_profile(tenant_schema, actor)
   end
 
@@ -125,7 +113,9 @@
 
   defp try_srql_targeting(tenant_schema, device_uid, actor) do
     case SrqlTargetResolver.resolve_for_device(tenant_schema, device_uid, actor) do
-      {:ok, profile} -> profile
+      {:ok, profile} ->
+        profile
+
       {:error, reason} ->
         Logger.warning("SysmonCompiler: SRQL targeting failed - #{inspect(reason)}")
         nil
@@ -135,8 +125,15 @@
   @doc """
   Compiles a profile to the agent config format.
   """
-  @spec compile_profile(SysmonProfile.t(), String.t(), map()) :: map()
-  def compile_profile(profile, _tenant_schema, _actor, config_source \\ "profile") do
+  @spec compile_profile(SysmonProfile.t()) :: map()
+  def compile_profile(profile) do
+    config_source =
+      cond do
+        profile.is_default -> "default"
+        not is_nil(profile.target_query) -> "srql"
+        true -> "profile"
+      end
+
     %{
       "enabled" => profile.enabled,
       "sample_interval" => profile.sample_interval,
@@ -174,98 +171,7 @@
     }
   end
 
-  # Private helpers
-
-  defp try_device_assignment(_tenant_schema, nil, _actor), do: nil
-
-  defp try_device_assignment(tenant_schema, device_uid, actor) do
-    query =
-      SysmonProfileAssignment
-      |> Ash.Query.for_read(:for_device, %{device_uid: device_uid},
-        actor: actor,
-        tenant: tenant_schema
-      )
-      |> Ash.Query.load(:profile)
-
-    case Ash.read_one(query, actor: actor) do
-      {:ok, nil} ->
-        nil
-
-      {:ok, assignment} ->
-        assignment.profile
-
-      {:error, reason} ->
-        Logger.warning("SysmonCompiler: failed to load device assignment - #{inspect(reason)}")
-        nil
-    end
-  end
-
-  defp try_tag_assignment(_tenant_schema, nil, _actor), do: nil
-
-  defp try_tag_assignment(tenant_schema, device_uid, actor) do
-    # Load device to get its tags
-    device = load_device(tenant_schema, device_uid, actor)
-
-    if device && is_map(device.tags) && map_size(device.tags) > 0 do
-      # Get all tag assignments and find the highest priority match
-      assignments = load_tag_assignments(tenant_schema, actor)
-
-      matching_assignment =
-        assignments
-        |> Enum.filter(&tag_matches_device?(&1, device.tags))
-        |> Enum.sort_by(& &1.priority, :desc)
-        |> List.first()
-
-      if matching_assignment do
-        matching_assignment.profile
-      else
-        nil
-      end
-    else
-      nil
-    end
-  end
-
-  defp load_device(tenant_schema, device_uid, actor) do
-    query =
-      Device
-      |> Ash.Query.for_read(:by_uid, %{uid: device_uid}, actor: actor, tenant: tenant_schema)
-
-    case Ash.read_one(query, actor: actor) do
-      {:ok, device} -> device
-      {:error, _} -> nil
-    end
-  end
-
-  defp load_tag_assignments(tenant_schema, actor) do
-    query =
-      SysmonProfileAssignment
-      |> Ash.Query.for_read(:read, %{}, actor: actor, tenant: tenant_schema)
-      |> Ash.Query.filter(assignment_type == :tag)
-      |> Ash.Query.load(:profile)
-
-    case Ash.read(query, actor: actor) do
-      {:ok, assignments} -> assignments
-      {:error, _} -> []
-    end
-  end
-
-  defp tag_matches_device?(assignment, device_tags) do
-    tag_key = assignment.tag_key
-    tag_value = assignment.tag_value
-
-    device_value = Map.get(device_tags, tag_key)
-
-    cond do
-      # Device doesn't have this tag
-      is_nil(device_value) -> false
-      # Assignment matches any value for this key
-      is_nil(tag_value) -> true
-      # Assignment matches specific value
-      true -> device_value == tag_value
-    end
-  end
-
+  # Get the default profile for the tenant
   defp get_default_profile(tenant_schema, actor) do
     query =
       SysmonProfile

# File: elixir/serviceradar_core/lib/serviceradar/sysmon_profiles/sysmon_profile_assignment.ex

The current implementation uses two methods for sysmon profile targeting: SRQL
queries and separate assignment resources. This should be consolidated into a
single, SRQL-based system to reduce complexity.

Examples:

elixir/serviceradar_core/lib/serviceradar/agent_config/compilers/sysmon_compiler.ex [109-121]
  @spec resolve_profile(String.t(), String.t() | nil, String.t() | nil, map()) ::
          SysmonProfile.t() | nil
  def resolve_profile(tenant_schema, device_uid, _agent_id, actor) do
    # Resolution order:
    # 1. Device-specific assignment (legacy)
    # 2. SRQL targeting profiles
    # 3. Tag-based assignment (legacy)
    # 4. Default profile
    try_device_assignment(tenant_schema, device_uid, actor) ||
      try_srql_targeting(tenant_schema, device_uid, actor) ||

 ... (clipped 3 lines)
elixir/serviceradar_core/lib/serviceradar/sysmon_profiles/sysmon_profile_assignment.ex [1-233]
defmodule ServiceRadar.SysmonProfiles.SysmonProfileAssignment do
  @moduledoc """
  Assignment of sysmon profiles to devices or tags.

  SysmonProfileAssignment enables flexible profile targeting:
  - Device-specific assignments: Directly assign a profile to a specific device
  - Tag-based assignments: Assign a profile to all devices matching a tag

  ## Assignment Types


 ... (clipped 223 lines)

Solution Walkthrough:

Before:

# Two separate resources for targeting
resource SysmonProfile do
  # ...
  attribute :target_query, :string # SRQL-based targeting
  # ...
end

resource SysmonProfileAssignment do
  # ...
  attribute :assignment_type, :atom, one_of: [:device, :tag] # Direct/tag assignment
  attribute :device_uid, :string
  attribute :tag_key, :string
  # ...
end

# Complex resolution logic in the compiler
def resolve_profile(device_uid) do
  try_device_assignment(device_uid) || # Checks SysmonProfileAssignment
  try_srql_targeting(device_uid) ||    # Checks SysmonProfile.target_query
  try_tag_assignment(device_uid) ||    # Checks SysmonProfileAssignment
  get_default_profile()
end

After:

# A single resource for profiles and targeting
resource SysmonProfile do
  # ...
  attribute :target_query, :string # All targeting is done via SRQL
  # ...
end

# The SysmonProfileAssignment resource is removed.

# Simplified resolution logic in the compiler
def resolve_profile(device_uid) do
  # UI for direct assignment would generate a query like "uid:<id>"
  try_srql_targeting(device_uid) || # Checks SysmonProfile.target_query
  get_default_profile()
end

Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a significant design issue where two parallel sysmon profile targeting mechanisms (SRQL and direct assignments) are introduced, leading to increased complexity in the backend resolution logic and a confusing user experience.

High
Possible issue
Prevent race condition in stop

Modify the Stop function to hold the mutex lock for the entire critical section
to prevent a race condition that could cause a panic.

pkg/agent/sysmon_service.go [195-233]

 // Stop halts the sysmon collector and config refresh loop.
 func (s *SysmonService) Stop(ctx context.Context) error {
 	s.mu.Lock()
 
 	if !s.started {
 		s.mu.Unlock()
 		return nil
 	}
 
 	// Stop the config refresh loop
 	if s.stopRefresh != nil {
 		close(s.stopRefresh)
 	}
+
+	// Stop the collector while holding the lock
+	if s.collector != nil {
+		if err := s.collector.Stop(); err != nil {
+			s.mu.Unlock()
+			return fmt.Errorf("failed to stop sysmon collector: %w", err)
+		}
+	}
+
+	s.started = false
 	s.mu.Unlock()
 
 	// Wait for refresh loop to finish (with timeout from context)
 	if s.refreshDone != nil {
 		select {
 		case <-s.refreshDone:
 			// Refresh loop stopped
 		case <-ctx.Done():
 			s.logger.Warn().Msg("Timeout waiting for config refresh loop to stop")
 		case <-time.After(5 * time.Second):
 			s.logger.Warn().Msg("Timeout waiting for config refresh loop to stop")
 		}
 	}
 
-	s.mu.Lock()
-	defer s.mu.Unlock()
-
-	if s.collector != nil {
-		if err := s.collector.Stop(); err != nil {
-			return fmt.Errorf("failed to stop sysmon collector: %w", err)
-		}
-	}
-
-	s.started = false
 	s.logger.Info().Msg("Sysmon service stopped")
 	return nil
 }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a race condition in the Stop function that could lead to a panic from a double-close on a channel. This is a critical concurrency bug fix.

High
Fix incorrect device count preview

Update the apply_srql_filter/2 function to correctly handle all filter operators
(contains, not_contains, equals, not_equals) when calculating the target device
count preview, ensuring the count is accurate.

web-ng/lib/serviceradar_web_ng_web/live/settings/sysmon_profiles_live/index.ex [940-964]

-defp apply_srql_filter(query, %{field: field, op: _op, value: value}) when is_binary(field) do
+defp apply_srql_filter(query, %{field: field, op: op, value: value}) when is_binary(field) do
   if String.starts_with?(field, "tags.") do
     tag_key = String.replace_prefix(field, "tags.", "")
+    # This part still only handles equality for tags. A more complex implementation
+    # would be needed for other operators on tags. For now, we keep it as is.
     Ash.Query.filter(query, fragment("tags @> ?", ^%{tag_key => value}))
   else
     # Map common fields
     mapped_field =
       case field do
         "hostname" -> :hostname
         "uid" -> :uid
         "type" -> :type_id
         "os" -> :os
         "status" -> :status
         _ -> nil
       end
 
     if mapped_field do
-      Ash.Query.filter_input(query, %{mapped_field => %{eq: value}})
+      case op do
+        "contains" -> Ash.Query.filter_input(query, %{mapped_field => %{like: "%#{value}%"}})
+        "not_contains" -> Ash.Query.filter_input(query, %{mapped_field => %{not: %{like: "%#{value}%"}}})
+        "not_equals" -> Ash.Query.filter_input(query, %{mapped_field => %{not: %{eq: value}}})
+        _ -> Ash.Query.filter_input(query, %{mapped_field => %{eq: value}})
+      end
     else
       query
     end
   end
 rescue
   _ -> query
 end

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a bug where the filter operator is ignored, leading to an inaccurate device count preview, and provides a correct fix.

Medium
Prevent incorrect deletion of assignments
Suggestion Impact:The PR did not implement the suggested query filter or the change from Ash.destroy!/2 to Ash.destroy/2. Instead, it removed the entire bulk sysmon profile assignment workflow, including create_sysmon_assignments/3 and all related UI/event handlers, eliminating the deletion logic altogether (and therefore also eliminating the risk of incorrect deletions in that code path).

code diff:

@@ -233,52 +230,6 @@
            socket
            |> assign(:bulk_edit_form, to_form(params, as: :bulk))
            |> put_flash(:error, "Failed to apply tags: #{reason}")}
-      end
-    end
-  end
-
-  # Bulk sysmon profile assignment handlers
-  def handle_event("open_bulk_sysmon_modal", _params, socket) do
-    scope = socket.assigns.current_scope
-    profiles = load_sysmon_profiles(scope)
-
-    {:noreply,
-     socket
-     |> assign(:show_bulk_sysmon_modal, true)
-     |> assign(:available_sysmon_profiles, profiles)
-     |> assign(:bulk_sysmon_profile_id, nil)}
-  end
-
-  def handle_event("close_bulk_sysmon_modal", _params, socket) do
-    {:noreply,
-     socket
-     |> assign(:show_bulk_sysmon_modal, false)
-     |> assign(:bulk_sysmon_profile_id, nil)}
-  end
-
-  def handle_event("select_bulk_sysmon_profile", %{"profile_id" => profile_id}, socket) do
-    {:noreply, assign(socket, :bulk_sysmon_profile_id, profile_id)}
-  end
-
-  def handle_event("apply_bulk_sysmon_profile", _params, socket) do
-    profile_id = socket.assigns.bulk_sysmon_profile_id
-
-    if is_nil(profile_id) or profile_id == "" do
-      {:noreply, put_flash(socket, :error, "Please select a profile")}
-    else
-      case apply_sysmon_profile_to_devices(socket, profile_id) do
-        {:ok, count} ->
-          {:noreply,
-           socket
-           |> assign(:show_bulk_sysmon_modal, false)
-           |> assign(:bulk_sysmon_profile_id, nil)
-           |> assign(:selected_devices, MapSet.new())
-           |> assign(:select_all_matching, false)
-           |> assign(:total_matching_count, nil)
-           |> put_flash(:info, "Assigned sysmon profile to #{count} device(s)")}
-
-        {:error, reason} ->
-          {:noreply, put_flash(socket, :error, "Failed to assign profile: #{reason}")}
       end
     end
   end
@@ -490,9 +441,6 @@
             </button>
           </div>
           <div class="flex items-center gap-2">
-            <.ui_button variant="ghost" size="sm" phx-click="open_bulk_sysmon_modal">
-              <.icon name="hero-cpu-chip" class="size-4" /> Assign Profile
-            </.ui_button>
             <.ui_button variant="primary" size="sm" phx-click="open_bulk_edit_modal">
               <.icon name="hero-tag" class="size-4" /> Bulk Edit
             </.ui_button>
@@ -575,7 +523,9 @@
                   <% has_sysmon =
                     is_binary(device_uid) and Map.get(@sysmon_presence, device_uid, false) == true %>
                   <% sysmon_profile =
-                    if is_binary(device_uid), do: Map.get(@sysmon_profiles_by_device, device_uid), else: nil %>
+                    if is_binary(device_uid),
+                      do: Map.get(@sysmon_profiles_by_device, device_uid),
+                      else: nil %>
                   <tr class={"hover:bg-base-200/40 #{if is_selected, do: "bg-primary/5", else: ""}"}>
                     <td class="text-center">
                       <input
@@ -660,13 +610,6 @@
         form={@bulk_edit_form}
         selected_count={@effective_count}
       />
-      <!-- Bulk Sysmon Profile Assignment Modal -->
-      <.bulk_sysmon_modal
-        :if={@show_bulk_sysmon_modal}
-        profiles={@available_sysmon_profiles}
-        selected_profile_id={@bulk_sysmon_profile_id}
-        selected_count={@effective_count}
-      />
     </Layouts.app>
     """
   end
@@ -720,82 +663,6 @@
       </div>
       <form method="dialog" class="modal-backdrop">
         <button phx-click="close_bulk_edit_modal">close</button>
-      </form>
-    </dialog>
-    """
-  end
-
-  # Bulk Sysmon Profile Modal Component
-  attr :profiles, :list, required: true
-  attr :selected_profile_id, :string, default: nil
-  attr :selected_count, :integer, required: true
-
-  defp bulk_sysmon_modal(assigns) do
-    ~H"""
-    <dialog id="bulk_sysmon_modal" class="modal modal-open">
-      <div class="modal-box max-w-lg">
-        <form method="dialog">
-          <button
-            class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
-            phx-click="close_bulk_sysmon_modal"
-          >
-            x
-          </button>
-        </form>
-
-        <h3 class="text-lg font-bold">Assign Sysmon Profile</h3>
-        <p class="py-2 text-sm text-base-content/70">
-          Assign a sysmon monitoring profile to {@selected_count} selected device(s).
-        </p>
-
-        <div class="form-control w-full">
-          <label class="label">
-            <span class="label-text font-medium">Select Profile</span>
-          </label>
-          <div class="space-y-2">
-            <%= for profile <- @profiles do %>
-              <label class={[
-                "flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-colors",
-                if(@selected_profile_id == profile.id, do: "border-primary bg-primary/10", else: "border-base-200 hover:bg-base-100")
-              ]}>
-                <input
-                  type="radio"
-                  name="sysmon_profile"
-                  class="radio radio-primary radio-sm"
-                  checked={@selected_profile_id == profile.id}
-                  phx-click="select_bulk_sysmon_profile"
-                  phx-value-profile_id={profile.id}
-                />
-                <div class="flex-1">
-                  <div class="font-medium text-sm">{profile.name}</div>
-                  <div class="text-xs text-base-content/60">
-                    Interval: {profile.interval_seconds}s
-                    <%= if profile.is_default do %>
-                      <span class="badge badge-ghost badge-xs ml-2">Default</span>
-                    <% end %>
-                  </div>
-                </div>
-              </label>
-            <% end %>
-          </div>
-        </div>
-
-        <div class="flex justify-end gap-2 pt-4">
-          <button type="button" phx-click="close_bulk_sysmon_modal" class="btn btn-ghost">
-            Cancel
-          </button>
-          <button
-            type="button"
-            phx-click="apply_bulk_sysmon_profile"
-            class="btn btn-primary"
-            disabled={is_nil(@selected_profile_id)}
-          >
-            Assign Profile
-          </button>
-        </div>
-      </div>
-      <form method="dialog" class="modal-backdrop">
-        <button phx-click="close_bulk_sysmon_modal">close</button>
       </form>
     </dialog>
     """
@@ -1476,123 +1343,21 @@
   end
 
   # Sysmon profile helpers
-  defp load_sysmon_profiles(scope) do
-    SysmonProfile
-    |> Ash.Query.for_read(:read, %{})
-    |> Ash.Query.sort([:name])
-    |> Ash.read!(scope: scope)
-  rescue
-    _ -> []
-  end
-
-  defp apply_sysmon_profile_to_devices(socket, profile_id) do
-    scope = socket.assigns.current_scope
-
-    cond do
-      socket.assigns.select_all_matching and
-          not is_integer(socket.assigns.total_matching_count) ->
-        {:error, "Unable to determine selection size. Please try again."}
-
-      socket.assigns.select_all_matching and
-          socket.assigns.total_matching_count > 10_000 ->
-        {:error, "Too many devices selected. Narrow your filters and try again."}
-
-      true ->
-        case get_selected_uids(socket) do
-          [] ->
-            {:error, "No devices selected"}
-
-          uids ->
-            create_sysmon_assignments(scope, uids, profile_id)
-        end
-    end
-  end
-
-  defp create_sysmon_assignments(scope, device_uids, profile_id) do
-    # First, remove any existing device-specific assignments for these devices
-    existing_query =
-      SysmonProfileAssignment
-      |> Ash.Query.for_read(:read, %{})
-      |> Ash.Query.filter(device_uid in ^device_uids)
-
-    # Delete existing assignments
-    case Ash.read(existing_query, scope: scope) do
-      {:ok, existing} ->
-        Enum.each(existing, fn assignment ->
-          Ash.destroy!(assignment, scope: scope)
-        end)
-
-      _ ->
-        :ok
-    end
-
-    # Create new assignments for each device
-    results =
-      Enum.map(device_uids, fn device_uid ->
-        SysmonProfileAssignment
-        |> Ash.Changeset.for_create(:create, %{
-          sysmon_profile_id: profile_id,
-          device_uid: device_uid,
-          priority: 100
-        })
-        |> Ash.create(scope: scope)
-      end)
-
-    success_count = Enum.count(results, fn
-      {:ok, _} -> true
-      _ -> false
-    end)
-
-    if success_count > 0 do
-      {:ok, success_count}
-    else
-      {:error, "Failed to create assignments"}
-    end
-  end

Refine the create_sysmon_assignments/3 function to only delete device-specific
assignments by adding assignment_type == :device to the query. Also, replace
Ash.destroy!/2 with Ash.destroy/2 for safer error handling.

web-ng/lib/serviceradar_web_ng_web/live/device_live/index.ex [1511-1551]

 defp create_sysmon_assignments(scope, device_uids, profile_id) do
   # First, remove any existing device-specific assignments for these devices
   existing_query =
     SysmonProfileAssignment
     |> Ash.Query.for_read(:read, %{})
-    |> Ash.Query.filter(device_uid in ^device_uids)
+    |> Ash.Query.filter(device_uid in ^device_uids and assignment_type == :device)
 
   # Delete existing assignments
   case Ash.read(existing_query, scope: scope) do
     {:ok, existing} ->
       Enum.each(existing, fn assignment ->
-        Ash.destroy!(assignment, scope: scope)
+        Ash.destroy(assignment, scope: scope)
       end)
 
     _ ->
       :ok
   end
 
   # Create new assignments for each device
   results =
     Enum.map(device_uids, fn device_uid ->
       SysmonProfileAssignment
       |> Ash.Changeset.for_create(:create, %{
         sysmon_profile_id: profile_id,
         device_uid: device_uid,
+        assignment_type: :device,
         priority: 100
       })
       |> Ash.create(scope: scope)
     end)
 
   success_count = Enum.count(results, fn
     {:ok, _} -> true
     _ -> false
   end)
 
   if success_count > 0 do
     {:ok, success_count}
   else
     {:error, "Failed to create assignments"}
   end
 end

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a bug that could cause incorrect data deletion and improves robustness by replacing a crashing function call with a safer alternative.

Medium
Use correct interval field
Suggestion Impact:The commit did not replace `profile.interval_seconds` with `profile.sample_interval`. Instead, it removed the entire bulk sysmon profile assignment modal and its event handlers, including the template line that referenced `profile.interval_seconds`, thereby eliminating the runtime error by removing the feature/code path.

code diff:

@@ -233,52 +230,6 @@
            socket
            |> assign(:bulk_edit_form, to_form(params, as: :bulk))
            |> put_flash(:error, "Failed to apply tags: #{reason}")}
-      end
-    end
-  end
-
-  # Bulk sysmon profile assignment handlers
-  def handle_event("open_bulk_sysmon_modal", _params, socket) do
-    scope = socket.assigns.current_scope
-    profiles = load_sysmon_profiles(scope)
-
-    {:noreply,
-     socket
-     |> assign(:show_bulk_sysmon_modal, true)
-     |> assign(:available_sysmon_profiles, profiles)
-     |> assign(:bulk_sysmon_profile_id, nil)}
-  end
-
-  def handle_event("close_bulk_sysmon_modal", _params, socket) do
-    {:noreply,
-     socket
-     |> assign(:show_bulk_sysmon_modal, false)
-     |> assign(:bulk_sysmon_profile_id, nil)}
-  end
-
-  def handle_event("select_bulk_sysmon_profile", %{"profile_id" => profile_id}, socket) do
-    {:noreply, assign(socket, :bulk_sysmon_profile_id, profile_id)}
-  end
-
-  def handle_event("apply_bulk_sysmon_profile", _params, socket) do
-    profile_id = socket.assigns.bulk_sysmon_profile_id
-
-    if is_nil(profile_id) or profile_id == "" do
-      {:noreply, put_flash(socket, :error, "Please select a profile")}
-    else
-      case apply_sysmon_profile_to_devices(socket, profile_id) do
-        {:ok, count} ->
-          {:noreply,
-           socket
-           |> assign(:show_bulk_sysmon_modal, false)
-           |> assign(:bulk_sysmon_profile_id, nil)
-           |> assign(:selected_devices, MapSet.new())
-           |> assign(:select_all_matching, false)
-           |> assign(:total_matching_count, nil)
-           |> put_flash(:info, "Assigned sysmon profile to #{count} device(s)")}
-
-        {:error, reason} ->
-          {:noreply, put_flash(socket, :error, "Failed to assign profile: #{reason}")}
       end
     end
   end
@@ -490,9 +441,6 @@
             </button>
           </div>
           <div class="flex items-center gap-2">
-            <.ui_button variant="ghost" size="sm" phx-click="open_bulk_sysmon_modal">
-              <.icon name="hero-cpu-chip" class="size-4" /> Assign Profile
-            </.ui_button>
             <.ui_button variant="primary" size="sm" phx-click="open_bulk_edit_modal">
               <.icon name="hero-tag" class="size-4" /> Bulk Edit
             </.ui_button>
@@ -575,7 +523,9 @@
                   <% has_sysmon =
                     is_binary(device_uid) and Map.get(@sysmon_presence, device_uid, false) == true %>
                   <% sysmon_profile =
-                    if is_binary(device_uid), do: Map.get(@sysmon_profiles_by_device, device_uid), else: nil %>
+                    if is_binary(device_uid),
+                      do: Map.get(@sysmon_profiles_by_device, device_uid),
+                      else: nil %>
                   <tr class={"hover:bg-base-200/40 #{if is_selected, do: "bg-primary/5", else: ""}"}>
                     <td class="text-center">
                       <input
@@ -660,13 +610,6 @@
         form={@bulk_edit_form}
         selected_count={@effective_count}
       />
-      <!-- Bulk Sysmon Profile Assignment Modal -->
-      <.bulk_sysmon_modal
-        :if={@show_bulk_sysmon_modal}
-        profiles={@available_sysmon_profiles}
-        selected_profile_id={@bulk_sysmon_profile_id}
-        selected_count={@effective_count}
-      />
     </Layouts.app>
     """
   end
@@ -720,82 +663,6 @@
       </div>
       <form method="dialog" class="modal-backdrop">
         <button phx-click="close_bulk_edit_modal">close</button>
-      </form>
-    </dialog>
-    """
-  end
-
-  # Bulk Sysmon Profile Modal Component
-  attr :profiles, :list, required: true
-  attr :selected_profile_id, :string, default: nil
-  attr :selected_count, :integer, required: true
-
-  defp bulk_sysmon_modal(assigns) do
-    ~H"""
-    <dialog id="bulk_sysmon_modal" class="modal modal-open">
-      <div class="modal-box max-w-lg">
-        <form method="dialog">
-          <button
-            class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
-            phx-click="close_bulk_sysmon_modal"
-          >
-            x
-          </button>
-        </form>
-
-        <h3 class="text-lg font-bold">Assign Sysmon Profile</h3>
-        <p class="py-2 text-sm text-base-content/70">
-          Assign a sysmon monitoring profile to {@selected_count} selected device(s).
-        </p>
-
-        <div class="form-control w-full">
-          <label class="label">
-            <span class="label-text font-medium">Select Profile</span>
-          </label>
-          <div class="space-y-2">
-            <%= for profile <- @profiles do %>
-              <label class={[
-                "flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-colors",
-                if(@selected_profile_id == profile.id, do: "border-primary bg-primary/10", else: "border-base-200 hover:bg-base-100")
-              ]}>
-                <input
-                  type="radio"
-                  name="sysmon_profile"
-                  class="radio radio-primary radio-sm"
-                  checked={@selected_profile_id == profile.id}
-                  phx-click="select_bulk_sysmon_profile"
-                  phx-value-profile_id={profile.id}
-                />
-                <div class="flex-1">
-                  <div class="font-medium text-sm">{profile.name}</div>
-                  <div class="text-xs text-base-content/60">
-                    Interval: {profile.interval_seconds}s
-                    <%= if profile.is_default do %>
-                      <span class="badge badge-ghost badge-xs ml-2">Default</span>
-                    <% end %>
-                  </div>
-                </div>
-              </label>
-            <% end %>
-          </div>
-        </div>
-
-        <div class="flex justify-end gap-2 pt-4">
-          <button type="button" phx-click="close_bulk_sysmon_modal" class="btn btn-ghost">
-            Cancel
-          </button>
-          <button
-            type="button"
-            phx-click="apply_bulk_sysmon_profile"
-            class="btn btn-primary"
-            disabled={is_nil(@selected_profile_id)}
-          >
-            Assign Profile
-          </button>
-        </div>
-      </div>
-      <form method="dialog" class="modal-backdrop">
-        <button phx-click="close_bulk_sysmon_modal">close</button>
       </form>
     </dialog>
     """
@@ -1476,123 +1343,21 @@

In the bulk sysmon modal, replace the incorrect field profile.interval_seconds
with the correct field profile.sample_interval to display the profile's interval
and prevent a runtime error.

web-ng/lib/serviceradar_web_ng_web/live/device_live/index.ex [771-777]

 <div class="text-xs text-base-content/60">
-  Interval: {profile.interval_seconds}s
+  Interval: {profile.sample_interval}
   <%= if profile.is_default do %>
     <span class="badge badge-ghost badge-xs ml-2">Default</span>
   <% end %>
 </div>

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies the use of a non-existent field (interval_seconds) which would cause a runtime error, and provides the correct field name (sample_interval).

Medium
Enforce a single default profile

Implement a custom Ash change to enforce a single default sysmon profile per
tenant by unsetting the current default before setting a new one.

elixir/serviceradar_core/lib/serviceradar/sysmon_profiles/sysmon_profile.ex [130-139]

+# In a new file: elixir/serviceradar_core/lib/serviceradar/sysmon_profiles/changes/set_as_default.ex
+defmodule ServiceRadar.SysmonProfiles.Changes.SetAsDefault do
+  use Ash.Resource.Change
+
+  def change(changeset, _, context) do
+    # Unset other default profiles for the tenant
+    ServiceRadar.SysmonProfiles.SysmonProfile
+    |> Ash.Query.filter(is_default == true)
+    |> Ash.update_all([set: [is_default: false]],
+      tenant: Ash.Changeset.get_tenant(changeset),
+      actor: context.actor,
+      authorize?: false
+    )
+
+    # Set the current profile as the default
+    Ash.Changeset.change_attribute(changeset, :is_default, true)
+  end
+end
+
+# In elixir/serviceradar_core/lib/serviceradar/sysmon_profiles/sysmon_profile.ex
 update :set_as_default do
   description "Set this profile as the default for the tenant"
   accept []
   require_atomic? false
 
-  change fn changeset, _context ->
-    # This will be handled by a custom change that unsets other defaults
-    Ash.Changeset.change_attribute(changeset, :is_default, true)
-  end
+  change ServiceRadar.SysmonProfiles.Changes.SetAsDefault
 end
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a data integrity issue where multiple default profiles could exist for a tenant and provides a robust solution using a custom Ash.Resource.Change, which aligns with the developer's intent noted in the code comments.

Medium
Ensure agent-device linking on registration

Update the guard clause in link_agent_to_device/4 to handle cases where the
agent's device_uid is nil, ensuring the agent-device link is created on first
registration.

elixir/serviceradar_core/lib/serviceradar/edge/agent_gateway_sync.ex [322-346]

 defp link_agent_to_device(agent_id, device_uid, tenant_schema, actor) do
   case Agent.get_by_uid(agent_id, tenant: tenant_schema, actor: actor) do
-    {:ok, %Agent{device_uid: existing_uid} = agent} when existing_uid != device_uid ->
+    {:ok, %Agent{device_uid: existing_uid} = agent} when is_nil(existing_uid) or existing_uid != device_uid ->
       agent
       |> Ash.Changeset.for_update(:gateway_sync, %{device_uid: device_uid})
       |> Ash.update(tenant: tenant_schema, actor: actor)
       |> case do
         {:ok, _} ->
           Logger.debug("Linked agent #{agent_id} to device #{device_uid}")
           :ok
 
         {:error, reason} ->
           Logger.warning("Failed to link agent #{agent_id} to device: #{inspect(reason)}")
           :ok
       end
 
     {:ok, _agent} ->
       # Already linked or same device
       :ok
 
     {:error, _} ->
       # Agent not found yet, will be linked on next update
       :ok
   end
 end
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: This suggestion identifies a subtle but critical bug where an agent would not be linked to a device on its initial registration because the guard clause doesn't account for a nil device_uid. The fix is correct and essential for the feature's functionality.

Medium
Prevent race condition during assignment
Suggestion Impact:The commit did not implement the proposed upsert/update-or-create logic. Instead, it removed the direct device assignment functionality entirely (deleted the handle_event that performed assignments and removed load_device_assignment/assign_sysmon_profile_to_device/remove_device_sysmon_assignment plus the SysmonProfileAssignment alias). This eliminates the original delete+create race window by removing that code path rather than refactoring it.

code diff:

@@ -154,34 +154,6 @@
      push_patch(socket,
        to: page_path <> "?" <> URI.encode_query(%{"q" => query, "limit" => socket.assigns.limit})
      )}
-  end
-
-  def handle_event("assign_sysmon_profile", %{"profile_id" => profile_id}, socket) do
-    scope = socket.assigns.current_scope
-    device_uid = socket.assigns.device_uid
-
-    result =
-      if profile_id == "" do
-        # Remove direct assignment
-        remove_device_sysmon_assignment(scope, device_uid)
-      else
-        # Create or update direct assignment
-        assign_sysmon_profile_to_device(scope, device_uid, profile_id)
-      end
-
-    case result do
-      :ok ->
-        {sysmon_profile_info, available_profiles} = load_sysmon_profile_info(scope, device_uid)
-
-        {:noreply,
-         socket
-         |> assign(:sysmon_profile_info, sysmon_profile_info)
-         |> assign(:available_profiles, available_profiles)
-         |> put_flash(:info, "Sysmon profile updated")}
-
-      {:error, reason} ->
-        {:noreply, put_flash(socket, :error, "Failed to update profile: #{inspect(reason)}")}
-    end
   end
 
   def handle_event(_event, _params, socket), do: {:noreply, socket}
@@ -1953,13 +1925,11 @@
   def sysmon_config_section(assigns) do
     profile = Map.get(assigns.profile_info, :profile)
     source = Map.get(assigns.profile_info, :source, "default")
-    direct_assignment = Map.get(assigns.profile_info, :direct_assignment)
 
     assigns =
       assigns
       |> assign(:profile, profile)
       |> assign(:source, source)
-      |> assign(:direct_assignment, direct_assignment)
 
     ~H"""
     <div class="rounded-xl border border-base-200 bg-base-100 shadow-sm">
@@ -1974,59 +1944,42 @@
       </div>
 
       <div class="p-4">
-        <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
-          <!-- Current Profile Info -->
-          <div class="space-y-3">
-            <div class="text-xs font-semibold uppercase tracking-wide text-base-content/60">
-              Effective Profile
+        <div class="space-y-3">
+          <div class="text-xs font-semibold uppercase tracking-wide text-base-content/60">
+            Effective Profile
+          </div>
+          <div :if={@profile} class="space-y-2">
+            <div class="flex items-center gap-2">
+              <span class="font-medium">{@profile.name}</span>
+              <.ui_badge :if={@profile.is_default} variant="info" size="xs">Default</.ui_badge>
             </div>
-            <div :if={@profile} class="space-y-2">
-              <div class="flex items-center gap-2">
-                <span class="font-medium">{@profile.name}</span>
-                <.ui_badge :if={@profile.is_default} variant="info" size="xs">Default</.ui_badge>
-              </div>
-              <div class="text-xs text-base-content/60">
-                Interval: <span class="font-mono">{@profile.sample_interval}</span>
-              </div>
-              <div class="flex flex-wrap gap-1 mt-1">
-                <.ui_badge :if={@profile.collect_cpu} variant="ghost" size="xs">CPU</.ui_badge>
-                <.ui_badge :if={@profile.collect_memory} variant="ghost" size="xs">Memory</.ui_badge>
-                <.ui_badge :if={@profile.collect_disk} variant="ghost" size="xs">Disk</.ui_badge>
-                <.ui_badge :if={@profile.collect_network} variant="ghost" size="xs">Network</.ui_badge>
-                <.ui_badge :if={@profile.collect_processes} variant="ghost" size="xs">Processes</.ui_badge>
-              </div>
+            <div class="text-xs text-base-content/60">
+              Interval: <span class="font-mono">{@profile.sample_interval}</span>
             </div>
-            <div :if={is_nil(@profile)} class="text-sm text-base-content/60">
-              Using default configuration
+            <div :if={@profile.target_query && @source == "srql"} class="text-xs text-base-content/60">
+              Matched by:
+              <code class="font-mono bg-base-200/50 px-1 rounded">{@profile.target_query}</code>
+            </div>
+            <div class="flex flex-wrap gap-1 mt-1">
+              <.ui_badge :if={@profile.collect_cpu} variant="ghost" size="xs">CPU</.ui_badge>
+              <.ui_badge :if={@profile.collect_memory} variant="ghost" size="xs">Memory</.ui_badge>
+              <.ui_badge :if={@profile.collect_disk} variant="ghost" size="xs">Disk</.ui_badge>
+              <.ui_badge :if={@profile.collect_network} variant="ghost" size="xs">
+                Network
+              </.ui_badge>
+              <.ui_badge :if={@profile.collect_processes} variant="ghost" size="xs">
+                Processes
+              </.ui_badge>
             </div>
           </div>
-
-          <!-- Assignment Controls -->
-          <div class="space-y-3">
-            <div class="text-xs font-semibold uppercase tracking-wide text-base-content/60">
-              Direct Assignment
-            </div>
-            <form phx-change="assign_sysmon_profile" class="flex items-center gap-2">
-              <select
-                name="profile_id"
-                class="select select-bordered select-sm flex-1"
-              >
-                <option value="" selected={is_nil(@direct_assignment)}>
-                  Use tag/default profile
-                </option>
-                <%= for p <- @available_profiles do %>
-                  <option
-                    value={p.id}
-                    selected={@direct_assignment && @direct_assignment.profile_id == p.id}
-                  >
-                    {p.name}
-                  </option>
-                <% end %>
-              </select>
-            </form>
-            <div class="text-xs text-base-content/50">
-              Direct assignments override tag-based and default profiles.
-            </div>
+          <div :if={is_nil(@profile)} class="text-sm text-base-content/60">
+            Using default configuration
+          </div>
+          <div class="text-xs text-base-content/50 pt-2">
+            Profile targeting is configured via SRQL queries in <.link
+              navigate="/settings/sysmon"
+              class="link link-primary"
+            >Settings</.link>.
           </div>
         </div>
       </div>
@@ -2039,11 +1992,10 @@
   defp source_badge(assigns) do
     {label, variant} =
       case assigns.source do
-        "device" -> {"Direct", "primary"}
-        "tag" -> {"Tag", "secondary"}
+        "srql" -> {"SRQL Targeting", "primary"}
         "default" -> {"Default", "ghost"}
         "local" -> {"Local Override", "warning"}
-        _ -> {"Unknown", "ghost"}
+        _ -> {"Default", "ghost"}
       end
 
     assigns =
@@ -2073,28 +2025,24 @@
       tenant_schema = ServiceRadarWebNGWeb.TenantResolver.schema_for_tenant_id(tenant_id)
       actor = get_sweep_actor(scope)
 
-      # Load available profiles
+      # Load available profiles (for reference)
       available_profiles = load_available_profiles(tenant_schema, actor)
 
-      # Check for direct device assignment
-      direct_assignment = load_device_assignment(tenant_schema, device_uid, actor)
-
-      # Resolve the effective profile
+      # Resolve the effective profile via SRQL targeting
       profile = SysmonCompiler.resolve_profile(tenant_schema, device_uid, nil, actor)
 
+      # Determine source based on profile type
       source =
         cond do
-          direct_assignment -> "device"
-          # Could check for tag match here, but for simplicity we'll show "default" for non-direct
-          profile && profile.is_default -> "default"
-          profile -> "tag"
+          is_nil(profile) -> "default"
+          profile.is_default -> "default"
+          not is_nil(profile.target_query) -> "srql"
           true -> "default"
         end
 
       profile_info = %{
         profile: profile,
-        source: source,
-        direct_assignment: direct_assignment
+        source: source
       }
 
       {profile_info, available_profiles}
@@ -2112,57 +2060,4 @@
       {:error, _} -> []
     end
   end
-
-  defp load_device_assignment(tenant_schema, device_uid, actor) do
-    query =
-      SysmonProfileAssignment
-      |> Ash.Query.for_read(:for_device, %{device_uid: device_uid}, actor: actor, tenant: tenant_schema)
-
-    case Ash.read_one(query, actor: actor) do
-      {:ok, assignment} -> assignment
-      {:error, _} -> nil
-    end
-  end
-
-  defp assign_sysmon_profile_to_device(scope, device_uid, profile_id) do
-    tenant_id = Scope.tenant_id(scope)
-    tenant_schema = ServiceRadarWebNGWeb.TenantResolver.schema_for_tenant_id(tenant_id)
-    actor = get_sweep_actor(scope)
-
-    # First, remove any existing direct assignment
-    case load_device_assignment(tenant_schema, device_uid, actor) do
-      nil -> :ok
-      existing -> Ash.destroy(existing, actor: actor, tenant: tenant_schema)
-    end
-
-    # Create new assignment
-    attrs = %{
-      profile_id: profile_id,
-      assignment_type: :device,
-      device_uid: device_uid,
-      priority: 100
-    }
-
-    changeset = Ash.Changeset.for_create(SysmonProfileAssignment, :create, attrs)
-
-    case Ash.create(changeset, actor: actor, tenant: tenant_schema) do
-      {:ok, _} -> :ok
-      {:error, reason} -> {:error, reason}
-    end
-  end
-
-  defp remove_device_sysmon_assignment(scope, device_uid) do
-    tenant_id = Scope.tenant_id(scope)
-    tenant_schema = ServiceRadarWebNGWeb.TenantResolver.schema_for_tenant_id(tenant_id)
-    actor = get_sweep_actor(scope)
-
-    case load_device_assignment(tenant_schema, device_uid, actor) do
-      nil -> :ok
-      assignment ->
-        case Ash.destroy(assignment, actor: actor, tenant: tenant_schema) do
-          :ok -> :ok
-          {:error, reason} -> {:error, reason}
-        end
-    end
-  end
 end

Refactor assign_sysmon_profile_to_device/3 to perform an atomic upsert
operation. Instead of deleting and creating, update the existing assignment if
it exists, or create a new one if it does not, to prevent a potential race
condition.

web-ng/lib/serviceradar_web_ng_web/live/device_live/show.ex [2127-2152]

 defp assign_sysmon_profile_to_device(scope, device_uid, profile_id) do
   tenant_id = Scope.tenant_id(scope)
   tenant_schema = ServiceRadarWebNGWeb.TenantResolver.schema_for_tenant_id(tenant_id)
   actor = get_sweep_actor(scope)
 
-  # First, remove any existing direct assignment
   case load_device_assignment(tenant_schema, device_uid, actor) do
-    nil -> :ok
-    existing -> Ash.destroy(existing, actor: actor, tenant: tenant_schema)
-  end
+    nil ->
+      # Create new assignment if none exists
+      attrs = %{
+        profile_id: profile_id,
+        assignment_type: :device,
+        device_uid: device_uid,
+        priority: 100
+      }
+      changeset = Ash.Changeset.for_create(SysmonProfileAssignment, :create, attrs)
+      case Ash.create(changeset, actor: actor, tenant: tenant_schema) do
+        {:ok, _} -> :ok
+        {:error, reason} -> {:error, reason}
+      end
 
-  # Create new assignment
-  attrs = %{
-    profile_id: profile_id,
-    assignment_type: :device,
-    device_uid: device_uid,
-    priority: 100
-  }
-
-  changeset = Ash.Changeset.for_create(SysmonProfileAssignment, :create, attrs)
-
-  case Ash.create(changeset, actor: actor, tenant: tenant_schema) do
-    {:ok, _} -> :ok
-    {:error, reason} -> {:error, reason}
+    existing ->
+      # Update existing assignment
+      changeset = Ash.Changeset.for_update(existing, :update, %{profile_id: profile_id})
+      case Ash.update(changeset, actor: actor, tenant: tenant_schema) do
+        {:ok, _} -> :ok
+        {:error, reason} -> {:error, reason}
+      end
   end
 end

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a potential race condition and proposes a more robust upsert-style logic, which improves the atomicity and reliability of the operation.

Medium
Avoid silently ignoring invalid filters

Remove the rescue block in apply_filter/2 to prevent silently ignoring invalid
SRQL filters and to allow proper error propagation and logging.

elixir/serviceradar_core/lib/serviceradar/sysmon_profiles/srql_target_resolver.ex [177-191]

 defp apply_filter(query, %{field: field, op: op, value: value}) when is_binary(field) do
   # Map common SRQL field names to Device attributes
   mapped_field = map_field(field)
 
   # Handle special cases like tags
   if String.starts_with?(field, "tags.") do
     # tags.key:value -> filter on tags JSONB
     tag_key = String.replace_prefix(field, "tags.", "")
     apply_tag_filter(query, tag_key, value)
   else
     apply_standard_filter(query, mapped_field, op, value)
   end
-rescue
-  _ -> query
 end
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly points out that silently swallowing errors is poor practice and can lead to incorrect behavior. Removing the rescue block improves error handling by leveraging the existing error logging in the calling function, making debugging easier.

Medium
Allow disabling sysmon via remote config

Update applySysmonConfig to reconfigure the sysmon service even when
protoConfig.Enabled is false, allowing the service to be disabled remotely.

pkg/agent/push_loop.go [1028-1071]

 // applySysmonConfig applies sysmon configuration from the gateway to the embedded sysmon service.
 func (p *PushLoop) applySysmonConfig(protoConfig *proto.SysmonConfig) {
 	p.server.mu.RLock()
 	sysmonSvc := p.server.sysmonService
 	p.server.mu.RUnlock()
 
 	if sysmonSvc == nil {
 		p.logger.Debug().Msg("Sysmon service not initialized, skipping config apply")
 		return
 	}
 
 	// Convert proto config to sysmon.Config
 	cfg := protoToSysmonConfig(protoConfig)
 
-	// Check if this is a meaningful configuration change
-	if !protoConfig.Enabled {
-		p.logger.Info().Msg("Sysmon disabled in gateway config")
-		// Don't stop the service here - let admin do that explicitly
-		return
-	}
-
 	// Parse and apply the configuration
 	parsed, err := cfg.Parse()
 	if err != nil {
 		p.logger.Error().Err(err).Msg("Failed to parse sysmon config from gateway")
 		return
 	}
 
 	if err := sysmonSvc.Reconfigure(parsed); err != nil {
 		p.logger.Error().Err(err).Msg("Failed to apply sysmon config from gateway")
+		return
+	}
+
+	// Check if this is a meaningful configuration change
+	if !protoConfig.Enabled {
+		p.logger.Info().Msg("Sysmon disabled via gateway config")
+		// The service is now reconfigured to be disabled, but not stopped.
+		// The collector loop will continue but collections will do nothing.
 		return
 	}
 
 	p.logger.Info().
 		Str("profile_id", protoConfig.ProfileId).
 		Str("profile_name", protoConfig.ProfileName).
 		Str("config_source", protoConfig.ConfigSource).
 		Str("sample_interval", cfg.SampleInterval).
 		Bool("cpu", cfg.CollectCPU).
 		Bool("memory", cfg.CollectMemory).
 		Bool("disk", cfg.CollectDisk).
 		Bool("network", cfg.CollectNetwork).
 		Bool("processes", cfg.CollectProcesses).
 		Msg("Applied sysmon config from gateway")
 }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly points out that the service cannot be disabled via remote configuration, which is a logical flaw in the implementation. Fixing this improves the remote management capability of the feature.

Medium
General
Allow modal close clicks
Suggestion Impact:The PR did not implement the suggested wrapper removals. Instead, it removed the entire bulk sysmon modal UI and its related LiveView event handlers, which eliminates the problematic usage by deleting the feature/component altogether.

code diff:

@@ -233,52 +230,6 @@
            socket
            |> assign(:bulk_edit_form, to_form(params, as: :bulk))
            |> put_flash(:error, "Failed to apply tags: #{reason}")}
-      end
-    end
-  end
-
-  # Bulk sysmon profile assignment handlers
-  def handle_event("open_bulk_sysmon_modal", _params, socket) do
-    scope = socket.assigns.current_scope
-    profiles = load_sysmon_profiles(scope)
-
-    {:noreply,
-     socket
-     |> assign(:show_bulk_sysmon_modal, true)
-     |> assign(:available_sysmon_profiles, profiles)
-     |> assign(:bulk_sysmon_profile_id, nil)}
-  end
-
-  def handle_event("close_bulk_sysmon_modal", _params, socket) do
-    {:noreply,
-     socket
-     |> assign(:show_bulk_sysmon_modal, false)
-     |> assign(:bulk_sysmon_profile_id, nil)}
-  end
-
-  def handle_event("select_bulk_sysmon_profile", %{"profile_id" => profile_id}, socket) do
-    {:noreply, assign(socket, :bulk_sysmon_profile_id, profile_id)}
-  end
-
-  def handle_event("apply_bulk_sysmon_profile", _params, socket) do
-    profile_id = socket.assigns.bulk_sysmon_profile_id
-
-    if is_nil(profile_id) or profile_id == "" do
-      {:noreply, put_flash(socket, :error, "Please select a profile")}
-    else
-      case apply_sysmon_profile_to_devices(socket, profile_id) do
-        {:ok, count} ->
-          {:noreply,
-           socket
-           |> assign(:show_bulk_sysmon_modal, false)
-           |> assign(:bulk_sysmon_profile_id, nil)
-           |> assign(:selected_devices, MapSet.new())
-           |> assign(:select_all_matching, false)
-           |> assign(:total_matching_count, nil)
-           |> put_flash(:info, "Assigned sysmon profile to #{count} device(s)")}
-
-        {:error, reason} ->
-          {:noreply, put_flash(socket, :error, "Failed to assign profile: #{reason}")}
       end
     end
   end
@@ -490,9 +441,6 @@
             </button>
           </div>
           <div class="flex items-center gap-2">
-            <.ui_button variant="ghost" size="sm" phx-click="open_bulk_sysmon_modal">
-              <.icon name="hero-cpu-chip" class="size-4" /> Assign Profile
-            </.ui_button>
             <.ui_button variant="primary" size="sm" phx-click="open_bulk_edit_modal">
               <.icon name="hero-tag" class="size-4" /> Bulk Edit
             </.ui_button>
@@ -575,7 +523,9 @@
                   <% has_sysmon =
                     is_binary(device_uid) and Map.get(@sysmon_presence, device_uid, false) == true %>
                   <% sysmon_profile =
-                    if is_binary(device_uid), do: Map.get(@sysmon_profiles_by_device, device_uid), else: nil %>
+                    if is_binary(device_uid),
+                      do: Map.get(@sysmon_profiles_by_device, device_uid),
+                      else: nil %>
                   <tr class={"hover:bg-base-200/40 #{if is_selected, do: "bg-primary/5", else: ""}"}>
                     <td class="text-center">
                       <input
@@ -660,13 +610,6 @@
         form={@bulk_edit_form}
         selected_count={@effective_count}
       />
-      <!-- Bulk Sysmon Profile Assignment Modal -->
-      <.bulk_sysmon_modal
-        :if={@show_bulk_sysmon_modal}
-        profiles={@available_sysmon_profiles}
-        selected_profile_id={@bulk_sysmon_profile_id}
-        selected_count={@effective_count}
-      />
     </Layouts.app>
     """
   end
@@ -720,82 +663,6 @@
       </div>
       <form method="dialog" class="modal-backdrop">
         <button phx-click="close_bulk_edit_modal">close</button>
-      </form>
-    </dialog>
-    """
-  end
-
-  # Bulk Sysmon Profile Modal Component
-  attr :profiles, :list, required: true
-  attr :selected_profile_id, :string, default: nil
-  attr :selected_count, :integer, required: true
-
-  defp bulk_sysmon_modal(assigns) do
-    ~H"""
-    <dialog id="bulk_sysmon_modal" class="modal modal-open">
-      <div class="modal-box max-w-lg">
-        <form method="dialog">
-          <button
-            class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
-            phx-click="close_bulk_sysmon_modal"
-          >
-            x
-          </button>
-        </form>
-
-        <h3 class="text-lg font-bold">Assign Sysmon Profile</h3>
-        <p class="py-2 text-sm text-base-content/70">
-          Assign a sysmon monitoring profile to {@selected_count} selected device(s).
-        </p>
-
-        <div class="form-control w-full">
-          <label class="label">
-            <span class="label-text font-medium">Select Profile</span>
-          </label>
-          <div class="space-y-2">
-            <%= for profile <- @profiles do %>
-              <label class={[
-                "flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-colors",
-                if(@selected_profile_id == profile.id, do: "border-primary bg-primary/10", else: "border-base-200 hover:bg-base-100")
-              ]}>
-                <input
-                  type="radio"
-                  name="sysmon_profile"
-                  class="radio radio-primary radio-sm"
-                  checked={@selected_profile_id == profile.id}
-                  phx-click="select_bulk_sysmon_profile"
-                  phx-value-profile_id={profile.id}
-                />
-                <div class="flex-1">
-                  <div class="font-medium text-sm">{profile.name}</div>
-                  <div class="text-xs text-base-content/60">
-                    Interval: {profile.interval_seconds}s
-                    <%= if profile.is_default do %>
-                      <span class="badge badge-ghost badge-xs ml-2">Default</span>
-                    <% end %>
-                  </div>
-                </div>
-              </label>
-            <% end %>
-          </div>
-        </div>
-
-        <div class="flex justify-end gap-2 pt-4">
-          <button type="button" phx-click="close_bulk_sysmon_modal" class="btn btn-ghost">
-            Cancel
-          </button>
-          <button
-            type="button"
-            phx-click="apply_bulk_sysmon_profile"
-            class="btn btn-primary"
-            disabled={is_nil(@selected_profile_id)}
-          >
-            Assign Profile
-          </button>
-        </div>
-      </div>
-      <form method="dialog" class="modal-backdrop">
-        <button phx-click="close_bulk_sysmon_modal">close</button>
       </form>
     </dialog>
     """

Remove the wrappers from the modal's close button and
backdrop to ensure their phx-click="close_bulk_sysmon_modal" events are handled
by LiveView.

web-ng/lib/serviceradar_web_ng_web/live/device_live/index.ex [735-800]

 <dialog id="bulk_sysmon_modal" class="modal modal-open">
   <div class="modal-box max-w-lg">
-    <form method="dialog">
-      <button
-        class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
-        phx-click="close_bulk_sysmon_modal"
-      >
-        x
-      </button>
-    </form>
+    <button
+      class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
+      phx-click="close_bulk_sysmon_modal"
+      type="button"
+    >
+      x
+    </button>
     ...
   </div>
-  <form method="dialog" class="modal-backdrop">
-    <button phx-click="close_bulk_sysmon_modal">close</button>
-  </form>
+  <div class="modal-backdrop" phx-click="close_bulk_sysmon_modal"></div>
 </dialog>

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that wrapping buttons with phx-click inside a <form method="dialog"> will break the LiveView event handling, and the proposed fix is correct.

Medium
Prioritize custom config directory first

**Modify loadCon...

Imported GitHub PR comment. Original author: @qodo-code-review[bot] Original URL: https://github.com/carverauto/serviceradar/pull/2279#issuecomment-3747419811 Original created: 2026-01-14T02:36:41Z --- ## PR Code Suggestions ✨ <!-- 7ab36f9 --> Explore these optional code suggestions: <table><thead><tr><td><strong>Category</strong></td><td align=left><strong>Suggestion&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </strong></td><td align=center><strong>Impact</strong></td></tr><tbody><tr><td rowspan=1>High-level</td> <td> <details><summary>✅ <s>Consolidate sysmon profile targeting mechanisms</s></summary> ___ <details><summary><b>Suggestion Impact:</b></summary>The compiler was simplified to resolve profiles only via SRQL targeting (then default), removed all device/tag assignment resolution logic and references, updated documentation/comments accordingly, and deleted the SysmonProfileAssignment resource file (effectively consolidating targeting into SRQL). code diff: ```diff - Transforms SysmonProfile and SysmonProfileAssignment Ash resources into - agent-consumable sysmon configuration format. + Transforms SysmonProfile Ash resources into agent-consumable sysmon + configuration format using SRQL-based targeting. ## Resolution Order When resolving which profile applies to a device: - 1. Device-specific assignment (legacy, for backwards compatibility) - 2. SRQL targeting profiles (ordered by priority, highest first) - 3. Tag-based assignments (legacy, for backwards compatibility) - 4. Default tenant profile (fallback) + 1. SRQL targeting profiles (ordered by priority, highest first) + 2. Default tenant profile (fallback) - SRQL targeting is the preferred method. Device and tag assignments are - maintained for backwards compatibility during the migration period. + Profiles use `target_query` (SRQL) to define which devices they apply to. + Example: `target_query: "in:devices tags.role:database"` matches all devices + with the tag `role=database`. ## Output Format @@ -46,16 +45,15 @@ alias ServiceRadar.Actors.SystemActor alias ServiceRadar.Cluster.TenantSchemas - alias ServiceRadar.Inventory.Device alias ServiceRadar.SysmonProfiles.SrqlTargetResolver - alias ServiceRadar.SysmonProfiles.{SysmonProfile, SysmonProfileAssignment} + alias ServiceRadar.SysmonProfiles.SysmonProfile @impl true def config_type, do: :sysmon @impl true def source_resources do - [SysmonProfile, SysmonProfileAssignment] + [SysmonProfile] end @impl true @@ -68,7 +66,7 @@ profile = resolve_profile(tenant_schema, device_uid, agent_id, actor) if profile do - config = compile_profile(profile, tenant_schema, actor) + config = compile_profile(profile) {:ok, config} else # Return default config if no profile found @@ -95,28 +93,18 @@ end @doc """ - Resolves the sysmon profile for a device. + Resolves the sysmon profile for a device using SRQL targeting. Resolution order: - 1. Device-specific assignment (legacy, for backwards compatibility) - 2. SRQL targeting profiles (ordered by priority, highest first) - 3. Tag-based assignment (legacy, for backwards compatibility) - 4. Default profile for tenant + 1. SRQL targeting profiles (ordered by priority, highest first) + 2. Default profile for tenant - Returns `{profile, config_source}` tuple where config_source indicates - how the profile was resolved ("device", "srql", "tag", or "default"). + Returns the matching SysmonProfile or nil if no profile matches. """ @spec resolve_profile(String.t(), String.t() | nil, String.t() | nil, map()) :: SysmonProfile.t() | nil def resolve_profile(tenant_schema, device_uid, _agent_id, actor) do - # Resolution order: - # 1. Device-specific assignment (legacy) - # 2. SRQL targeting profiles - # 3. Tag-based assignment (legacy) - # 4. Default profile - try_device_assignment(tenant_schema, device_uid, actor) || - try_srql_targeting(tenant_schema, device_uid, actor) || - try_tag_assignment(tenant_schema, device_uid, actor) || + try_srql_targeting(tenant_schema, device_uid, actor) || get_default_profile(tenant_schema, actor) end @@ -125,7 +113,9 @@ defp try_srql_targeting(tenant_schema, device_uid, actor) do case SrqlTargetResolver.resolve_for_device(tenant_schema, device_uid, actor) do - {:ok, profile} -> profile + {:ok, profile} -> + profile + {:error, reason} -> Logger.warning("SysmonCompiler: SRQL targeting failed - #{inspect(reason)}") nil @@ -135,8 +125,15 @@ @doc """ Compiles a profile to the agent config format. """ - @spec compile_profile(SysmonProfile.t(), String.t(), map()) :: map() - def compile_profile(profile, _tenant_schema, _actor, config_source \\ "profile") do + @spec compile_profile(SysmonProfile.t()) :: map() + def compile_profile(profile) do + config_source = + cond do + profile.is_default -> "default" + not is_nil(profile.target_query) -> "srql" + true -> "profile" + end + %{ "enabled" => profile.enabled, "sample_interval" => profile.sample_interval, @@ -174,98 +171,7 @@ } end - # Private helpers - - defp try_device_assignment(_tenant_schema, nil, _actor), do: nil - - defp try_device_assignment(tenant_schema, device_uid, actor) do - query = - SysmonProfileAssignment - |> Ash.Query.for_read(:for_device, %{device_uid: device_uid}, - actor: actor, - tenant: tenant_schema - ) - |> Ash.Query.load(:profile) - - case Ash.read_one(query, actor: actor) do - {:ok, nil} -> - nil - - {:ok, assignment} -> - assignment.profile - - {:error, reason} -> - Logger.warning("SysmonCompiler: failed to load device assignment - #{inspect(reason)}") - nil - end - end - - defp try_tag_assignment(_tenant_schema, nil, _actor), do: nil - - defp try_tag_assignment(tenant_schema, device_uid, actor) do - # Load device to get its tags - device = load_device(tenant_schema, device_uid, actor) - - if device && is_map(device.tags) && map_size(device.tags) > 0 do - # Get all tag assignments and find the highest priority match - assignments = load_tag_assignments(tenant_schema, actor) - - matching_assignment = - assignments - |> Enum.filter(&tag_matches_device?(&1, device.tags)) - |> Enum.sort_by(& &1.priority, :desc) - |> List.first() - - if matching_assignment do - matching_assignment.profile - else - nil - end - else - nil - end - end - - defp load_device(tenant_schema, device_uid, actor) do - query = - Device - |> Ash.Query.for_read(:by_uid, %{uid: device_uid}, actor: actor, tenant: tenant_schema) - - case Ash.read_one(query, actor: actor) do - {:ok, device} -> device - {:error, _} -> nil - end - end - - defp load_tag_assignments(tenant_schema, actor) do - query = - SysmonProfileAssignment - |> Ash.Query.for_read(:read, %{}, actor: actor, tenant: tenant_schema) - |> Ash.Query.filter(assignment_type == :tag) - |> Ash.Query.load(:profile) - - case Ash.read(query, actor: actor) do - {:ok, assignments} -> assignments - {:error, _} -> [] - end - end - - defp tag_matches_device?(assignment, device_tags) do - tag_key = assignment.tag_key - tag_value = assignment.tag_value - - device_value = Map.get(device_tags, tag_key) - - cond do - # Device doesn't have this tag - is_nil(device_value) -> false - # Assignment matches any value for this key - is_nil(tag_value) -> true - # Assignment matches specific value - true -> device_value == tag_value - end - end - + # Get the default profile for the tenant defp get_default_profile(tenant_schema, actor) do query = SysmonProfile # File: elixir/serviceradar_core/lib/serviceradar/sysmon_profiles/sysmon_profile_assignment.ex ``` </details> ___ **The current implementation uses two methods for sysmon profile targeting: SRQL <br>queries and separate assignment resources. This should be consolidated into a <br>single, SRQL-based system to reduce complexity.** ### Examples: <details> <summary> <a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-0112a6c9ad8e3f4e689216d15a910efca1bf1a9c1ae89f5dc992ee685c417b51R109-R121">elixir/serviceradar_core/lib/serviceradar/agent_config/compilers/sysmon_compiler.ex [109-121]</a> </summary> ```elixir @spec resolve_profile(String.t(), String.t() | nil, String.t() | nil, map()) :: SysmonProfile.t() | nil def resolve_profile(tenant_schema, device_uid, _agent_id, actor) do # Resolution order: # 1. Device-specific assignment (legacy) # 2. SRQL targeting profiles # 3. Tag-based assignment (legacy) # 4. Default profile try_device_assignment(tenant_schema, device_uid, actor) || try_srql_targeting(tenant_schema, device_uid, actor) || ... (clipped 3 lines) ``` </details> <details> <summary> <a href="https://github.com/carverauto/serviceradar/pull/2279/files#diff-a2f8d3c7fdc5e0a92286b1aebf95371402f0715db4a8af31d49a04035207243bR1-R233">elixir/serviceradar_core/lib/serviceradar/sysmon_profiles/sysmon_profile_assignment.ex [1-233]</a> </summary> ```elixir defmodule ServiceRadar.SysmonProfiles.SysmonProfileAssignment do @moduledoc """ Assignment of sysmon profiles to devices or tags. SysmonProfileAssignment enables flexible profile targeting: - Device-specific assignments: Directly assign a profile to a specific device - Tag-based assignments: Assign a profile to all devices matching a tag ## Assignment Types ... (clipped 223 lines) ``` </details> ### Solution Walkthrough: #### Before: ```elixir # Two separate resources for targeting resource SysmonProfile do # ... attribute :target_query, :string # SRQL-based targeting # ... end resource SysmonProfileAssignment do # ... attribute :assignment_type, :atom, one_of: [:device, :tag] # Direct/tag assignment attribute :device_uid, :string attribute :tag_key, :string # ... end # Complex resolution logic in the compiler def resolve_profile(device_uid) do try_device_assignment(device_uid) || # Checks SysmonProfileAssignment try_srql_targeting(device_uid) || # Checks SysmonProfile.target_query try_tag_assignment(device_uid) || # Checks SysmonProfileAssignment get_default_profile() end ``` #### After: ```elixir # A single resource for profiles and targeting resource SysmonProfile do # ... attribute :target_query, :string # All targeting is done via SRQL # ... end # The SysmonProfileAssignment resource is removed. # Simplified resolution logic in the compiler def resolve_profile(device_uid) do # UI for direct assignment would generate a query like "uid:<id>" try_srql_targeting(device_uid) || # Checks SysmonProfile.target_query get_default_profile() end ``` <details><summary>Suggestion importance[1-10]: 9</summary> __ Why: The suggestion correctly identifies a significant design issue where two parallel sysmon profile targeting mechanisms (SRQL and direct assignments) are introduced, leading to increased complexity in the backend resolution logic and a confusing user experience. </details></details></td><td align=center>High </td></tr><tr><td rowspan=9>Possible issue</td> <td> <details><summary>Prevent race condition in stop</summary> ___ **Modify the <code>Stop</code> function to hold the mutex lock for the entire critical section <br>to prevent a race condition that could cause a panic.** [pkg/agent/sysmon_service.go [195-233]](https://github.com/carverauto/serviceradar/pull/2279/files#diff-8c00578b299aa029ef81f77ebe8959d20b7122299ee24e525c345f7c91c28d48R195-R233) ```diff // Stop halts the sysmon collector and config refresh loop. func (s *SysmonService) Stop(ctx context.Context) error { s.mu.Lock() if !s.started { s.mu.Unlock() return nil } // Stop the config refresh loop if s.stopRefresh != nil { close(s.stopRefresh) } + + // Stop the collector while holding the lock + if s.collector != nil { + if err := s.collector.Stop(); err != nil { + s.mu.Unlock() + return fmt.Errorf("failed to stop sysmon collector: %w", err) + } + } + + s.started = false s.mu.Unlock() // Wait for refresh loop to finish (with timeout from context) if s.refreshDone != nil { select { case <-s.refreshDone: // Refresh loop stopped case <-ctx.Done(): s.logger.Warn().Msg("Timeout waiting for config refresh loop to stop") case <-time.After(5 * time.Second): s.logger.Warn().Msg("Timeout waiting for config refresh loop to stop") } } - s.mu.Lock() - defer s.mu.Unlock() - - if s.collector != nil { - if err := s.collector.Stop(); err != nil { - return fmt.Errorf("failed to stop sysmon collector: %w", err) - } - } - - s.started = false s.logger.Info().Msg("Sysmon service stopped") return nil } ``` `[To ensure code accuracy, apply this suggestion manually]` <details><summary>Suggestion importance[1-10]: 9</summary> __ Why: The suggestion correctly identifies a race condition in the `Stop` function that could lead to a panic from a double-close on a channel. This is a critical concurrency bug fix. </details></details></td><td align=center>High </td></tr><tr><td> <details><summary>Fix incorrect device count preview<!-- not_implemented --></summary> ___ **Update the <code>apply_srql_filter/2</code> function to correctly handle all filter operators <br>(<code>contains</code>, <code>not_contains</code>, <code>equals</code>, <code>not_equals</code>) when calculating the target device <br>count preview, ensuring the count is accurate.** [web-ng/lib/serviceradar_web_ng_web/live/settings/sysmon_profiles_live/index.ex [940-964]](https://github.com/carverauto/serviceradar/pull/2279/files#diff-8849d05a7171d718f17babecdf46709110ad58d7dbf378ed29429f2a4d50a5c5R940-R964) ```diff -defp apply_srql_filter(query, %{field: field, op: _op, value: value}) when is_binary(field) do +defp apply_srql_filter(query, %{field: field, op: op, value: value}) when is_binary(field) do if String.starts_with?(field, "tags.") do tag_key = String.replace_prefix(field, "tags.", "") + # This part still only handles equality for tags. A more complex implementation + # would be needed for other operators on tags. For now, we keep it as is. Ash.Query.filter(query, fragment("tags @> ?", ^%{tag_key => value})) else # Map common fields mapped_field = case field do "hostname" -> :hostname "uid" -> :uid "type" -> :type_id "os" -> :os "status" -> :status _ -> nil end if mapped_field do - Ash.Query.filter_input(query, %{mapped_field => %{eq: value}}) + case op do + "contains" -> Ash.Query.filter_input(query, %{mapped_field => %{like: "%#{value}%"}}) + "not_contains" -> Ash.Query.filter_input(query, %{mapped_field => %{not: %{like: "%#{value}%"}}}) + "not_equals" -> Ash.Query.filter_input(query, %{mapped_field => %{not: %{eq: value}}}) + _ -> Ash.Query.filter_input(query, %{mapped_field => %{eq: value}}) + end else query end end rescue _ -> query end ``` `[To ensure code accuracy, apply this suggestion manually]` <details><summary>Suggestion importance[1-10]: 8</summary> __ Why: The suggestion correctly identifies a bug where the filter operator is ignored, leading to an inaccurate device count preview, and provides a correct fix. </details></details></td><td align=center>Medium </td></tr><tr><td> <details><summary>✅ <s>Prevent incorrect deletion of assignments<!-- not_implemented --></s></summary> ___ <details><summary><b>Suggestion Impact:</b></summary>The PR did not implement the suggested query filter or the change from Ash.destroy!/2 to Ash.destroy/2. Instead, it removed the entire bulk sysmon profile assignment workflow, including create_sysmon_assignments/3 and all related UI/event handlers, eliminating the deletion logic altogether (and therefore also eliminating the risk of incorrect deletions in that code path). code diff: ```diff @@ -233,52 +230,6 @@ socket |> assign(:bulk_edit_form, to_form(params, as: :bulk)) |> put_flash(:error, "Failed to apply tags: #{reason}")} - end - end - end - - # Bulk sysmon profile assignment handlers - def handle_event("open_bulk_sysmon_modal", _params, socket) do - scope = socket.assigns.current_scope - profiles = load_sysmon_profiles(scope) - - {:noreply, - socket - |> assign(:show_bulk_sysmon_modal, true) - |> assign(:available_sysmon_profiles, profiles) - |> assign(:bulk_sysmon_profile_id, nil)} - end - - def handle_event("close_bulk_sysmon_modal", _params, socket) do - {:noreply, - socket - |> assign(:show_bulk_sysmon_modal, false) - |> assign(:bulk_sysmon_profile_id, nil)} - end - - def handle_event("select_bulk_sysmon_profile", %{"profile_id" => profile_id}, socket) do - {:noreply, assign(socket, :bulk_sysmon_profile_id, profile_id)} - end - - def handle_event("apply_bulk_sysmon_profile", _params, socket) do - profile_id = socket.assigns.bulk_sysmon_profile_id - - if is_nil(profile_id) or profile_id == "" do - {:noreply, put_flash(socket, :error, "Please select a profile")} - else - case apply_sysmon_profile_to_devices(socket, profile_id) do - {:ok, count} -> - {:noreply, - socket - |> assign(:show_bulk_sysmon_modal, false) - |> assign(:bulk_sysmon_profile_id, nil) - |> assign(:selected_devices, MapSet.new()) - |> assign(:select_all_matching, false) - |> assign(:total_matching_count, nil) - |> put_flash(:info, "Assigned sysmon profile to #{count} device(s)")} - - {:error, reason} -> - {:noreply, put_flash(socket, :error, "Failed to assign profile: #{reason}")} end end end @@ -490,9 +441,6 @@ </button> </div> <div class="flex items-center gap-2"> - <.ui_button variant="ghost" size="sm" phx-click="open_bulk_sysmon_modal"> - <.icon name="hero-cpu-chip" class="size-4" /> Assign Profile - </.ui_button> <.ui_button variant="primary" size="sm" phx-click="open_bulk_edit_modal"> <.icon name="hero-tag" class="size-4" /> Bulk Edit </.ui_button> @@ -575,7 +523,9 @@ <% has_sysmon = is_binary(device_uid) and Map.get(@sysmon_presence, device_uid, false) == true %> <% sysmon_profile = - if is_binary(device_uid), do: Map.get(@sysmon_profiles_by_device, device_uid), else: nil %> + if is_binary(device_uid), + do: Map.get(@sysmon_profiles_by_device, device_uid), + else: nil %> <tr class={"hover:bg-base-200/40 #{if is_selected, do: "bg-primary/5", else: ""}"}> <td class="text-center"> <input @@ -660,13 +610,6 @@ form={@bulk_edit_form} selected_count={@effective_count} /> - <!-- Bulk Sysmon Profile Assignment Modal --> - <.bulk_sysmon_modal - :if={@show_bulk_sysmon_modal} - profiles={@available_sysmon_profiles} - selected_profile_id={@bulk_sysmon_profile_id} - selected_count={@effective_count} - /> </Layouts.app> """ end @@ -720,82 +663,6 @@ </div> <form method="dialog" class="modal-backdrop"> <button phx-click="close_bulk_edit_modal">close</button> - </form> - </dialog> - """ - end - - # Bulk Sysmon Profile Modal Component - attr :profiles, :list, required: true - attr :selected_profile_id, :string, default: nil - attr :selected_count, :integer, required: true - - defp bulk_sysmon_modal(assigns) do - ~H""" - <dialog id="bulk_sysmon_modal" class="modal modal-open"> - <div class="modal-box max-w-lg"> - <form method="dialog"> - <button - class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" - phx-click="close_bulk_sysmon_modal" - > - x - </button> - </form> - - <h3 class="text-lg font-bold">Assign Sysmon Profile</h3> - <p class="py-2 text-sm text-base-content/70"> - Assign a sysmon monitoring profile to {@selected_count} selected device(s). - </p> - - <div class="form-control w-full"> - <label class="label"> - <span class="label-text font-medium">Select Profile</span> - </label> - <div class="space-y-2"> - <%= for profile <- @profiles do %> - <label class={[ - "flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-colors", - if(@selected_profile_id == profile.id, do: "border-primary bg-primary/10", else: "border-base-200 hover:bg-base-100") - ]}> - <input - type="radio" - name="sysmon_profile" - class="radio radio-primary radio-sm" - checked={@selected_profile_id == profile.id} - phx-click="select_bulk_sysmon_profile" - phx-value-profile_id={profile.id} - /> - <div class="flex-1"> - <div class="font-medium text-sm">{profile.name}</div> - <div class="text-xs text-base-content/60"> - Interval: {profile.interval_seconds}s - <%= if profile.is_default do %> - <span class="badge badge-ghost badge-xs ml-2">Default</span> - <% end %> - </div> - </div> - </label> - <% end %> - </div> - </div> - - <div class="flex justify-end gap-2 pt-4"> - <button type="button" phx-click="close_bulk_sysmon_modal" class="btn btn-ghost"> - Cancel - </button> - <button - type="button" - phx-click="apply_bulk_sysmon_profile" - class="btn btn-primary" - disabled={is_nil(@selected_profile_id)} - > - Assign Profile - </button> - </div> - </div> - <form method="dialog" class="modal-backdrop"> - <button phx-click="close_bulk_sysmon_modal">close</button> </form> </dialog> """ @@ -1476,123 +1343,21 @@ end # Sysmon profile helpers - defp load_sysmon_profiles(scope) do - SysmonProfile - |> Ash.Query.for_read(:read, %{}) - |> Ash.Query.sort([:name]) - |> Ash.read!(scope: scope) - rescue - _ -> [] - end - - defp apply_sysmon_profile_to_devices(socket, profile_id) do - scope = socket.assigns.current_scope - - cond do - socket.assigns.select_all_matching and - not is_integer(socket.assigns.total_matching_count) -> - {:error, "Unable to determine selection size. Please try again."} - - socket.assigns.select_all_matching and - socket.assigns.total_matching_count > 10_000 -> - {:error, "Too many devices selected. Narrow your filters and try again."} - - true -> - case get_selected_uids(socket) do - [] -> - {:error, "No devices selected"} - - uids -> - create_sysmon_assignments(scope, uids, profile_id) - end - end - end - - defp create_sysmon_assignments(scope, device_uids, profile_id) do - # First, remove any existing device-specific assignments for these devices - existing_query = - SysmonProfileAssignment - |> Ash.Query.for_read(:read, %{}) - |> Ash.Query.filter(device_uid in ^device_uids) - - # Delete existing assignments - case Ash.read(existing_query, scope: scope) do - {:ok, existing} -> - Enum.each(existing, fn assignment -> - Ash.destroy!(assignment, scope: scope) - end) - - _ -> - :ok - end - - # Create new assignments for each device - results = - Enum.map(device_uids, fn device_uid -> - SysmonProfileAssignment - |> Ash.Changeset.for_create(:create, %{ - sysmon_profile_id: profile_id, - device_uid: device_uid, - priority: 100 - }) - |> Ash.create(scope: scope) - end) - - success_count = Enum.count(results, fn - {:ok, _} -> true - _ -> false - end) - - if success_count > 0 do - {:ok, success_count} - else - {:error, "Failed to create assignments"} - end - end ``` </details> ___ **Refine the <code>create_sysmon_assignments/3</code> function to only delete device-specific <br>assignments by adding <code>assignment_type == :device</code> to the query. Also, replace <br><code>Ash.destroy!/2</code> with <code>Ash.destroy/2</code> for safer error handling.** [web-ng/lib/serviceradar_web_ng_web/live/device_live/index.ex [1511-1551]](https://github.com/carverauto/serviceradar/pull/2279/files#diff-261a01f4876e5984e1d9e9b38a3540675dfb0272abc30e6bdb2a4fa610353cc7R1511-R1551) ```diff defp create_sysmon_assignments(scope, device_uids, profile_id) do # First, remove any existing device-specific assignments for these devices existing_query = SysmonProfileAssignment |> Ash.Query.for_read(:read, %{}) - |> Ash.Query.filter(device_uid in ^device_uids) + |> Ash.Query.filter(device_uid in ^device_uids and assignment_type == :device) # Delete existing assignments case Ash.read(existing_query, scope: scope) do {:ok, existing} -> Enum.each(existing, fn assignment -> - Ash.destroy!(assignment, scope: scope) + Ash.destroy(assignment, scope: scope) end) _ -> :ok end # Create new assignments for each device results = Enum.map(device_uids, fn device_uid -> SysmonProfileAssignment |> Ash.Changeset.for_create(:create, %{ sysmon_profile_id: profile_id, device_uid: device_uid, + assignment_type: :device, priority: 100 }) |> Ash.create(scope: scope) end) success_count = Enum.count(results, fn {:ok, _} -> true _ -> false end) if success_count > 0 do {:ok, success_count} else {:error, "Failed to create assignments"} end end ``` `[To ensure code accuracy, apply this suggestion manually]` <details><summary>Suggestion importance[1-10]: 8</summary> __ Why: The suggestion correctly identifies a bug that could cause incorrect data deletion and improves robustness by replacing a crashing function call with a safer alternative. </details></details></td><td align=center>Medium </td></tr><tr><td> <details><summary>✅ <s>Use correct interval field</s></summary> ___ <details><summary><b>Suggestion Impact:</b></summary>The commit did not replace `profile.interval_seconds` with `profile.sample_interval`. Instead, it removed the entire bulk sysmon profile assignment modal and its event handlers, including the template line that referenced `profile.interval_seconds`, thereby eliminating the runtime error by removing the feature/code path. code diff: ```diff @@ -233,52 +230,6 @@ socket |> assign(:bulk_edit_form, to_form(params, as: :bulk)) |> put_flash(:error, "Failed to apply tags: #{reason}")} - end - end - end - - # Bulk sysmon profile assignment handlers - def handle_event("open_bulk_sysmon_modal", _params, socket) do - scope = socket.assigns.current_scope - profiles = load_sysmon_profiles(scope) - - {:noreply, - socket - |> assign(:show_bulk_sysmon_modal, true) - |> assign(:available_sysmon_profiles, profiles) - |> assign(:bulk_sysmon_profile_id, nil)} - end - - def handle_event("close_bulk_sysmon_modal", _params, socket) do - {:noreply, - socket - |> assign(:show_bulk_sysmon_modal, false) - |> assign(:bulk_sysmon_profile_id, nil)} - end - - def handle_event("select_bulk_sysmon_profile", %{"profile_id" => profile_id}, socket) do - {:noreply, assign(socket, :bulk_sysmon_profile_id, profile_id)} - end - - def handle_event("apply_bulk_sysmon_profile", _params, socket) do - profile_id = socket.assigns.bulk_sysmon_profile_id - - if is_nil(profile_id) or profile_id == "" do - {:noreply, put_flash(socket, :error, "Please select a profile")} - else - case apply_sysmon_profile_to_devices(socket, profile_id) do - {:ok, count} -> - {:noreply, - socket - |> assign(:show_bulk_sysmon_modal, false) - |> assign(:bulk_sysmon_profile_id, nil) - |> assign(:selected_devices, MapSet.new()) - |> assign(:select_all_matching, false) - |> assign(:total_matching_count, nil) - |> put_flash(:info, "Assigned sysmon profile to #{count} device(s)")} - - {:error, reason} -> - {:noreply, put_flash(socket, :error, "Failed to assign profile: #{reason}")} end end end @@ -490,9 +441,6 @@ </button> </div> <div class="flex items-center gap-2"> - <.ui_button variant="ghost" size="sm" phx-click="open_bulk_sysmon_modal"> - <.icon name="hero-cpu-chip" class="size-4" /> Assign Profile - </.ui_button> <.ui_button variant="primary" size="sm" phx-click="open_bulk_edit_modal"> <.icon name="hero-tag" class="size-4" /> Bulk Edit </.ui_button> @@ -575,7 +523,9 @@ <% has_sysmon = is_binary(device_uid) and Map.get(@sysmon_presence, device_uid, false) == true %> <% sysmon_profile = - if is_binary(device_uid), do: Map.get(@sysmon_profiles_by_device, device_uid), else: nil %> + if is_binary(device_uid), + do: Map.get(@sysmon_profiles_by_device, device_uid), + else: nil %> <tr class={"hover:bg-base-200/40 #{if is_selected, do: "bg-primary/5", else: ""}"}> <td class="text-center"> <input @@ -660,13 +610,6 @@ form={@bulk_edit_form} selected_count={@effective_count} /> - <!-- Bulk Sysmon Profile Assignment Modal --> - <.bulk_sysmon_modal - :if={@show_bulk_sysmon_modal} - profiles={@available_sysmon_profiles} - selected_profile_id={@bulk_sysmon_profile_id} - selected_count={@effective_count} - /> </Layouts.app> """ end @@ -720,82 +663,6 @@ </div> <form method="dialog" class="modal-backdrop"> <button phx-click="close_bulk_edit_modal">close</button> - </form> - </dialog> - """ - end - - # Bulk Sysmon Profile Modal Component - attr :profiles, :list, required: true - attr :selected_profile_id, :string, default: nil - attr :selected_count, :integer, required: true - - defp bulk_sysmon_modal(assigns) do - ~H""" - <dialog id="bulk_sysmon_modal" class="modal modal-open"> - <div class="modal-box max-w-lg"> - <form method="dialog"> - <button - class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" - phx-click="close_bulk_sysmon_modal" - > - x - </button> - </form> - - <h3 class="text-lg font-bold">Assign Sysmon Profile</h3> - <p class="py-2 text-sm text-base-content/70"> - Assign a sysmon monitoring profile to {@selected_count} selected device(s). - </p> - - <div class="form-control w-full"> - <label class="label"> - <span class="label-text font-medium">Select Profile</span> - </label> - <div class="space-y-2"> - <%= for profile <- @profiles do %> - <label class={[ - "flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-colors", - if(@selected_profile_id == profile.id, do: "border-primary bg-primary/10", else: "border-base-200 hover:bg-base-100") - ]}> - <input - type="radio" - name="sysmon_profile" - class="radio radio-primary radio-sm" - checked={@selected_profile_id == profile.id} - phx-click="select_bulk_sysmon_profile" - phx-value-profile_id={profile.id} - /> - <div class="flex-1"> - <div class="font-medium text-sm">{profile.name}</div> - <div class="text-xs text-base-content/60"> - Interval: {profile.interval_seconds}s - <%= if profile.is_default do %> - <span class="badge badge-ghost badge-xs ml-2">Default</span> - <% end %> - </div> - </div> - </label> - <% end %> - </div> - </div> - - <div class="flex justify-end gap-2 pt-4"> - <button type="button" phx-click="close_bulk_sysmon_modal" class="btn btn-ghost"> - Cancel - </button> - <button - type="button" - phx-click="apply_bulk_sysmon_profile" - class="btn btn-primary" - disabled={is_nil(@selected_profile_id)} - > - Assign Profile - </button> - </div> - </div> - <form method="dialog" class="modal-backdrop"> - <button phx-click="close_bulk_sysmon_modal">close</button> </form> </dialog> """ @@ -1476,123 +1343,21 @@ ``` </details> ___ **In the bulk sysmon modal, replace the incorrect field <code>profile.interval_seconds</code> <br>with the correct field <code>profile.sample_interval</code> to display the profile's interval <br>and prevent a runtime error.** [web-ng/lib/serviceradar_web_ng_web/live/device_live/index.ex [771-777]](https://github.com/carverauto/serviceradar/pull/2279/files#diff-261a01f4876e5984e1d9e9b38a3540675dfb0272abc30e6bdb2a4fa610353cc7R771-R777) ```diff <div class="text-xs text-base-content/60"> - Interval: {profile.interval_seconds}s + Interval: {profile.sample_interval} <%= if profile.is_default do %> <span class="badge badge-ghost badge-xs ml-2">Default</span> <% end %> </div> ``` `[To ensure code accuracy, apply this suggestion manually]` <details><summary>Suggestion importance[1-10]: 8</summary> __ Why: The suggestion correctly identifies the use of a non-existent field (`interval_seconds`) which would cause a runtime error, and provides the correct field name (`sample_interval`). </details></details></td><td align=center>Medium </td></tr><tr><td> <details><summary>Enforce a single default profile<!-- not_implemented --></summary> ___ **Implement a custom Ash change to enforce a single default sysmon profile per <br>tenant by unsetting the current default before setting a new one.** [elixir/serviceradar_core/lib/serviceradar/sysmon_profiles/sysmon_profile.ex [130-139]](https://github.com/carverauto/serviceradar/pull/2279/files#diff-81bed24a71209f091c692710727cbfb6eec5386891e7dd2323fad7fc21763f17R130-R139) ```diff +# In a new file: elixir/serviceradar_core/lib/serviceradar/sysmon_profiles/changes/set_as_default.ex +defmodule ServiceRadar.SysmonProfiles.Changes.SetAsDefault do + use Ash.Resource.Change + + def change(changeset, _, context) do + # Unset other default profiles for the tenant + ServiceRadar.SysmonProfiles.SysmonProfile + |> Ash.Query.filter(is_default == true) + |> Ash.update_all([set: [is_default: false]], + tenant: Ash.Changeset.get_tenant(changeset), + actor: context.actor, + authorize?: false + ) + + # Set the current profile as the default + Ash.Changeset.change_attribute(changeset, :is_default, true) + end +end + +# In elixir/serviceradar_core/lib/serviceradar/sysmon_profiles/sysmon_profile.ex update :set_as_default do description "Set this profile as the default for the tenant" accept [] require_atomic? false - change fn changeset, _context -> - # This will be handled by a custom change that unsets other defaults - Ash.Changeset.change_attribute(changeset, :is_default, true) - end + change ServiceRadar.SysmonProfiles.Changes.SetAsDefault end ``` - [ ] **Apply / Chat** <!-- /improve --apply_suggestion=5 --> <details><summary>Suggestion importance[1-10]: 8</summary> __ Why: The suggestion correctly identifies a data integrity issue where multiple default profiles could exist for a tenant and provides a robust solution using a custom `Ash.Resource.Change`, which aligns with the developer's intent noted in the code comments. </details></details></td><td align=center>Medium </td></tr><tr><td> <details><summary>Ensure agent-device linking on registration</summary> ___ **Update the guard clause in <code>link_agent_to_device/4</code> to handle cases where the <br>agent's <code>device_uid</code> is <code>nil</code>, ensuring the agent-device link is created on first <br>registration.** [elixir/serviceradar_core/lib/serviceradar/edge/agent_gateway_sync.ex [322-346]](https://github.com/carverauto/serviceradar/pull/2279/files#diff-9b32fb2972aa43999e1afb261429b23bcba6a8868eab704270158bb12e1825beR322-R346) ```diff defp link_agent_to_device(agent_id, device_uid, tenant_schema, actor) do case Agent.get_by_uid(agent_id, tenant: tenant_schema, actor: actor) do - {:ok, %Agent{device_uid: existing_uid} = agent} when existing_uid != device_uid -> + {:ok, %Agent{device_uid: existing_uid} = agent} when is_nil(existing_uid) or existing_uid != device_uid -> agent |> Ash.Changeset.for_update(:gateway_sync, %{device_uid: device_uid}) |> Ash.update(tenant: tenant_schema, actor: actor) |> case do {:ok, _} -> Logger.debug("Linked agent #{agent_id} to device #{device_uid}") :ok {:error, reason} -> Logger.warning("Failed to link agent #{agent_id} to device: #{inspect(reason)}") :ok end {:ok, _agent} -> # Already linked or same device :ok {:error, _} -> # Agent not found yet, will be linked on next update :ok end end ``` - [ ] **Apply / Chat** <!-- /improve --apply_suggestion=6 --> <details><summary>Suggestion importance[1-10]: 8</summary> __ Why: This suggestion identifies a subtle but critical bug where an agent would not be linked to a device on its initial registration because the guard clause doesn't account for a `nil` `device_uid`. The fix is correct and essential for the feature's functionality. </details></details></td><td align=center>Medium </td></tr><tr><td> <details><summary>✅ <s>Prevent race condition during assignment<!-- not_implemented --></s></summary> ___ <details><summary><b>Suggestion Impact:</b></summary>The commit did not implement the proposed upsert/update-or-create logic. Instead, it removed the direct device assignment functionality entirely (deleted the handle_event that performed assignments and removed load_device_assignment/assign_sysmon_profile_to_device/remove_device_sysmon_assignment plus the SysmonProfileAssignment alias). This eliminates the original delete+create race window by removing that code path rather than refactoring it. code diff: ```diff @@ -154,34 +154,6 @@ push_patch(socket, to: page_path <> "?" <> URI.encode_query(%{"q" => query, "limit" => socket.assigns.limit}) )} - end - - def handle_event("assign_sysmon_profile", %{"profile_id" => profile_id}, socket) do - scope = socket.assigns.current_scope - device_uid = socket.assigns.device_uid - - result = - if profile_id == "" do - # Remove direct assignment - remove_device_sysmon_assignment(scope, device_uid) - else - # Create or update direct assignment - assign_sysmon_profile_to_device(scope, device_uid, profile_id) - end - - case result do - :ok -> - {sysmon_profile_info, available_profiles} = load_sysmon_profile_info(scope, device_uid) - - {:noreply, - socket - |> assign(:sysmon_profile_info, sysmon_profile_info) - |> assign(:available_profiles, available_profiles) - |> put_flash(:info, "Sysmon profile updated")} - - {:error, reason} -> - {:noreply, put_flash(socket, :error, "Failed to update profile: #{inspect(reason)}")} - end end def handle_event(_event, _params, socket), do: {:noreply, socket} @@ -1953,13 +1925,11 @@ def sysmon_config_section(assigns) do profile = Map.get(assigns.profile_info, :profile) source = Map.get(assigns.profile_info, :source, "default") - direct_assignment = Map.get(assigns.profile_info, :direct_assignment) assigns = assigns |> assign(:profile, profile) |> assign(:source, source) - |> assign(:direct_assignment, direct_assignment) ~H""" <div class="rounded-xl border border-base-200 bg-base-100 shadow-sm"> @@ -1974,59 +1944,42 @@ </div> <div class="p-4"> - <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> - <!-- Current Profile Info --> - <div class="space-y-3"> - <div class="text-xs font-semibold uppercase tracking-wide text-base-content/60"> - Effective Profile + <div class="space-y-3"> + <div class="text-xs font-semibold uppercase tracking-wide text-base-content/60"> + Effective Profile + </div> + <div :if={@profile} class="space-y-2"> + <div class="flex items-center gap-2"> + <span class="font-medium">{@profile.name}</span> + <.ui_badge :if={@profile.is_default} variant="info" size="xs">Default</.ui_badge> </div> - <div :if={@profile} class="space-y-2"> - <div class="flex items-center gap-2"> - <span class="font-medium">{@profile.name}</span> - <.ui_badge :if={@profile.is_default} variant="info" size="xs">Default</.ui_badge> - </div> - <div class="text-xs text-base-content/60"> - Interval: <span class="font-mono">{@profile.sample_interval}</span> - </div> - <div class="flex flex-wrap gap-1 mt-1"> - <.ui_badge :if={@profile.collect_cpu} variant="ghost" size="xs">CPU</.ui_badge> - <.ui_badge :if={@profile.collect_memory} variant="ghost" size="xs">Memory</.ui_badge> - <.ui_badge :if={@profile.collect_disk} variant="ghost" size="xs">Disk</.ui_badge> - <.ui_badge :if={@profile.collect_network} variant="ghost" size="xs">Network</.ui_badge> - <.ui_badge :if={@profile.collect_processes} variant="ghost" size="xs">Processes</.ui_badge> - </div> + <div class="text-xs text-base-content/60"> + Interval: <span class="font-mono">{@profile.sample_interval}</span> </div> - <div :if={is_nil(@profile)} class="text-sm text-base-content/60"> - Using default configuration + <div :if={@profile.target_query && @source == "srql"} class="text-xs text-base-content/60"> + Matched by: + <code class="font-mono bg-base-200/50 px-1 rounded">{@profile.target_query}</code> + </div> + <div class="flex flex-wrap gap-1 mt-1"> + <.ui_badge :if={@profile.collect_cpu} variant="ghost" size="xs">CPU</.ui_badge> + <.ui_badge :if={@profile.collect_memory} variant="ghost" size="xs">Memory</.ui_badge> + <.ui_badge :if={@profile.collect_disk} variant="ghost" size="xs">Disk</.ui_badge> + <.ui_badge :if={@profile.collect_network} variant="ghost" size="xs"> + Network + </.ui_badge> + <.ui_badge :if={@profile.collect_processes} variant="ghost" size="xs"> + Processes + </.ui_badge> </div> </div> - - <!-- Assignment Controls --> - <div class="space-y-3"> - <div class="text-xs font-semibold uppercase tracking-wide text-base-content/60"> - Direct Assignment - </div> - <form phx-change="assign_sysmon_profile" class="flex items-center gap-2"> - <select - name="profile_id" - class="select select-bordered select-sm flex-1" - > - <option value="" selected={is_nil(@direct_assignment)}> - Use tag/default profile - </option> - <%= for p <- @available_profiles do %> - <option - value={p.id} - selected={@direct_assignment && @direct_assignment.profile_id == p.id} - > - {p.name} - </option> - <% end %> - </select> - </form> - <div class="text-xs text-base-content/50"> - Direct assignments override tag-based and default profiles. - </div> + <div :if={is_nil(@profile)} class="text-sm text-base-content/60"> + Using default configuration + </div> + <div class="text-xs text-base-content/50 pt-2"> + Profile targeting is configured via SRQL queries in <.link + navigate="/settings/sysmon" + class="link link-primary" + >Settings</.link>. </div> </div> </div> @@ -2039,11 +1992,10 @@ defp source_badge(assigns) do {label, variant} = case assigns.source do - "device" -> {"Direct", "primary"} - "tag" -> {"Tag", "secondary"} + "srql" -> {"SRQL Targeting", "primary"} "default" -> {"Default", "ghost"} "local" -> {"Local Override", "warning"} - _ -> {"Unknown", "ghost"} + _ -> {"Default", "ghost"} end assigns = @@ -2073,28 +2025,24 @@ tenant_schema = ServiceRadarWebNGWeb.TenantResolver.schema_for_tenant_id(tenant_id) actor = get_sweep_actor(scope) - # Load available profiles + # Load available profiles (for reference) available_profiles = load_available_profiles(tenant_schema, actor) - # Check for direct device assignment - direct_assignment = load_device_assignment(tenant_schema, device_uid, actor) - - # Resolve the effective profile + # Resolve the effective profile via SRQL targeting profile = SysmonCompiler.resolve_profile(tenant_schema, device_uid, nil, actor) + # Determine source based on profile type source = cond do - direct_assignment -> "device" - # Could check for tag match here, but for simplicity we'll show "default" for non-direct - profile && profile.is_default -> "default" - profile -> "tag" + is_nil(profile) -> "default" + profile.is_default -> "default" + not is_nil(profile.target_query) -> "srql" true -> "default" end profile_info = %{ profile: profile, - source: source, - direct_assignment: direct_assignment + source: source } {profile_info, available_profiles} @@ -2112,57 +2060,4 @@ {:error, _} -> [] end end - - defp load_device_assignment(tenant_schema, device_uid, actor) do - query = - SysmonProfileAssignment - |> Ash.Query.for_read(:for_device, %{device_uid: device_uid}, actor: actor, tenant: tenant_schema) - - case Ash.read_one(query, actor: actor) do - {:ok, assignment} -> assignment - {:error, _} -> nil - end - end - - defp assign_sysmon_profile_to_device(scope, device_uid, profile_id) do - tenant_id = Scope.tenant_id(scope) - tenant_schema = ServiceRadarWebNGWeb.TenantResolver.schema_for_tenant_id(tenant_id) - actor = get_sweep_actor(scope) - - # First, remove any existing direct assignment - case load_device_assignment(tenant_schema, device_uid, actor) do - nil -> :ok - existing -> Ash.destroy(existing, actor: actor, tenant: tenant_schema) - end - - # Create new assignment - attrs = %{ - profile_id: profile_id, - assignment_type: :device, - device_uid: device_uid, - priority: 100 - } - - changeset = Ash.Changeset.for_create(SysmonProfileAssignment, :create, attrs) - - case Ash.create(changeset, actor: actor, tenant: tenant_schema) do - {:ok, _} -> :ok - {:error, reason} -> {:error, reason} - end - end - - defp remove_device_sysmon_assignment(scope, device_uid) do - tenant_id = Scope.tenant_id(scope) - tenant_schema = ServiceRadarWebNGWeb.TenantResolver.schema_for_tenant_id(tenant_id) - actor = get_sweep_actor(scope) - - case load_device_assignment(tenant_schema, device_uid, actor) do - nil -> :ok - assignment -> - case Ash.destroy(assignment, actor: actor, tenant: tenant_schema) do - :ok -> :ok - {:error, reason} -> {:error, reason} - end - end - end end ``` </details> ___ **Refactor <code>assign_sysmon_profile_to_device/3</code> to perform an atomic upsert <br>operation. Instead of deleting and creating, update the existing assignment if <br>it exists, or create a new one if it does not, to prevent a potential race <br>condition.** [web-ng/lib/serviceradar_web_ng_web/live/device_live/show.ex [2127-2152]](https://github.com/carverauto/serviceradar/pull/2279/files#diff-92d8e0af57d2f65dfb8562e896dbea17ef9f47074ffd14f58261decc76f80c24R2127-R2152) ```diff defp assign_sysmon_profile_to_device(scope, device_uid, profile_id) do tenant_id = Scope.tenant_id(scope) tenant_schema = ServiceRadarWebNGWeb.TenantResolver.schema_for_tenant_id(tenant_id) actor = get_sweep_actor(scope) - # First, remove any existing direct assignment case load_device_assignment(tenant_schema, device_uid, actor) do - nil -> :ok - existing -> Ash.destroy(existing, actor: actor, tenant: tenant_schema) - end + nil -> + # Create new assignment if none exists + attrs = %{ + profile_id: profile_id, + assignment_type: :device, + device_uid: device_uid, + priority: 100 + } + changeset = Ash.Changeset.for_create(SysmonProfileAssignment, :create, attrs) + case Ash.create(changeset, actor: actor, tenant: tenant_schema) do + {:ok, _} -> :ok + {:error, reason} -> {:error, reason} + end - # Create new assignment - attrs = %{ - profile_id: profile_id, - assignment_type: :device, - device_uid: device_uid, - priority: 100 - } - - changeset = Ash.Changeset.for_create(SysmonProfileAssignment, :create, attrs) - - case Ash.create(changeset, actor: actor, tenant: tenant_schema) do - {:ok, _} -> :ok - {:error, reason} -> {:error, reason} + existing -> + # Update existing assignment + changeset = Ash.Changeset.for_update(existing, :update, %{profile_id: profile_id}) + case Ash.update(changeset, actor: actor, tenant: tenant_schema) do + {:ok, _} -> :ok + {:error, reason} -> {:error, reason} + end end end ``` `[To ensure code accuracy, apply this suggestion manually]` <details><summary>Suggestion importance[1-10]: 7</summary> __ Why: The suggestion correctly identifies a potential race condition and proposes a more robust upsert-style logic, which improves the atomicity and reliability of the operation. </details></details></td><td align=center>Medium </td></tr><tr><td> <details><summary>Avoid silently ignoring invalid filters</summary> ___ **Remove the <code>rescue</code> block in <code>apply_filter/2</code> to prevent silently ignoring invalid <br>SRQL filters and to allow proper error propagation and logging.** [elixir/serviceradar_core/lib/serviceradar/sysmon_profiles/srql_target_resolver.ex [177-191]](https://github.com/carverauto/serviceradar/pull/2279/files#diff-92fc55ddb645aa284c8bded705d5750290d8058cce77ca339f97d5d77fe5b9d2R177-R191) ```diff defp apply_filter(query, %{field: field, op: op, value: value}) when is_binary(field) do # Map common SRQL field names to Device attributes mapped_field = map_field(field) # Handle special cases like tags if String.starts_with?(field, "tags.") do # tags.key:value -> filter on tags JSONB tag_key = String.replace_prefix(field, "tags.", "") apply_tag_filter(query, tag_key, value) else apply_standard_filter(query, mapped_field, op, value) end -rescue - _ -> query end ``` - [ ] **Apply / Chat** <!-- /improve --apply_suggestion=8 --> <details><summary>Suggestion importance[1-10]: 7</summary> __ Why: The suggestion correctly points out that silently swallowing errors is poor practice and can lead to incorrect behavior. Removing the `rescue` block improves error handling by leveraging the existing error logging in the calling function, making debugging easier. </details></details></td><td align=center>Medium </td></tr><tr><td> <details><summary>Allow disabling sysmon via remote config</summary> ___ **Update <code>applySysmonConfig</code> to reconfigure the <code>sysmon</code> service even when <br><code>protoConfig.Enabled</code> is <code>false</code>, allowing the service to be disabled remotely.** [pkg/agent/push_loop.go [1028-1071]](https://github.com/carverauto/serviceradar/pull/2279/files#diff-5f0d59be34ef26b449d7f5fd2b198a29b277936b9708a699f7487415ed6c2785R1028-R1071) ```diff // applySysmonConfig applies sysmon configuration from the gateway to the embedded sysmon service. func (p *PushLoop) applySysmonConfig(protoConfig *proto.SysmonConfig) { p.server.mu.RLock() sysmonSvc := p.server.sysmonService p.server.mu.RUnlock() if sysmonSvc == nil { p.logger.Debug().Msg("Sysmon service not initialized, skipping config apply") return } // Convert proto config to sysmon.Config cfg := protoToSysmonConfig(protoConfig) - // Check if this is a meaningful configuration change - if !protoConfig.Enabled { - p.logger.Info().Msg("Sysmon disabled in gateway config") - // Don't stop the service here - let admin do that explicitly - return - } - // Parse and apply the configuration parsed, err := cfg.Parse() if err != nil { p.logger.Error().Err(err).Msg("Failed to parse sysmon config from gateway") return } if err := sysmonSvc.Reconfigure(parsed); err != nil { p.logger.Error().Err(err).Msg("Failed to apply sysmon config from gateway") + return + } + + // Check if this is a meaningful configuration change + if !protoConfig.Enabled { + p.logger.Info().Msg("Sysmon disabled via gateway config") + // The service is now reconfigured to be disabled, but not stopped. + // The collector loop will continue but collections will do nothing. return } p.logger.Info(). Str("profile_id", protoConfig.ProfileId). Str("profile_name", protoConfig.ProfileName). Str("config_source", protoConfig.ConfigSource). Str("sample_interval", cfg.SampleInterval). Bool("cpu", cfg.CollectCPU). Bool("memory", cfg.CollectMemory). Bool("disk", cfg.CollectDisk). Bool("network", cfg.CollectNetwork). Bool("processes", cfg.CollectProcesses). Msg("Applied sysmon config from gateway") } ``` `[To ensure code accuracy, apply this suggestion manually]` <details><summary>Suggestion importance[1-10]: 7</summary> __ Why: The suggestion correctly points out that the service cannot be disabled via remote configuration, which is a logical flaw in the implementation. Fixing this improves the remote management capability of the feature. </details></details></td><td align=center>Medium </td></tr><tr><td rowspan=3>General</td> <td> <details><summary>✅ <s>Allow modal close clicks<!-- not_implemented --></s></summary> ___ <details><summary><b>Suggestion Impact:</b></summary>The PR did not implement the suggested wrapper removals. Instead, it removed the entire bulk sysmon modal UI and its related LiveView event handlers, which eliminates the problematic <form method="dialog"> usage by deleting the feature/component altogether. code diff: ```diff @@ -233,52 +230,6 @@ socket |> assign(:bulk_edit_form, to_form(params, as: :bulk)) |> put_flash(:error, "Failed to apply tags: #{reason}")} - end - end - end - - # Bulk sysmon profile assignment handlers - def handle_event("open_bulk_sysmon_modal", _params, socket) do - scope = socket.assigns.current_scope - profiles = load_sysmon_profiles(scope) - - {:noreply, - socket - |> assign(:show_bulk_sysmon_modal, true) - |> assign(:available_sysmon_profiles, profiles) - |> assign(:bulk_sysmon_profile_id, nil)} - end - - def handle_event("close_bulk_sysmon_modal", _params, socket) do - {:noreply, - socket - |> assign(:show_bulk_sysmon_modal, false) - |> assign(:bulk_sysmon_profile_id, nil)} - end - - def handle_event("select_bulk_sysmon_profile", %{"profile_id" => profile_id}, socket) do - {:noreply, assign(socket, :bulk_sysmon_profile_id, profile_id)} - end - - def handle_event("apply_bulk_sysmon_profile", _params, socket) do - profile_id = socket.assigns.bulk_sysmon_profile_id - - if is_nil(profile_id) or profile_id == "" do - {:noreply, put_flash(socket, :error, "Please select a profile")} - else - case apply_sysmon_profile_to_devices(socket, profile_id) do - {:ok, count} -> - {:noreply, - socket - |> assign(:show_bulk_sysmon_modal, false) - |> assign(:bulk_sysmon_profile_id, nil) - |> assign(:selected_devices, MapSet.new()) - |> assign(:select_all_matching, false) - |> assign(:total_matching_count, nil) - |> put_flash(:info, "Assigned sysmon profile to #{count} device(s)")} - - {:error, reason} -> - {:noreply, put_flash(socket, :error, "Failed to assign profile: #{reason}")} end end end @@ -490,9 +441,6 @@ </button> </div> <div class="flex items-center gap-2"> - <.ui_button variant="ghost" size="sm" phx-click="open_bulk_sysmon_modal"> - <.icon name="hero-cpu-chip" class="size-4" /> Assign Profile - </.ui_button> <.ui_button variant="primary" size="sm" phx-click="open_bulk_edit_modal"> <.icon name="hero-tag" class="size-4" /> Bulk Edit </.ui_button> @@ -575,7 +523,9 @@ <% has_sysmon = is_binary(device_uid) and Map.get(@sysmon_presence, device_uid, false) == true %> <% sysmon_profile = - if is_binary(device_uid), do: Map.get(@sysmon_profiles_by_device, device_uid), else: nil %> + if is_binary(device_uid), + do: Map.get(@sysmon_profiles_by_device, device_uid), + else: nil %> <tr class={"hover:bg-base-200/40 #{if is_selected, do: "bg-primary/5", else: ""}"}> <td class="text-center"> <input @@ -660,13 +610,6 @@ form={@bulk_edit_form} selected_count={@effective_count} /> - <!-- Bulk Sysmon Profile Assignment Modal --> - <.bulk_sysmon_modal - :if={@show_bulk_sysmon_modal} - profiles={@available_sysmon_profiles} - selected_profile_id={@bulk_sysmon_profile_id} - selected_count={@effective_count} - /> </Layouts.app> """ end @@ -720,82 +663,6 @@ </div> <form method="dialog" class="modal-backdrop"> <button phx-click="close_bulk_edit_modal">close</button> - </form> - </dialog> - """ - end - - # Bulk Sysmon Profile Modal Component - attr :profiles, :list, required: true - attr :selected_profile_id, :string, default: nil - attr :selected_count, :integer, required: true - - defp bulk_sysmon_modal(assigns) do - ~H""" - <dialog id="bulk_sysmon_modal" class="modal modal-open"> - <div class="modal-box max-w-lg"> - <form method="dialog"> - <button - class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" - phx-click="close_bulk_sysmon_modal" - > - x - </button> - </form> - - <h3 class="text-lg font-bold">Assign Sysmon Profile</h3> - <p class="py-2 text-sm text-base-content/70"> - Assign a sysmon monitoring profile to {@selected_count} selected device(s). - </p> - - <div class="form-control w-full"> - <label class="label"> - <span class="label-text font-medium">Select Profile</span> - </label> - <div class="space-y-2"> - <%= for profile <- @profiles do %> - <label class={[ - "flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-colors", - if(@selected_profile_id == profile.id, do: "border-primary bg-primary/10", else: "border-base-200 hover:bg-base-100") - ]}> - <input - type="radio" - name="sysmon_profile" - class="radio radio-primary radio-sm" - checked={@selected_profile_id == profile.id} - phx-click="select_bulk_sysmon_profile" - phx-value-profile_id={profile.id} - /> - <div class="flex-1"> - <div class="font-medium text-sm">{profile.name}</div> - <div class="text-xs text-base-content/60"> - Interval: {profile.interval_seconds}s - <%= if profile.is_default do %> - <span class="badge badge-ghost badge-xs ml-2">Default</span> - <% end %> - </div> - </div> - </label> - <% end %> - </div> - </div> - - <div class="flex justify-end gap-2 pt-4"> - <button type="button" phx-click="close_bulk_sysmon_modal" class="btn btn-ghost"> - Cancel - </button> - <button - type="button" - phx-click="apply_bulk_sysmon_profile" - class="btn btn-primary" - disabled={is_nil(@selected_profile_id)} - > - Assign Profile - </button> - </div> - </div> - <form method="dialog" class="modal-backdrop"> - <button phx-click="close_bulk_sysmon_modal">close</button> </form> </dialog> """ ``` </details> ___ **Remove the <code><form method="dialog"></code> wrappers from the modal's close button and <br>backdrop to ensure their <code>phx-click="close_bulk_sysmon_modal"</code> events are handled <br>by LiveView.** [web-ng/lib/serviceradar_web_ng_web/live/device_live/index.ex [735-800]](https://github.com/carverauto/serviceradar/pull/2279/files#diff-261a01f4876e5984e1d9e9b38a3540675dfb0272abc30e6bdb2a4fa610353cc7R735-R800) ```diff <dialog id="bulk_sysmon_modal" class="modal modal-open"> <div class="modal-box max-w-lg"> - <form method="dialog"> - <button - class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" - phx-click="close_bulk_sysmon_modal" - > - x - </button> - </form> + <button + class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" + phx-click="close_bulk_sysmon_modal" + type="button" + > + x + </button> ... </div> - <form method="dialog" class="modal-backdrop"> - <button phx-click="close_bulk_sysmon_modal">close</button> - </form> + <div class="modal-backdrop" phx-click="close_bulk_sysmon_modal"></div> </dialog> ``` `[To ensure code accuracy, apply this suggestion manually]` <details><summary>Suggestion importance[1-10]: 7</summary> __ Why: The suggestion correctly identifies that wrapping buttons with `phx-click` inside a `<form method="dialog">` will break the LiveView event handling, and the proposed fix is correct. </details></details></td><td align=center>Medium </td></tr><tr><td> <details><summary>Prioritize custom config directory first</summary> ___ **Modify <code>loadCon...
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
carverauto/serviceradar!2660
No description provided.