merging testing into staging #2649

Merged
mfreeman451 merged 28 commits from refs/pull/2649/head into staging 2026-01-11 18:01:51 +00:00
mfreeman451 commented 2026-01-11 18:01:29 +00:00 (Migrated from github.com)
Owner

Imported from GitHub pull request.

Original GitHub pull request: #2246
Original author: @mfreeman451
Original URL: https://github.com/carverauto/serviceradar/pull/2246
Original created: 2026-01-11T18:01:29Z
Original updated: 2026-01-11T18:04:33Z
Original head: carverauto/serviceradar:testing
Original base: staging
Original merged: 2026-01-11T18:01:51Z 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, Tests


Description

  • Comprehensive multi-tenant infrastructure implementation with 30+ database tables for user management, agents, devices, gateways, and monitoring systems

  • New stateful alert engine with bucketed event aggregation supporting time-windowed rule evaluation and alert lifecycle management

  • Gateway process implementation for distributed check execution with load balancing and execution metrics tracking

  • Enhanced LiveView components for infrastructure monitoring, cluster status, agent details, and integration source management

  • Job scheduler UI redesigned with search, filtering, pagination, and support for multiple job sources (Cron and AshOban triggers)

  • Edge package creation refactored with Ash forms and automatic certificate generation

  • Comprehensive test coverage for TenantRegistry multi-tenant isolation and infrastructure components

  • Migration from legacy poller-based architecture to gateway-based distributed execution model

  • Significant codebase refactoring from Go to Elixir with removal of legacy Go packages and old user/device management modules


Diagram Walkthrough

flowchart LR
  DB["Database Schema<br/>30+ Tables"]
  TenantReg["TenantRegistry<br/>Multi-tenant Isolation"]
  AlertEngine["Stateful Alert Engine<br/>Bucketed Aggregation"]
  GatewayProc["Gateway Process<br/>Distributed Execution"]
  LiveViews["Enhanced LiveViews<br/>Infrastructure/Cluster/Agent"]
  
  DB --> TenantReg
  TenantReg --> GatewayProc
  AlertEngine --> LiveViews
  GatewayProc --> LiveViews

File Walkthrough

Relevant files
Database schema
1 files
20260107043446_initial_schema.exs
Initial tenant schema migration with core infrastructure tables

elixir/serviceradar_core/priv/repo/tenant_migrations/20260107043446_initial_schema.exs

  • Comprehensive database migration creating 30+ tables for multi-tenant
    infrastructure
  • Defines schema for user management, agents, devices, gateways, and
    monitoring systems
  • Establishes relationships between core entities (devices, agents,
    gateways, partitions)
  • Includes encryption support for sensitive data (credentials, keys) and
    audit tables
+1416/-0
Feature
1 files
index.ex
Integration sources management LiveView with full CRUD UI

web-ng/lib/serviceradar_web_ng_web/live/admin/integration_live/index.ex

  • LiveView component for managing integration sources (Armis, SNMP,
    Syslog, Nmap, Custom)
  • Implements CRUD operations with modal-based UI for create, edit, and
    view details
  • Supports dynamic query management, network blacklist configuration,
    and credential handling
  • Includes filtering by source type and enabled status, with
    agent/partition assignment
+1471/-0
Tests
2 files
tenant_registry_test.exs
TenantRegistry unit tests with multi-tenant isolation coverage

elixir/serviceradar_core/test/serviceradar/cluster/tenant_registry_test.exs

  • Comprehensive test suite for TenantRegistry module with 15+ test cases
  • Tests registry creation, tenant isolation, gateway/agent registration
    and lookup
  • Validates process counting, heartbeat updates, and infrastructure
    cleanup
  • Ensures proper separation of concerns across multiple tenants
+310/-0 
test_helper.exs
ExUnit test framework initialization                                         

web-ng/serviceradar/test/test_helper.exs

  • Minimal test helper file that starts ExUnit test framework
+1/-0     
Enhancement
8 files
index.ex
New cluster status monitoring LiveView for settings           

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

  • New LiveView module for real-time cluster monitoring from settings
    area
  • Displays ERTS cluster topology, Horde-managed gateways/agents, and
    Oban job queue status
  • Implements PubSub subscriptions for cluster events and periodic
    refresh scheduling
  • Provides health metrics cards, cluster nodes table, gateways/agents
    tables, and recent events log
+1086/-0
index.ex
Refactor edge package creation with Ash forms and certificates

web-ng/lib/serviceradar_web_ng_web/live/admin/edge_package_live/index.ex

  • Migrated from Ecto changesets to AshPhoenix.Form for form handling
  • Added automatic certificate generation via create_with_tenant_cert
    function
  • Changed component type default from "poller" to "gateway" and added
    "sync" type option
  • Enhanced success modal with Docker/systemd install commands and
    improved UX with loading states
  • Updated to use SettingsComponents instead of AdminComponents and
    refactored layout structure
+546/-224
index.ex
New infrastructure monitoring LiveView with cluster visibility

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

  • New LiveView for displaying cluster nodes and agent gateways
    infrastructure
  • Implements tab-based navigation (overview, nodes, gateways, agents)
    with platform admin visibility controls
  • Caches gateways and agents locally with PubSub synchronization for
    real-time updates
  • Provides staleness detection using wall-clock time with monotonic time
    fallback
  • Includes debug panel for platform admins and summary cards for quick
    metrics
+1029/-0
20260110054954_add_stateful_alert_rules.exs
Add stateful alert rules database schema migration             

elixir/serviceradar_core/priv/repo/tenant_migrations/20260110054954_add_stateful_alert_rules.exs

  • Creates stateful_alert_rules table with rule configuration (name,
    signal, thresholds, windows, cooldown)
  • Creates stateful_alert_rule_states table for tracking per-group rule
    state and firing history
  • Adds unique indexes on tenant_id+name for rules and
    tenant_id+rule_id+group_key for states
  • Includes timestamp fields for last_seen, last_fired, and
    last_notification tracking
+85/-0   
show.ex
Agent detail view enhanced with live registry and system metrics

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

  • Enhanced agent detail view to display both live Horde registry data
    and database records with rich system information
  • Added live agent status indicators, gateway node system metrics
    (memory, processes, schedulers), and registration timeline
  • Integrated service checks display with check type badges and status
    indicators
  • Replaced hardcoded agent type mappings with dynamic lookups via
    ServiceRadar.Infrastructure.Agent module
  • Refactored component structure with new cards for capabilities,
    gateway node info, registration timeline, and service checks
+501/-159
index.ex
Job scheduler UI redesigned with search, filtering, and pagination

web-ng/lib/serviceradar_web_ng_web/live/admin/job_live/index.ex

  • Completely redesigned job scheduler UI from card-based layout to
    searchable, sortable table with pagination
  • Added support for multiple job sources (Cron jobs and AshOban
    triggers) with filtering and search capabilities
  • Implemented auto-refresh functionality with configurable intervals and
    manual trigger capability for jobs
  • Added role-based access control for viewing platform jobs, triggering
    jobs, and accessing Oban Web
  • Replaced inline schedule editing with read-only job display and
    navigation to detail pages
+610/-212
stateful_alert_engine.ex
New stateful alert engine with bucketed event aggregation

elixir/serviceradar_core/lib/serviceradar/observability/stateful_alert_engine.ex

  • New GenServer module implementing bucketed stateful alert evaluation
    for log and event rules
  • Supports time-windowed event aggregation with configurable bucket
    sizes and cooldown periods
  • Handles alert lifecycle (firing, recovery, renotification) with
    history tracking and snapshot persistence
  • Implements flexible rule matching for logs and events with support for
    grouping, severity filtering, and attribute matching
  • Integrates with Ash resources for rule management and state
    persistence via ETS and database snapshots
+960/-0 
gateway_process.ex
New gateway process for distributed check execution           

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

  • New GenServer module representing an agent gateway in the ERTS cluster
    for check execution and result aggregation
  • Provides synchronous and asynchronous job execution APIs with health
    checks and result retrieval
  • Implements agent discovery and load balancing with domain-based and
    partition-based selection strategies
  • Tracks execution metrics (jobs executed, checks executed, average
    execution time) and maintains registry heartbeats
  • Handles communication between Core (via AshOban), Gateway, and Agent
    processes for distributed check execution
+466/-0 
Additional files
101 files
.bazelignore +4/-0     
.bazelrc +5/-0     
.env-sample +33/-0   
.env.example +38/-0   
main.yml +18/-0   
sbom-images.yml +1/-3     
web-lint.yml +0/-60   
AGENTS.md +177/-11
INSTALL.md +19/-15 
MODULE.bazel +22/-2   
Makefile +58/-56 
README-Docker.md +17/-2   
README.md +3/-3     
ROADMAP.md +1/-1     
BUILD.bazel +11/-38 
BUILD.bazel +12/-0   
mix_release.bzl +141/-49
BUILD.bazel +1/-0     
README.md +4/-4     
config.json +5/-6     
main.go +174/-74
build.rs +0/-1     
monitoring.proto +3/-26   
server.rs +2/-0     
BUILD.bazel +1/-1     
main.go +1/-1     
README.md +2/-2     
monitoring.proto +2/-26   
server.rs +6/-6     
main.go +16/-2   
Cargo.toml +0/-3     
README.md +8/-8     
config.rs +85/-28 
grpc_server.rs +2/-2     
message_processor.rs +2/-16   
nats.rs +4/-0     
zen-consumer-with-otel.json +14/-11 
zen-consumer.json +14/-11 
.ko.yaml +0/-15   
BUILD.bazel +0/-17   
BUILD.bazel +0/-24   
app.go +0/-206 
config.json +0/-165 
config.json +0/-165 
main.go +0/-86   
BUILD.bazel +1/-0     
main.go +68/-0   
README.md +3/-3     
README.md +9/-12   
flowgger.toml +2/-1     
nats_output.rs +14/-0   
otel.toml +3/-1     
otel.toml.example +5/-2     
config.rs +21/-3   
nats_output.rs +22/-5   
setup.rs +1/-0     
BUILD.bazel +0/-25   
config.json +0/-111 
main.go +0/-138 
BUILD.bazel +0/-25   
config.json +0/-77   
main.go +0/-123 
main.go +1/-1     
README.md +3/-3     
config.rs +22/-1   
main.rs +23/-3   
docker-compose.dev.yml +23/-34 
docker-compose.elx.yml +117/-0 
docker-compose.spiffe.yml +39/-192
docker-compose.yml +316/-269
README.md +6/-5     
Dockerfile.agent-gateway +94/-0   
Dockerfile.core +0/-93   
Dockerfile.core-elx +108/-0 
Dockerfile.poller +0/-70   
Dockerfile.sync +0/-95   
Dockerfile.tools +1/-2     
Dockerfile.web +0/-110 
Dockerfile.web-ng +6/-0     
agent-minimal.docker.json +6/-6     
agent.docker.json +5/-20   
agent.mtls.json +7/-10   
bootstrap-nested-spire.sh +0/-80   
.gitkeep +1/-0     
datasvc.docker.json +3/-2     
datasvc.mtls.json +14/-1   
db-event-writer.docker.json +15/-11 
db-event-writer.mtls.json +10/-8   
FRICTION_POINTS.md +0/-355 
README.md +0/-207 
SETUP_GUIDE.md +0/-307 
docker-compose.edge-e2e.yml +0/-27   
manage-packages.sh +0/-211 
setup-edge-e2e.sh +0/-198 
edge-poller-restart.sh +0/-178 
downstream-agent.conf +0/-32   
env +0/-4     
server.conf +0/-51   
upstream-agent.conf +0/-32   
entrypoint-certs.sh +13/-9   
Additional files not shown

Imported from GitHub pull request. Original GitHub pull request: #2246 Original author: @mfreeman451 Original URL: https://github.com/carverauto/serviceradar/pull/2246 Original created: 2026-01-11T18:01:29Z Original updated: 2026-01-11T18:04:33Z Original head: carverauto/serviceradar:testing Original base: staging Original merged: 2026-01-11T18:01:51Z 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, Tests ___ ### **Description** - Comprehensive multi-tenant infrastructure implementation with 30+ database tables for user management, agents, devices, gateways, and monitoring systems - New stateful alert engine with bucketed event aggregation supporting time-windowed rule evaluation and alert lifecycle management - Gateway process implementation for distributed check execution with load balancing and execution metrics tracking - Enhanced LiveView components for infrastructure monitoring, cluster status, agent details, and integration source management - Job scheduler UI redesigned with search, filtering, pagination, and support for multiple job sources (Cron and AshOban triggers) - Edge package creation refactored with Ash forms and automatic certificate generation - Comprehensive test coverage for TenantRegistry multi-tenant isolation and infrastructure components - Migration from legacy poller-based architecture to gateway-based distributed execution model - Significant codebase refactoring from Go to Elixir with removal of legacy Go packages and old user/device management modules ___ ### Diagram Walkthrough ```mermaid flowchart LR DB["Database Schema<br/>30+ Tables"] TenantReg["TenantRegistry<br/>Multi-tenant Isolation"] AlertEngine["Stateful Alert Engine<br/>Bucketed Aggregation"] GatewayProc["Gateway Process<br/>Distributed Execution"] LiveViews["Enhanced LiveViews<br/>Infrastructure/Cluster/Agent"] DB --> TenantReg TenantReg --> GatewayProc AlertEngine --> LiveViews GatewayProc --> LiveViews ``` <details><summary><h3>File Walkthrough</h3></summary> <table><thead><tr><th></th><th align="left">Relevant files</th></tr></thead><tbody><tr><td><strong>Database schema</strong></td><td><details><summary>1 files</summary><table> <tr> <td> <details> <summary><strong>20260107043446_initial_schema.exs</strong><dd><code>Initial tenant schema migration with core infrastructure tables</code></dd></summary> <hr> elixir/serviceradar_core/priv/repo/tenant_migrations/20260107043446_initial_schema.exs <ul><li>Comprehensive database migration creating 30+ tables for multi-tenant <br>infrastructure<br> <li> Defines schema for user management, agents, devices, gateways, and <br>monitoring systems<br> <li> Establishes relationships between core entities (devices, agents, <br>gateways, partitions)<br> <li> Includes encryption support for sensitive data (credentials, keys) and <br>audit tables</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-0d217dc9822fab0d3390e8ec21040f98e67106e5c9126e043a9b701efcbfb576">+1416/-0</a></td> </tr> </table></details></td></tr><tr><td><strong>Feature</strong></td><td><details><summary>1 files</summary><table> <tr> <td> <details> <summary><strong>index.ex</strong><dd><code>Integration sources management LiveView with full CRUD UI</code></dd></summary> <hr> web-ng/lib/serviceradar_web_ng_web/live/admin/integration_live/index.ex <ul><li>LiveView component for managing integration sources (Armis, SNMP, <br>Syslog, Nmap, Custom)<br> <li> Implements CRUD operations with modal-based UI for create, edit, and <br>view details<br> <li> Supports dynamic query management, network blacklist configuration, <br>and credential handling<br> <li> Includes filtering by source type and enabled status, with <br>agent/partition assignment</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-61d0262af13a42905ebbd793e83537e61bfc09493df1664a82eb2536980ee1cd">+1471/-0</a></td> </tr> </table></details></td></tr><tr><td><strong>Tests</strong></td><td><details><summary>2 files</summary><table> <tr> <td> <details> <summary><strong>tenant_registry_test.exs</strong><dd><code>TenantRegistry unit tests with multi-tenant isolation coverage</code></dd></summary> <hr> elixir/serviceradar_core/test/serviceradar/cluster/tenant_registry_test.exs <ul><li>Comprehensive test suite for <code>TenantRegistry</code> module with 15+ test cases<br> <li> Tests registry creation, tenant isolation, gateway/agent registration <br>and lookup<br> <li> Validates process counting, heartbeat updates, and infrastructure <br>cleanup<br> <li> Ensures proper separation of concerns across multiple tenants</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-ff7e8a23791514dee76b77ef78fa2f8bc548f42a4d09897ae375e2ca7734fca5">+310/-0</a>&nbsp; </td> </tr> <tr> <td> <details> <summary><strong>test_helper.exs</strong><dd><code>ExUnit test framework initialization</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> web-ng/serviceradar/test/test_helper.exs - Minimal test helper file that starts ExUnit test framework </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-2f4b2939497dc679e70f8bb20e57263bed9c409a489f400f3308bbdda723f905">+1/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> </table></details></td></tr><tr><td><strong>Enhancement</strong></td><td><details><summary>8 files</summary><table> <tr> <td> <details> <summary><strong>index.ex</strong><dd><code>New cluster status monitoring LiveView for settings</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> web-ng/lib/serviceradar_web_ng_web/live/settings/cluster_live/index.ex <ul><li>New LiveView module for real-time cluster monitoring from settings <br>area<br> <li> Displays ERTS cluster topology, Horde-managed gateways/agents, and <br>Oban job queue status<br> <li> Implements PubSub subscriptions for cluster events and periodic <br>refresh scheduling<br> <li> Provides health metrics cards, cluster nodes table, gateways/agents <br>tables, and recent events log</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-644e64ac49eaa128f9b429c8507d6a8a9ad820e2796721dd665caba8a154d24e">+1086/-0</a></td> </tr> <tr> <td> <details> <summary><strong>index.ex</strong><dd><code>Refactor edge package creation with Ash forms and certificates</code></dd></summary> <hr> web-ng/lib/serviceradar_web_ng_web/live/admin/edge_package_live/index.ex <ul><li>Migrated from Ecto changesets to AshPhoenix.Form for form handling<br> <li> Added automatic certificate generation via <code>create_with_tenant_cert</code> <br>function<br> <li> Changed component type default from "poller" to "gateway" and added <br>"sync" type option<br> <li> Enhanced success modal with Docker/systemd install commands and <br>improved UX with loading states<br> <li> Updated to use <code>SettingsComponents</code> instead of <code>AdminComponents</code> and <br>refactored layout structure</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-d0d22a987aa942ef25088696daea862e36bd8ca083ce29e4670f63e62947c62f">+546/-224</a></td> </tr> <tr> <td> <details> <summary><strong>index.ex</strong><dd><code>New infrastructure monitoring LiveView with cluster visibility</code></dd></summary> <hr> web-ng/lib/serviceradar_web_ng_web/live/infrastructure_live/index.ex <ul><li>New LiveView for displaying cluster nodes and agent gateways <br>infrastructure<br> <li> Implements tab-based navigation (overview, nodes, gateways, agents) <br>with platform admin visibility controls<br> <li> Caches gateways and agents locally with PubSub synchronization for <br>real-time updates<br> <li> Provides staleness detection using wall-clock time with monotonic time <br>fallback<br> <li> Includes debug panel for platform admins and summary cards for quick <br>metrics</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-f1af30a84da554ef3d43b226a9303174b33dd5d27e23e9b702031483074e5f54">+1029/-0</a></td> </tr> <tr> <td> <details> <summary><strong>20260110054954_add_stateful_alert_rules.exs</strong><dd><code>Add stateful alert rules database schema migration</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> elixir/serviceradar_core/priv/repo/tenant_migrations/20260110054954_add_stateful_alert_rules.exs <ul><li>Creates <code>stateful_alert_rules</code> table with rule configuration (name, <br>signal, thresholds, windows, cooldown)<br> <li> Creates <code>stateful_alert_rule_states</code> table for tracking per-group rule <br>state and firing history<br> <li> Adds unique indexes on tenant_id+name for rules and <br>tenant_id+rule_id+group_key for states<br> <li> Includes timestamp fields for last_seen, last_fired, and <br>last_notification tracking</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-0f4b254a0546c5b2926a95e946ac30080b8fc11db180d64d6342dad1b97c66db">+85/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td> <details> <summary><strong>show.ex</strong><dd><code>Agent detail view enhanced with live registry and system metrics</code></dd></summary> <hr> web-ng/lib/serviceradar_web_ng_web/live/agent_live/show.ex <ul><li>Enhanced agent detail view to display both live Horde registry data <br>and database records with rich system information<br> <li> Added live agent status indicators, gateway node system metrics <br>(memory, processes, schedulers), and registration timeline<br> <li> Integrated service checks display with check type badges and status <br>indicators<br> <li> Replaced hardcoded agent type mappings with dynamic lookups via <br><code>ServiceRadar.Infrastructure.Agent</code> module<br> <li> Refactored component structure with new cards for capabilities, <br>gateway node info, registration timeline, and service checks</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-5e622205d1abddd8ad7dcf7a8ca1be583804d622d3d38b75140e9b909cf0534a">+501/-159</a></td> </tr> <tr> <td> <details> <summary><strong>index.ex</strong><dd><code>Job scheduler UI redesigned with search, filtering, and pagination</code></dd></summary> <hr> web-ng/lib/serviceradar_web_ng_web/live/admin/job_live/index.ex <ul><li>Completely redesigned job scheduler UI from card-based layout to <br>searchable, sortable table with pagination<br> <li> Added support for multiple job sources (Cron jobs and AshOban <br>triggers) with filtering and search capabilities<br> <li> Implemented auto-refresh functionality with configurable intervals and <br>manual trigger capability for jobs<br> <li> Added role-based access control for viewing platform jobs, triggering <br>jobs, and accessing Oban Web<br> <li> Replaced inline schedule editing with read-only job display and <br>navigation to detail pages</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-b275777f111009bbb4976d57623231aae2262452ef320a9f91ecbf202144115a">+610/-212</a></td> </tr> <tr> <td> <details> <summary><strong>stateful_alert_engine.ex</strong><dd><code>New stateful alert engine with bucketed event aggregation</code></dd></summary> <hr> elixir/serviceradar_core/lib/serviceradar/observability/stateful_alert_engine.ex <ul><li>New GenServer module implementing bucketed stateful alert evaluation <br>for log and event rules<br> <li> Supports time-windowed event aggregation with configurable bucket <br>sizes and cooldown periods<br> <li> Handles alert lifecycle (firing, recovery, renotification) with <br>history tracking and snapshot persistence<br> <li> Implements flexible rule matching for logs and events with support for <br>grouping, severity filtering, and attribute matching<br> <li> Integrates with Ash resources for rule management and state <br>persistence via ETS and database snapshots</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-bae3a52db882de8c947e62f219a95dff8db4e155e37d9a361dbe14ec25fcd3bd">+960/-0</a>&nbsp; </td> </tr> <tr> <td> <details> <summary><strong>gateway_process.ex</strong><dd><code>New gateway process for distributed check execution</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> elixir/serviceradar_core/lib/serviceradar/edge/gateway_process.ex <ul><li>New GenServer module representing an agent gateway in the ERTS cluster <br>for check execution and result aggregation<br> <li> Provides synchronous and asynchronous job execution APIs with health <br>checks and result retrieval<br> <li> Implements agent discovery and load balancing with domain-based and <br>partition-based selection strategies<br> <li> Tracks execution metrics (jobs executed, checks executed, average <br>execution time) and maintains registry heartbeats<br> <li> Handles communication between Core (via AshOban), Gateway, and Agent <br>processes for distributed check execution</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-b0b102ecc88366a0e9eea09de860b80929729880cd47f1aa96d6cfcb0ccc9f8f">+466/-0</a>&nbsp; </td> </tr> </table></details></td></tr><tr><td><strong>Additional files</strong></td><td><details><summary>101 files</summary><table> <tr> <td><strong>.bazelignore</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-a5641cd37d6ad98b32cdfce1980836cc68312277bc6a7052f55da02ada5bc6cf">+4/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>.bazelrc</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-544556920c45b42cbfe40159b082ce8af6bd929e492d076769226265f215832f">+5/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>.env-sample</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-c4368a972a7fa60d9c4e333cebf68cdb9a67acb810451125c02e3b7eb2594e3d">+33/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>.env.example</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-a3046da0d15a27e89f2afe639b25748a7ad4d9290af3e7b1b6c1a5533c8f0a8c">+38/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>main.yml</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-7829468e86c1cc5d5133195b5cb48e1ff6c75e3e9203777f6b2e379d9e4882b3">+18/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>sbom-images.yml</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-306f4aa8e8e286f727246a7517eecd45f3535fd99a644f60d635b9fa39875f54">+1/-3</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>web-lint.yml</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-9d090859e31fc574efb47cacf534d619a3e83d55e59da3be2484999b9055b1b2">+0/-60</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>AGENTS.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-a54ff182c7e8acf56acfd6e4b9c3ff41e2c41a31c9b211b2deb9df75d9a478f9">+177/-11</a></td> </tr> <tr> <td><strong>INSTALL.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-09b140a43ebfdd8dbec31ce72cafffd15164d2860fd390692a030bcb932b54a0">+19/-15</a>&nbsp; </td> </tr> <tr> <td><strong>MODULE.bazel</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-6136fc12446089c3db7360e923203dd114b6a1466252e71667c6791c20fe6bdc">+22/-2</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>Makefile</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-76ed074a9305c04054cdebb9e9aad2d818052b07091de1f20cad0bbac34ffb52">+58/-56</a>&nbsp; </td> </tr> <tr> <td><strong>README-Docker.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-9fd61d24482efe68c22d8d41e2a1dcc440f39195aa56e7a050f2abe598179efd">+17/-2</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>README.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5">+3/-3</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>ROADMAP.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-683343bdf93f55ed3cada86151abb8051282e1936e58d4e0a04beca95dff6e51">+1/-1</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>BUILD.bazel</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-884fa9353a5226345e44fbabea3300efc7a87dfbcde0b6a42521ca51823f1b68">+11/-38</a>&nbsp; </td> </tr> <tr> <td><strong>BUILD.bazel</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-0e80ea46aeb61a873324685edb96eae864c7a2004fbb7ee404b4ec951190ba10">+12/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>mix_release.bzl</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-86ec281f99363b6b6eb1f49e21d83b7eeca93a35b552b9f305fffc6855e38ccd">+141/-49</a></td> </tr> <tr> <td><strong>BUILD.bazel</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-143f8d1549d52f28906f19ce28e5568a5be474470ff103c2c1e63c3e6b08d670">+1/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>README.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-bfd308915d0cf522e7fc76600dee687617dc69165ab22502a1d219850c0c0860">+4/-4</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>config.json</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-5b1bc8fe77422534739bdd3a38dc20d2634a86c171265c34e1b5d0c5a61b6bab">+5/-6</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>main.go</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-61358711e980ccf505246fd3915f97cbd3a380e9b66f6fa5aad46749968c5ca3">+174/-74</a></td> </tr> <tr> <td><strong>build.rs</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-251e7a923f45f8f903e510d10f183366bda06d281c8ecc3669e1858256e2186d">+0/-1</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>monitoring.proto</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-b56f709f4a0a3db694f2124353908318631f23e20b7846bc4b8ee869e2e0632a">+3/-26</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>server.rs</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-bce0f4ca6548712f224b73816825d28e831acbbff7dbed3c98671ed50f65d028">+2/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>BUILD.bazel</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-7da152990199fd73c1eecb40f9c49e0d4e6453a8ec1acb111e445c55d1ca0af0">+1/-1</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>main.go</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-f25402eade63525184cb5e7437accff93c7b9338eebe81add6dc5f2a9eb12550">+1/-1</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>README.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-2e9751b437fa61442aac074c7a4a912d0ac50ac3ea156ac8aedd8478d21c6bdb">+2/-2</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>monitoring.proto</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-9faf6025eb0d3d38383f5b7ad2b733abeb38454d5e4de3e83994e94b12d87a50">+2/-26</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>server.rs</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-2c4395fee16396339c3eea518ad9bec739174c67c9cedf62e6848c17136dd33e">+6/-6</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>main.go</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-ed4d81d29a7267f93fd77e17993fd3491b9ef6ded18490b4514d10ed1d803bc2">+16/-2</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>Cargo.toml</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-fcf0c672917b64a5b953a914af013f16dddd6a1d813810236364e32f1ae70382">+0/-3</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>README.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-643d2c3959322902c5bc9a22666b1e9ef71fa0bb87c9451b0e4147a4d5b51987">+8/-8</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>config.rs</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-05038f3867985e757de9027609950e682bad6d1992dac6acd7c28962a3c65dc4">+85/-28</a>&nbsp; </td> </tr> <tr> <td><strong>grpc_server.rs</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-e4564a93f6cf84ff91cd3d8141fc9272ec9b4ec19defd107afa42be01fcfed5b">+2/-2</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>message_processor.rs</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-9fcbc5358a9009e60a8cd22d21e5a9ea652787c727732d0b869e0865495114c3">+2/-16</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>nats.rs</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-97f7335def0ad5d644b594a1076ae2d7080b11259cbb8de22c7946cc8e4b39f8">+4/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>zen-consumer-with-otel.json</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-68375f1f7847e1fbdf75664f6be65b1ad94ae6ce86ed73fc5964d65054668acb">+14/-11</a>&nbsp; </td> </tr> <tr> <td><strong>zen-consumer.json</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-4d308af9802a93a0f656e8c02a3b5fcd8991407bb18360f087470db74e1f9524">+14/-11</a>&nbsp; </td> </tr> <tr> <td><strong>.ko.yaml</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-c403e088cfbc9e150c604da6a056a188e7a5f5585c45cdb515460f000008b441">+0/-15</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>BUILD.bazel</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-cf437f055db002c5a1dba0ac4a4949d0ecc4b095e8e760bf5aec0f5d4c08f572">+0/-17</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>BUILD.bazel</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-5570e460b26d54e1c23df4652073efa22cfd37353237956b5ffc4f355bbb3346">+0/-24</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>app.go</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-4ad8a289575edf3b163088617b7a40ae1305c29ced0c7d59b3751c57d6938072">+0/-206</a>&nbsp; </td> </tr> <tr> <td><strong>config.json</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-2423ef78d36e905ae993b69ff59f5df6b2e1b9492fb0fa8c6d0aad7c76d2d229">+0/-165</a>&nbsp; </td> </tr> <tr> <td><strong>config.json</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-ef778d85ac6f9652c25cb0d631f0fe8dfb3edac4dde5d719a4fc2926fb5c3216">+0/-165</a>&nbsp; </td> </tr> <tr> <td><strong>main.go</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-4ab3fd1d4debc53dd2499d94a0f60c648fdae4235dd1e3678095a975f5bb434a">+0/-86</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>BUILD.bazel</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-c62c0139ebdb337369f4067567cd2c52b8e7decb3ddfabc77f9f67b2f6e5789c">+1/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>main.go</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-5e7731adfb877918cd65d9d5531621312496450fd550fea2682efca4ca8fe816">+68/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>README.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-0b0725713b87dca1de57200214a4fe04633f0d856c39aa8032280227bf8e8141">+3/-3</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>README.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-f425b4378f84e0ba0c6f532facff17ff5d55b4dc6033d8bf35130a159cd2ba32">+9/-12</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>flowgger.toml</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-af9f49f931e282dca53d1f0521b036d222fe671f77e61a876a84cf4c6d7cca4d">+2/-1</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>nats_output.rs</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-a82e2e4d413539bf0b414b5629665b19648447523994cba639c4d1238aa5a0c1">+14/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>otel.toml</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-c64b9ace832b8ea57a2be62f84166e03bb1904882635d444ec76a880cdf14cc0">+3/-1</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>otel.toml.example</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-c1889866f35f98cdba9cd229fc119273c5fa5fca501451db23813b575f6fec66">+5/-2</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>config.rs</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-abbaec651da3d6af96b482e0f77bb909b65dbe0cabd78b5803769cc9dab0a1b0">+21/-3</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>nats_output.rs</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-6b585ea3564a481174e04da1270e2e13edd4e2b980d02a2652d6d21e6d82a498">+22/-5</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>setup.rs</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-3891f667deb20fd26e296d3e2742c57378d3764fe1743118e612465ae360391f">+1/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>BUILD.bazel</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-e1f7c698e0e3a4e6afa971c1140e71cbf22593fbb19c81cb26b02c15c5dc46ec">+0/-25</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>config.json</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-9edc2486fff55fc399e0ac96dba5137948a7ea7285f5ef7846835355684b7ab5">+0/-111</a>&nbsp; </td> </tr> <tr> <td><strong>main.go</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-4b8ec845da50cd58d011e69f9d1c30530ee1968df26616b8768bb1fc03433bbe">+0/-138</a>&nbsp; </td> </tr> <tr> <td><strong>BUILD.bazel</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-4f5d2ea4260d490a0d6f28adde0b35eca8af77d22f3ee366a783946c53687619">+0/-25</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>config.json</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-bcac20d6b3cb81f0059e766839ba1ee59a885009249501b0ba1182ebb1daea25">+0/-77</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>main.go</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-78dc6bc53f1c760c66f43ff5f486bfe78a65bee8b2e0d4862293ec0892da2b29">+0/-123</a>&nbsp; </td> </tr> <tr> <td><strong>main.go</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-bc6eeb1b05bcb9179525e32fac1de9926b5823ec3504be546ab10c5c9740f544">+1/-1</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>README.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-9c32ee8446458b6fd2ae7fee52016f4b707a59978b67888cd5bee2804d934528">+3/-3</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>config.rs</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-c89b88ba4d2bf0a054d0ba69a672a92c30140b8d19503d67b980a218ffe3106d">+22/-1</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>main.rs</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-33b655d8730ae3e9c844ee280787d11f1b0d5343119188273f89558805f814ba">+23/-3</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>docker-compose.dev.yml</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-9542f82d64bbeebd91f6236324bfe199e9657e2cb1fd9779d5d6dcdcf9cd4de1">+23/-34</a>&nbsp; </td> </tr> <tr> <td><strong>docker-compose.elx.yml</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-9562070d7ad4a3e9b2d06567008cf35de1d96448d914b3b45bf6c36d97cdd914">+117/-0</a>&nbsp; </td> </tr> <tr> <td><strong>docker-compose.spiffe.yml</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-603fd9e7d40841d174f26b95d0cb0c9537430bf3f7a5da3ccbba4ea3d8ac66c9">+39/-192</a></td> </tr> <tr> <td><strong>docker-compose.yml</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-e45e45baeda1c1e73482975a664062aa56f20c03dd9d64a827aba57775bed0d3">+316/-269</a></td> </tr> <tr> <td><strong>README.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-da8fcbe728a9172b578e5d754f8e2df214c658c4321f610e63dd68bea828ab49">+6/-5</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>Dockerfile.agent-gateway</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-332bc81a932ae08efa711a71b60fe0954d99bf17ebdab00a3baaa177a44de8b0">+94/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>Dockerfile.core</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-34849cba9f0b40185bfcba10b1a076a87f4ab2d63e08d5a88fc932c60956df66">+0/-93</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>Dockerfile.core-elx</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-5ec7a971285669999af442a0c7f141c34f7fd9180257307f5c4ed12f789a2182">+108/-0</a>&nbsp; </td> </tr> <tr> <td><strong>Dockerfile.poller</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-d3ba129830fb366bfe23b00db4ef6218b10fc981d3c04842b1b3b3b367a8982f">+0/-70</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>Dockerfile.sync</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-0227933b9961fd553af1d229e89d71a0271fdc475081bbcef49b587941af1eda">+0/-95</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>Dockerfile.tools</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-0258db71e4070e342198965f1d046f3097640850b037df8a2287a7e239630add">+1/-2</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>Dockerfile.web</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-89393e743136e5cf7e1d67b378bc56da802452c481f2d63399bf72aac3d21e67">+0/-110</a>&nbsp; </td> </tr> <tr> <td><strong>Dockerfile.web-ng</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-92d43af1965575d56c3380ecc8a81024aac2ff36f039ec2d3839e9fc7852bc10">+6/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>agent-minimal.docker.json</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-1f09fad94636c90373af8e270f6ba0332ae4f4d1df50a4909729280a3a9691e6">+6/-6</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>agent.docker.json</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-5d33fe703515d03076d31261ecf946e9c6fc668cf5bf65099d49b670739e455e">+5/-20</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>agent.mtls.json</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-008f2216f159a9bd5db9cc90baaf6f1e64487df7af05b56ab3b9d6c4946aa95f">+7/-10</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>bootstrap-nested-spire.sh</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-ab4746a08fb1e0b307a1e47660cd22182e283a087cba87dcbff0fdfe750f44f1">+0/-80</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>.gitkeep</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-d72c41aab2d6f2c230a4340dfefe7917cdd12bed942c825aa0d4c9875a637bac">+1/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>datasvc.docker.json</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-3f2719d3dbfe042e8383739e3c78e74e5f851a44e5e46bea8e79c4b79fdcc34f">+3/-2</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>datasvc.mtls.json</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-3a45619e57f1e6e9a31486ec7fffb33ef246e271f82bac272ee0a946b88da70a">+14/-1</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>db-event-writer.docker.json</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-9fc51271f7ef5bb460160013e24e44e829b730656891d26fc49d5fe72fbb3147">+15/-11</a>&nbsp; </td> </tr> <tr> <td><strong>db-event-writer.mtls.json</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-7a33f95f7545499abf0ed9fc91b58499ab209639e4885019579c959583fc7496">+10/-8</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>FRICTION_POINTS.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-b0653c58880f810ba832c0500733d63de309db98b43009fe73a1862494cf41bd">+0/-355</a>&nbsp; </td> </tr> <tr> <td><strong>README.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-31849f033cfc932acee35f549c069abb1f36101c352e553dd6bff8713b29f98c">+0/-207</a>&nbsp; </td> </tr> <tr> <td><strong>SETUP_GUIDE.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-b4914f8640a78038e45f51235a624535672680dc902de5f107fc051f4f281913">+0/-307</a>&nbsp; </td> </tr> <tr> <td><strong>docker-compose.edge-e2e.yml</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-575d19ea771bdf8102cb9729db43a1bfd6afc2527160e54105beeac2e314f362">+0/-27</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>manage-packages.sh</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-3c2ff6febbddb956c71557894adaf7d0a39a1f20dda120fe126364946bc47280">+0/-211</a>&nbsp; </td> </tr> <tr> <td><strong>setup-edge-e2e.sh</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-2714e2c7e111f69ea9e9f5ddd7f6a70fa5ea96e3a53b851cb13b8b8b7cd12917">+0/-198</a>&nbsp; </td> </tr> <tr> <td><strong>edge-poller-restart.sh</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-96a8fe52c38fd0d7c14895127df34a27be311cac89c53d28ee178661b629bd22">+0/-178</a>&nbsp; </td> </tr> <tr> <td><strong>downstream-agent.conf</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-747de0375ced42af978ca7dac239862bdabb7f6bd0bd634f134b485517a7b4ee">+0/-32</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>env</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-686f1a954c542f2ec9bf14c3170648b65190ad242c7f3a95a0f872ae41b8b1c6">+0/-4</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>server.conf</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-025f5b5ab79526cf549ca1fdb90dd659ba76b438f05a7f77d916d18728c4b572">+0/-51</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>upstream-agent.conf</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-e8a869ddf4affa31536a8d4e4e6f09c40072a7026da2c609d93c6ecf04138902">+0/-32</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>entrypoint-certs.sh</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-83d6800b184a5233c66c69766286b0a60fece1bc64addb112d9f8dc019437f05">+13/-9</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>Additional files not shown</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2246/files#diff-2f328e4cd8dbe3ad193e49d92bcf045f47a6b72b1e9487d366f6b8288589b4ca"></a></td> </tr> </table></details></td></tr></tbody></table> </details> ___
qodo-code-review[bot] commented 2026-01-11 18:03:18 +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/2246#issuecomment-3735211678
Original created: 2026-01-11T18:03:18Z

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Authorization bypass

Description: load_tenant/1 calls Ash.get(Tenant, tenant_id, authorize?: false), which can bypass
authorization checks and may allow cross-tenant data access if an attacker can influence
tenant_id (directly or indirectly).
index.ex [994-998]

Referred Code
defp load_tenant(tenant_id) do
  case Ash.get(Tenant, tenant_id, authorize?: false) do
    {:ok, %Tenant{} = tenant} -> tenant
    _ -> nil
  end

Denial of service

Description: Converting user-controlled component_type with String.to_existing_atom/1 can raise on
unexpected values and crash the LiveView process, creating a realistic denial-of-service
vector via repeated invalid submissions.
index.ex [952-961]

Referred Code
transform_params: fn _form, params, _action ->
  # Convert component_type string to atom if needed
  params =
    case params["component_type"] do
      type when is_binary(type) and type != "" ->
        Map.put(params, "component_type", String.to_existing_atom(type))

      _ ->
        params
    end

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: Robust Error Handling and Edge Case Management

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

Status:
Unsafe atom conversion: User-supplied component_type is converted using String.to_existing_atom/1 without a
whitelist, which can raise and crash the LiveView for unexpected inputs.

Referred Code
# Convert component_type string to atom if needed
params =
  case params["component_type"] do
    type when is_binary(type) and type != "" ->
      Map.put(params, "component_type", String.to_existing_atom(type))

    _ ->
      params
  end

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:
Detailed error exposed: The UI displays internal error details to the end-user via put_flash(:error, "Failed
to create package: #{error_msg}"), which may leak implementation/system information.

Referred Code
  {:error, error} ->
    error_msg = format_error(error)

    {:noreply,
     socket
     |> assign(:creating, false)
     |> put_flash(:error, "Failed to create package: #{error_msg}")}
end

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

Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
- Requires Further Human Verification
🏷️ - Compliance label
Imported GitHub PR comment. Original author: @qodo-code-review[bot] Original URL: https://github.com/carverauto/serviceradar/pull/2246#issuecomment-3735211678 Original created: 2026-01-11T18:03:18Z --- ## PR Compliance Guide 🔍 <!-- https://github.com/carverauto/serviceradar/commit/a4ee0a868a53737667baa8ab1b5ff4d4f32392ca --> 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>Authorization bypass </strong></summary><br> <b>Description:</b> <code>load_tenant/1</code> calls <code>Ash.get(Tenant, tenant_id, authorize?: false)</code>, which can bypass <br>authorization checks and may allow cross-tenant data access if an attacker can influence <br><code>tenant_id</code> (directly or indirectly).<br> <strong><a href='https://github.com/carverauto/serviceradar/pull/2246/files#diff-d0d22a987aa942ef25088696daea862e36bd8ca083ce29e4670f63e62947c62fR994-R998'>index.ex [994-998]</a></strong><br> <details open><summary>Referred Code</summary> ```elixir defp load_tenant(tenant_id) do case Ash.get(Tenant, tenant_id, authorize?: false) do {:ok, %Tenant{} = tenant} -> tenant _ -> nil end ``` </details></details></td></tr> <tr><td><details><summary><strong>Denial of service </strong></summary><br> <b>Description:</b> Converting user-controlled <code>component_type</code> with <code>String.to_existing_atom/1</code> can raise on <br>unexpected values and crash the LiveView process, creating a realistic denial-of-service <br>vector via repeated invalid submissions.<br> <strong><a href='https://github.com/carverauto/serviceradar/pull/2246/files#diff-d0d22a987aa942ef25088696daea862e36bd8ca083ce29e4670f63e62947c62fR952-R961'>index.ex [952-961]</a></strong><br> <details open><summary>Referred Code</summary> ```elixir transform_params: fn _form, params, _action -> # Convert component_type string to atom if needed params = case params["component_type"] do type when is_binary(type) and type != "" -> Map.put(params, "component_type", String.to_existing_atom(type)) _ -> params end ``` </details></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=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=2>🔴</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/2246/files#diff-d0d22a987aa942ef25088696daea862e36bd8ca083ce29e4670f63e62947c62fR953-R961'><strong>Unsafe atom conversion</strong></a>: User-supplied <code>component_type</code> is converted using <code>String.to_existing_atom/1</code> without a <br>whitelist, which can raise and crash the LiveView for unexpected inputs.<br> <details open><summary>Referred Code</summary> ```elixir # Convert component_type string to atom if needed params = case params["component_type"] do type when is_binary(type) and type != "" -> Map.put(params, "component_type", String.to_existing_atom(type)) _ -> params 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: 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/2246/files#diff-d0d22a987aa942ef25088696daea862e36bd8ca083ce29e4670f63e62947c62fR171-R178'><strong>Detailed error exposed</strong></a>: The UI displays internal error details to the end-user via <code>put_flash(:error, &quot;Failed </code><br><code>to create package: #{error_msg}&quot;)</code>, which may leak implementation/system information.<br> <details open><summary>Referred Code</summary> ```elixir {:error, error} -> error_msg = format_error(error) {:noreply, socket |> assign(:creating, false) |> put_flash(:error, "Failed to create package: #{error_msg}")} 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=3>⚪</td> <tr><td align="center" colspan="2"> <!-- placeholder --> <!-- /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>
qodo-code-review[bot] commented 2026-01-11 18:04:33 +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/2246#issuecomment-3735215686
Original created: 2026-01-11T18:04:33Z

PR Code Suggestions

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
General
Whitelist sortable fields

In handle_event("sort", ...) replace String.to_existing_atom/1 with a whitelist
of allowed sort fields to prevent potential crashes from unsafe atom creation.

web-ng/lib/serviceradar_web_ng_web/live/admin/job_live/index.ex [83-102]

+@sortable_fields %{
+  "name" => :name,
+  "source" => :source,
+  "cron" => :cron,
+  "last_run_at" => :last_run_at,
+  "next_run_at" => :next_run_at
+}
+
 def handle_event("sort", %{"field" => field}, socket) do
-  field_atom = String.to_existing_atom(field)
+  field_atom = Map.get(@sortable_fields, field, socket.assigns.sort_by)
   current_sort = socket.assigns.sort_by
   current_dir = socket.assigns.sort_dir
 
-  {new_sort, new_dir} =
+  new_dir =
     if current_sort == field_atom do
-      # Toggle direction
-      {field_atom, if(current_dir == :asc, do: :desc, else: :asc)}
+      if(current_dir == :asc, do: :desc, else: :asc)
     else
-      # New field, default to asc
-      {field_atom, :asc}
+      :asc
     end
 
   {:noreply,
    socket
-   |> assign(:sort_by, new_sort)
+   |> assign(:sort_by, field_atom)
    |> assign(:sort_dir, new_dir)
    |> load_jobs()}
 end

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 9

__

Why: The suggestion addresses a potential denial-of-service vulnerability by replacing String.to_existing_atom/1 with a secure whitelisting approach for user-provided sort fields.

High
Normalize data to use consistent keys

In source_queries_to_form/1, normalize map keys to strings to handle
inconsistent data structures and improve code robustness, instead of using
fallbacks for both atom and string keys.

web-ng/lib/serviceradar_web_ng_web/live/admin/integration_live/index.ex [136-145]

 defp source_queries_to_form(queries) when is_list(queries) do
   Enum.map(queries, fn q ->
+    # Ensure keys are strings for consistency in the form
+    string_keyed_q =
+      if is_map(q) do
+        for {key, val} <- q, into: %{}, do: {to_string(key), val}
+      else
+        %{}
+      end
+
     %{
       "id" => System.unique_integer([:positive]),
-      "label" => q["label"] || Map.get(q, :label, ""),
-      "query" => q["query"] || Map.get(q, :query, ""),
-      "sweep_modes" => q["sweep_modes"] || Map.get(q, :sweep_modes, [])
+      "label" => Map.get(string_keyed_q, "label", ""),
+      "query" => Map.get(string_keyed_q, "query", ""),
+      "sweep_modes" => Map.get(string_keyed_q, "sweep_modes", [])
     }
   end)
 end

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 6

__

Why: The suggestion correctly points out that handling both atom and string keys can mask data consistency issues. Normalizing keys to a single type improves code clarity and maintainability, making it a valuable refactoring.

Low
Avoid broad exception swallowing for robustness

In get_leader_node/0, replace the broad try/rescue with more specific pattern
matching on the return value of :rpc.call to handle and log expected errors
gracefully.

web-ng/lib/serviceradar_web_ng_web/live/admin/job_live/index.ex [655-670]

 defp get_leader_node do
-  try do
-    case ServiceRadar.Cluster.ClusterStatus.find_coordinator() do
-      nil ->
-        nil
+  case ServiceRadar.Cluster.ClusterStatus.find_coordinator() do
+    nil ->
+      nil
 
-      coordinator_node ->
-        case :rpc.call(coordinator_node, Oban.Peer, :get_leader, []) do
-          leader when is_binary(leader) -> leader
-          _ -> nil
-        end
-    end
-  rescue
-    _ -> nil
+    coordinator_node ->
+      case :rpc.call(coordinator_node, Oban.Peer, :get_leader, []) do
+        leader when is_binary(leader) ->
+          leader
+
+        {:badrpc, reason} ->
+          Logger.warning("RPC call to get Oban leader failed: #{inspect(reason)}")
+          nil
+
+        _ ->
+          nil
+      end
   end
 end

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies the anti-pattern of swallowing all exceptions and proposes a more robust error handling strategy, which improves maintainability and debugging.

Low
Remove redundant unique indexes on primary keys

Remove redundant unique indexes on primary key columns for tables like
ocsf_devices, ocsf_agents, and others, as PostgreSQL automatically creates them.

elixir/serviceradar_core/priv/repo/tenant_migrations/20260107043446_initial_schema.exs [797]

 create table(:ocsf_devices, primary_key: false, prefix: prefix()) do
   add :uid, :text, null: false, primary_key: true
   ...
 end
-...
-create unique_index(:ocsf_devices, [:uid], name: "ocsf_devices_unique_uid_index")

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 5

__

Why: The suggestion correctly identifies that creating a unique index on a primary key column is redundant. Applying this change improves schema clarity and removes unnecessary database objects, which is a good practice.

Low
Avoid using exceptions for flow control

In call/2, replace the try/catch block with GenServer.whereis/1 to check if the
process is alive before making the GenServer.call, avoiding exceptions for flow
control.

elixir/serviceradar_core/lib/serviceradar/observability/stateful_alert_engine.ex [96-101]

 defp call(tenant_id, message) do
-  GenServer.call(via_tuple(tenant_id), message, :timer.seconds(15))
-catch
-  :exit, {:noproc, _} ->
-    {:error, :engine_not_running}
+  case GenServer.whereis(via_tuple(tenant_id)) do
+    nil ->
+      {:error, :engine_not_running}
+
+    pid when is_pid(pid) ->
+      GenServer.call(pid, message, :timer.seconds(15))
+  end
 end

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 5

__

Why: The suggestion replaces non-idiomatic exception-based flow control with a standard check using GenServer.whereis/1, improving code clarity and adherence to Elixir best practices.

Low
Possible issue
Add prefix to indexes

Add prefix: prefix() to all create index and create unique_index calls to ensure
they are created within the correct tenant-specific schema.

elixir/serviceradar_core/priv/repo/tenant_migrations/20260107043446_initial_schema.exs [60]

-create index(:tenant_cas, [:spki_sha256])
+create index(:tenant_cas, [:spki_sha256], prefix: prefix())

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 9

__

Why: This suggestion is critical for the correctness of the multi-tenant architecture. Without prefix: prefix(), indexes would be created in the public schema, leading to incorrect behavior and potential data integrity issues.

High
Use safe parsing for user input

Replace String.to_integer/1 with the safer Integer.parse/1 in the remove_query
event handler to prevent crashes from invalid user input.

web-ng/lib/serviceradar_web_ng_web/live/admin/integration_live/index.ex [224-230]

 def handle_event("remove_query", %{"id" => id}, socket) do
-  id = String.to_integer(id)
-  queries = Enum.reject(socket.assigns.form_queries, &(&1["id"] == id))
-  # Ensure at least one query remains
-  queries = if queries == [], do: [default_query()], else: queries
-  {:noreply, assign(socket, :form_queries, queries)}
+  case Integer.parse(id) do
+    {id, ""} ->
+      queries = Enum.reject(socket.assigns.form_queries, &(&1["id"] == id))
+      # Ensure at least one query remains
+      queries = if queries == [], do: [default_query()], else: queries
+      {:noreply, assign(socket, :form_queries, queries)}
+
+    _ ->
+      # Ignore invalid ID
+      {:noreply, socket}
+  end
 end

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a potential crash in the LiveView process due to unsafe string-to-integer conversion of user-provided input. Using Integer.parse/1 for safe parsing is a critical improvement for robustness and error handling.

Medium
Enable pgcrypto extension

Add execute("CREATE EXTENSION IF NOT EXISTS pgcrypto;") at the beginning of the
up function to ensure the pgcrypto extension is available for gen_random_uuid().

elixir/serviceradar_core/priv/repo/tenant_migrations/20260107043446_initial_schema.exs [10]

 def up do
+  execute("CREATE EXTENSION IF NOT EXISTS pgcrypto;")

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that the pgcrypto extension is a prerequisite for using gen_random_uuid(). Adding this command ensures the migration is self-contained and will not fail in a fresh database environment.

Medium
Use asynchronous RPC calls for performance

Refactor fetch_node_info/1 to use rpc.async_call/4 and rpc.yield_many/2 for
concurrent, non-blocking RPC calls to improve performance.

web-ng/lib/serviceradar_web_ng_web/live/agent_live/show.ex [164-189]

 defp fetch_node_info(node) when is_atom(node) do
-  try do
-    memory = :rpc.call(node, :erlang, :memory, [], 5000)
-    {uptime_ms, _} = :rpc.call(node, :erlang, :statistics, [:wall_clock], 5000)
+  requests = [
+    {:memory, {:erlang, :memory, []}},
+    {:statistics, {:erlang, :statistics, [:wall_clock]}},
+    {:process_count, {:erlang, :system_info, [:process_count]}},
+    {:port_count, {:erlang, :system_info, [:port_count]}},
+    {:otp_release, {:erlang, :system_info, [:otp_release]}},
+    {:schedulers, {:erlang, :system_info, [:schedulers]}},
+    {:schedulers_online, {:erlang, :system_info, [:schedulers_online]}}
+  ]
+
+  keys =
+    Enum.map(requests, fn {key, {m, f, a}} ->
+      :rpc.async_call(node, m, f, a)
+    end)
+
+  results = :rpc.yield_many(keys, 5000)
+
+  # Combine results into a map
+  info =
+    Enum.zip(requests, results)
+    |> Enum.reduce(%{}, fn {{key, _}, result}, acc ->
+      Map.put(acc, key, result)
+    end)
+
+  # Process results
+  if info[:memory] && info[:statistics] do
+    {uptime_ms, _} = info[:statistics]
 
     %{
-      process_count: :rpc.call(node, :erlang, :system_info, [:process_count], 5000),
-      port_count: :rpc.call(node, :erlang, :system_info, [:port_count], 5000),
-      otp_release: to_string(:rpc.call(node, :erlang, :system_info, [:otp_release], 5000)),
-      schedulers: :rpc.call(node, :erlang, :system_info, [:schedulers], 5000),
-      schedulers_online: :rpc.call(node, :erlang, :system_info, [:schedulers_online], 5000),
+      process_count: info[:process_count],
+      port_count: info[:port_count],
+      otp_release: to_string(info[:otp_release]),
+      schedulers: info[:schedulers],
+      schedulers_online: info[:schedulers_online],
       uptime_ms: uptime_ms,
-      memory_total: memory[:total],
-      memory_processes: memory[:processes],
-      memory_system: memory[:system],
-      memory_atom: memory[:atom],
-      memory_binary: memory[:binary],
-      memory_code: memory[:code],
-      memory_ets: memory[:ets]
+      memory_total: info[:memory][:total],
+      memory_processes: info[:memory][:processes],
+      memory_system: info[:memory][:system],
+      memory_atom: info[:memory][:atom],
+      memory_binary: info[:memory][:binary],
+      memory_code: info[:memory][:code],
+      memory_ets: info[:memory][:ets]
     }
-  rescue
-    _ -> nil
-  catch
-    :exit, _ -> nil
+  else
+    nil
   end
 end

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a performance bottleneck from sequential RPC calls and proposes a valid asynchronous alternative, which improves UI responsiveness.

Medium
Security
Prevent atom exhaustion security vulnerability

To prevent a potential atom exhaustion vulnerability, validate the
component_type against a whitelist of allowed values before converting the
string to an atom.

web-ng/lib/serviceradar_web_ng_web/live/admin/edge_package_live/index.ex [948-961]

     defp build_create_form(tenant, security_mode) do
       AshPhoenix.Form.for_create(OnboardingPackage, :create,
         domain: ServiceRadar.Edge,
         tenant: tenant,
         transform_params: fn _form, params, _action ->
           # Convert component_type string to atom if needed
           params =
             case params["component_type"] do
-              type when is_binary(type) and type != "" ->
-                Map.put(params, "component_type", String.to_existing_atom(type))
+              type when type in ["gateway", "agent", "checker", "sync"] ->
+                Map.put(params, "component_type", String.to_atom(type))
 
               _ ->
                 params
             end
 ...

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a potential atom exhaustion DoS vulnerability and provides a robust fix by whitelisting user input before atom conversion.

High
Guard tenant when listing packages

To prevent unintended data access, ensure that OnboardingPackages.list is only
called with a tenant when one is present; otherwise, return an empty list.

web-ng/lib/serviceradar_web_ng_web/live/admin/edge_package_live/index.ex [20-26]

 def mount(_params, _session, socket) do
   tenant = get_tenant(socket)
+  package_list =
+    if tenant do
+      OnboardingPackages.list(%{limit: 50}, tenant: tenant)
+    else
+      # no active tenant: restrict or return empty list
+      []
+    end
   socket =
     socket
     |> assign(:page_title, "Edge Onboarding")
-    |> assign(:packages, OnboardingPackages.list(%{limit: 50}, tenant: tenant))
+    |> assign(:packages, package_list)
     ...

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly points out a potential issue where a nil tenant might bypass tenancy scoping, and proposes a safer default of returning an empty list.

Medium
  • More
Imported GitHub PR comment. Original author: @qodo-code-review[bot] Original URL: https://github.com/carverauto/serviceradar/pull/2246#issuecomment-3735215686 Original created: 2026-01-11T18:04:33Z --- ## PR Code Suggestions ✨ <!-- a4ee0a8 --> 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=5>General</td> <td> <details><summary>Whitelist sortable fields</summary> ___ **In <code>handle_event("sort", ...)</code> replace <code>String.to_existing_atom/1</code> with a whitelist <br>of allowed sort fields to prevent potential crashes from unsafe atom creation.** [web-ng/lib/serviceradar_web_ng_web/live/admin/job_live/index.ex [83-102]](https://github.com/carverauto/serviceradar/pull/2246/files#diff-b275777f111009bbb4976d57623231aae2262452ef320a9f91ecbf202144115aR83-R102) ```diff +@sortable_fields %{ + "name" => :name, + "source" => :source, + "cron" => :cron, + "last_run_at" => :last_run_at, + "next_run_at" => :next_run_at +} + def handle_event("sort", %{"field" => field}, socket) do - field_atom = String.to_existing_atom(field) + field_atom = Map.get(@sortable_fields, field, socket.assigns.sort_by) current_sort = socket.assigns.sort_by current_dir = socket.assigns.sort_dir - {new_sort, new_dir} = + new_dir = if current_sort == field_atom do - # Toggle direction - {field_atom, if(current_dir == :asc, do: :desc, else: :asc)} + if(current_dir == :asc, do: :desc, else: :asc) else - # New field, default to asc - {field_atom, :asc} + :asc end {:noreply, socket - |> assign(:sort_by, new_sort) + |> assign(:sort_by, field_atom) |> assign(:sort_dir, new_dir) |> load_jobs()} end ``` `[To ensure code accuracy, apply this suggestion manually]` <details><summary>Suggestion importance[1-10]: 9</summary> __ Why: The suggestion addresses a potential denial-of-service vulnerability by replacing `String.to_existing_atom/1` with a secure whitelisting approach for user-provided sort fields. </details></details></td><td align=center>High </td></tr><tr><td> <details><summary>Normalize data to use consistent keys</summary> ___ **In <code>source_queries_to_form/1</code>, normalize map keys to strings to handle <br>inconsistent data structures and improve code robustness, instead of using <br>fallbacks for both atom and string keys.** [web-ng/lib/serviceradar_web_ng_web/live/admin/integration_live/index.ex [136-145]](https://github.com/carverauto/serviceradar/pull/2246/files#diff-61d0262af13a42905ebbd793e83537e61bfc09493df1664a82eb2536980ee1cdR136-R145) ```diff defp source_queries_to_form(queries) when is_list(queries) do Enum.map(queries, fn q -> + # Ensure keys are strings for consistency in the form + string_keyed_q = + if is_map(q) do + for {key, val} <- q, into: %{}, do: {to_string(key), val} + else + %{} + end + %{ "id" => System.unique_integer([:positive]), - "label" => q["label"] || Map.get(q, :label, ""), - "query" => q["query"] || Map.get(q, :query, ""), - "sweep_modes" => q["sweep_modes"] || Map.get(q, :sweep_modes, []) + "label" => Map.get(string_keyed_q, "label", ""), + "query" => Map.get(string_keyed_q, "query", ""), + "sweep_modes" => Map.get(string_keyed_q, "sweep_modes", []) } end) end ``` `[To ensure code accuracy, apply this suggestion manually]` <details><summary>Suggestion importance[1-10]: 6</summary> __ Why: The suggestion correctly points out that handling both atom and string keys can mask data consistency issues. Normalizing keys to a single type improves code clarity and maintainability, making it a valuable refactoring. </details></details></td><td align=center>Low </td></tr><tr><td> <details><summary>Avoid broad exception swallowing for robustness</summary> ___ **In <code>get_leader_node/0</code>, replace the broad <code>try/rescue</code> with more specific pattern <br>matching on the return value of <code>:rpc.call</code> to handle and log expected errors <br>gracefully.** [web-ng/lib/serviceradar_web_ng_web/live/admin/job_live/index.ex [655-670]](https://github.com/carverauto/serviceradar/pull/2246/files#diff-b275777f111009bbb4976d57623231aae2262452ef320a9f91ecbf202144115aR655-R670) ```diff defp get_leader_node do - try do - case ServiceRadar.Cluster.ClusterStatus.find_coordinator() do - nil -> - nil + case ServiceRadar.Cluster.ClusterStatus.find_coordinator() do + nil -> + nil - coordinator_node -> - case :rpc.call(coordinator_node, Oban.Peer, :get_leader, []) do - leader when is_binary(leader) -> leader - _ -> nil - end - end - rescue - _ -> nil + coordinator_node -> + case :rpc.call(coordinator_node, Oban.Peer, :get_leader, []) do + leader when is_binary(leader) -> + leader + + {:badrpc, reason} -> + Logger.warning("RPC call to get Oban leader failed: #{inspect(reason)}") + nil + + _ -> + nil + end end end ``` `[To ensure code accuracy, apply this suggestion manually]` <details><summary>Suggestion importance[1-10]: 6</summary> __ Why: The suggestion correctly identifies the anti-pattern of swallowing all exceptions and proposes a more robust error handling strategy, which improves maintainability and debugging. </details></details></td><td align=center>Low </td></tr><tr><td> <details><summary>Remove redundant unique indexes on primary keys</summary> ___ **Remove redundant unique indexes on primary key columns for tables like <br><code>ocsf_devices</code>, <code>ocsf_agents</code>, and others, as PostgreSQL automatically creates them.** [elixir/serviceradar_core/priv/repo/tenant_migrations/20260107043446_initial_schema.exs [797]](https://github.com/carverauto/serviceradar/pull/2246/files#diff-0d217dc9822fab0d3390e8ec21040f98e67106e5c9126e043a9b701efcbfb576R797-R797) ```diff create table(:ocsf_devices, primary_key: false, prefix: prefix()) do add :uid, :text, null: false, primary_key: true ... end -... -create unique_index(:ocsf_devices, [:uid], name: "ocsf_devices_unique_uid_index") ``` `[To ensure code accuracy, apply this suggestion manually]` <details><summary>Suggestion importance[1-10]: 5</summary> __ Why: The suggestion correctly identifies that creating a unique index on a primary key column is redundant. Applying this change improves schema clarity and removes unnecessary database objects, which is a good practice. </details></details></td><td align=center>Low </td></tr><tr><td> <details><summary>Avoid using exceptions for flow control</summary> ___ **In <code>call/2</code>, replace the <code>try/catch</code> block with <code>GenServer.whereis/1</code> to check if the <br>process is alive before making the <code>GenServer.call</code>, avoiding exceptions for flow <br>control.** [elixir/serviceradar_core/lib/serviceradar/observability/stateful_alert_engine.ex [96-101]](https://github.com/carverauto/serviceradar/pull/2246/files#diff-bae3a52db882de8c947e62f219a95dff8db4e155e37d9a361dbe14ec25fcd3bdR96-R101) ```diff defp call(tenant_id, message) do - GenServer.call(via_tuple(tenant_id), message, :timer.seconds(15)) -catch - :exit, {:noproc, _} -> - {:error, :engine_not_running} + case GenServer.whereis(via_tuple(tenant_id)) do + nil -> + {:error, :engine_not_running} + + pid when is_pid(pid) -> + GenServer.call(pid, message, :timer.seconds(15)) + end end ``` `[To ensure code accuracy, apply this suggestion manually]` <details><summary>Suggestion importance[1-10]: 5</summary> __ Why: The suggestion replaces non-idiomatic exception-based flow control with a standard check using `GenServer.whereis/1`, improving code clarity and adherence to Elixir best practices. </details></details></td><td align=center>Low </td></tr><tr><td rowspan=4>Possible issue</td> <td> <details><summary>Add prefix to indexes</summary> ___ **Add <code>prefix: prefix()</code> to all <code>create index</code> and <code>create unique_index</code> calls to ensure <br>they are created within the correct tenant-specific schema.** [elixir/serviceradar_core/priv/repo/tenant_migrations/20260107043446_initial_schema.exs [60]](https://github.com/carverauto/serviceradar/pull/2246/files#diff-0d217dc9822fab0d3390e8ec21040f98e67106e5c9126e043a9b701efcbfb576R60-R60) ```diff -create index(:tenant_cas, [:spki_sha256]) +create index(:tenant_cas, [:spki_sha256], prefix: prefix()) ``` `[To ensure code accuracy, apply this suggestion manually]` <details><summary>Suggestion importance[1-10]: 9</summary> __ Why: This suggestion is critical for the correctness of the multi-tenant architecture. Without `prefix: prefix()`, indexes would be created in the public schema, leading to incorrect behavior and potential data integrity issues. </details></details></td><td align=center>High </td></tr><tr><td> <details><summary>Use safe parsing for user input</summary> ___ **Replace <code>String.to_integer/1</code> with the safer <code>Integer.parse/1</code> in the <code>remove_query</code> <br>event handler to prevent crashes from invalid user input.** [web-ng/lib/serviceradar_web_ng_web/live/admin/integration_live/index.ex [224-230]](https://github.com/carverauto/serviceradar/pull/2246/files#diff-61d0262af13a42905ebbd793e83537e61bfc09493df1664a82eb2536980ee1cdR224-R230) ```diff def handle_event("remove_query", %{"id" => id}, socket) do - id = String.to_integer(id) - queries = Enum.reject(socket.assigns.form_queries, &(&1["id"] == id)) - # Ensure at least one query remains - queries = if queries == [], do: [default_query()], else: queries - {:noreply, assign(socket, :form_queries, queries)} + case Integer.parse(id) do + {id, ""} -> + queries = Enum.reject(socket.assigns.form_queries, &(&1["id"] == id)) + # Ensure at least one query remains + queries = if queries == [], do: [default_query()], else: queries + {:noreply, assign(socket, :form_queries, queries)} + + _ -> + # Ignore invalid ID + {:noreply, socket} + end end ``` `[To ensure code accuracy, apply this suggestion manually]` <details><summary>Suggestion importance[1-10]: 8</summary> __ Why: The suggestion correctly identifies a potential crash in the LiveView process due to unsafe string-to-integer conversion of user-provided input. Using `Integer.parse/1` for safe parsing is a critical improvement for robustness and error handling. </details></details></td><td align=center>Medium </td></tr><tr><td> <details><summary>Enable pgcrypto extension</summary> ___ **Add <code>execute("CREATE EXTENSION IF NOT EXISTS pgcrypto;")</code> at the beginning of the <br><code>up</code> function to ensure the <code>pgcrypto</code> extension is available for <code>gen_random_uuid()</code>.** [elixir/serviceradar_core/priv/repo/tenant_migrations/20260107043446_initial_schema.exs [10]](https://github.com/carverauto/serviceradar/pull/2246/files#diff-0d217dc9822fab0d3390e8ec21040f98e67106e5c9126e043a9b701efcbfb576R10-R10) ```diff def up do + execute("CREATE EXTENSION IF NOT EXISTS pgcrypto;") ``` `[To ensure code accuracy, apply this suggestion manually]` <details><summary>Suggestion importance[1-10]: 7</summary> __ Why: The suggestion correctly identifies that the `pgcrypto` extension is a prerequisite for using `gen_random_uuid()`. Adding this command ensures the migration is self-contained and will not fail in a fresh database environment. </details></details></td><td align=center>Medium </td></tr><tr><td> <details><summary>Use asynchronous RPC calls for performance</summary> ___ **Refactor <code>fetch_node_info/1</code> to use <code>rpc.async_call/4</code> and <code>rpc.yield_many/2</code> for <br>concurrent, non-blocking RPC calls to improve performance.** [web-ng/lib/serviceradar_web_ng_web/live/agent_live/show.ex [164-189]](https://github.com/carverauto/serviceradar/pull/2246/files#diff-5e622205d1abddd8ad7dcf7a8ca1be583804d622d3d38b75140e9b909cf0534aR164-R189) ```diff defp fetch_node_info(node) when is_atom(node) do - try do - memory = :rpc.call(node, :erlang, :memory, [], 5000) - {uptime_ms, _} = :rpc.call(node, :erlang, :statistics, [:wall_clock], 5000) + requests = [ + {:memory, {:erlang, :memory, []}}, + {:statistics, {:erlang, :statistics, [:wall_clock]}}, + {:process_count, {:erlang, :system_info, [:process_count]}}, + {:port_count, {:erlang, :system_info, [:port_count]}}, + {:otp_release, {:erlang, :system_info, [:otp_release]}}, + {:schedulers, {:erlang, :system_info, [:schedulers]}}, + {:schedulers_online, {:erlang, :system_info, [:schedulers_online]}} + ] + + keys = + Enum.map(requests, fn {key, {m, f, a}} -> + :rpc.async_call(node, m, f, a) + end) + + results = :rpc.yield_many(keys, 5000) + + # Combine results into a map + info = + Enum.zip(requests, results) + |> Enum.reduce(%{}, fn {{key, _}, result}, acc -> + Map.put(acc, key, result) + end) + + # Process results + if info[:memory] && info[:statistics] do + {uptime_ms, _} = info[:statistics] %{ - process_count: :rpc.call(node, :erlang, :system_info, [:process_count], 5000), - port_count: :rpc.call(node, :erlang, :system_info, [:port_count], 5000), - otp_release: to_string(:rpc.call(node, :erlang, :system_info, [:otp_release], 5000)), - schedulers: :rpc.call(node, :erlang, :system_info, [:schedulers], 5000), - schedulers_online: :rpc.call(node, :erlang, :system_info, [:schedulers_online], 5000), + process_count: info[:process_count], + port_count: info[:port_count], + otp_release: to_string(info[:otp_release]), + schedulers: info[:schedulers], + schedulers_online: info[:schedulers_online], uptime_ms: uptime_ms, - memory_total: memory[:total], - memory_processes: memory[:processes], - memory_system: memory[:system], - memory_atom: memory[:atom], - memory_binary: memory[:binary], - memory_code: memory[:code], - memory_ets: memory[:ets] + memory_total: info[:memory][:total], + memory_processes: info[:memory][:processes], + memory_system: info[:memory][:system], + memory_atom: info[:memory][:atom], + memory_binary: info[:memory][:binary], + memory_code: info[:memory][:code], + memory_ets: info[:memory][:ets] } - rescue - _ -> nil - catch - :exit, _ -> nil + else + nil end end ``` `[To ensure code accuracy, apply this suggestion manually]` <details><summary>Suggestion importance[1-10]: 7</summary> __ Why: The suggestion correctly identifies a performance bottleneck from sequential RPC calls and proposes a valid asynchronous alternative, which improves UI responsiveness. </details></details></td><td align=center>Medium </td></tr><tr><td rowspan=2>Security</td> <td> <details><summary>Prevent atom exhaustion security vulnerability</summary> ___ **To prevent a potential atom exhaustion vulnerability, validate the <br><code>component_type</code> against a whitelist of allowed values before converting the <br>string to an atom.** [web-ng/lib/serviceradar_web_ng_web/live/admin/edge_package_live/index.ex [948-961]](https://github.com/carverauto/serviceradar/pull/2246/files#diff-d0d22a987aa942ef25088696daea862e36bd8ca083ce29e4670f63e62947c62fR948-R961) ```diff defp build_create_form(tenant, security_mode) do AshPhoenix.Form.for_create(OnboardingPackage, :create, domain: ServiceRadar.Edge, tenant: tenant, transform_params: fn _form, params, _action -> # Convert component_type string to atom if needed params = case params["component_type"] do - type when is_binary(type) and type != "" -> - Map.put(params, "component_type", String.to_existing_atom(type)) + type when type in ["gateway", "agent", "checker", "sync"] -> + Map.put(params, "component_type", String.to_atom(type)) _ -> params end ... ``` `[To ensure code accuracy, apply this suggestion manually]` <details><summary>Suggestion importance[1-10]: 9</summary> __ Why: The suggestion correctly identifies a potential atom exhaustion DoS vulnerability and provides a robust fix by whitelisting user input before atom conversion. </details></details></td><td align=center>High </td></tr><tr><td> <details><summary>Guard tenant when listing packages</summary> ___ **To prevent unintended data access, ensure that <code>OnboardingPackages.list</code> is only <br>called with a <code>tenant</code> when one is present; otherwise, return an empty list.** [web-ng/lib/serviceradar_web_ng_web/live/admin/edge_package_live/index.ex [20-26]](https://github.com/carverauto/serviceradar/pull/2246/files#diff-d0d22a987aa942ef25088696daea862e36bd8ca083ce29e4670f63e62947c62fR20-R26) ```diff def mount(_params, _session, socket) do tenant = get_tenant(socket) + package_list = + if tenant do + OnboardingPackages.list(%{limit: 50}, tenant: tenant) + else + # no active tenant: restrict or return empty list + [] + end socket = socket |> assign(:page_title, "Edge Onboarding") - |> assign(:packages, OnboardingPackages.list(%{limit: 50}, tenant: tenant)) + |> assign(:packages, package_list) ... ``` `[To ensure code accuracy, apply this suggestion manually]` <details><summary>Suggestion importance[1-10]: 7</summary> __ Why: The suggestion correctly points out a potential issue where a `nil` tenant might bypass tenancy scoping, and proposes a safer default of returning an empty list. </details></details></td><td align=center>Medium </td></tr> <tr><td align="center" colspan="2"> - [ ] More <!-- /improve --more_suggestions=true --> </td><td></td></tr></tbody></table>
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!2649
No description provided.