Sensors and Virtual Sensors
Introduction
This module handles physical sensor registration, one-time host enrollment, interface inventory management, host heartbeat and metrics ingestion, and virtual sensor lifecycle management. A physical sensor is the hardware host, while a virtual sensor is a deployable Suricata workload that consumes two eligible interfaces on that host.
The sensor orchestration has been split into focused services:
SensorsService— Core CRUD and sensor management (480 lines)SensorInterfaceAllocationService— Network interface pool validationResilientHttpService— Circuit breaker + retry for external HTTP callsVirtualSensorsService— Virtual sensor lifecycle managementSensorHeartbeatService— Heartbeat processing
All controllers delegate to service interfaces. Repositories (SensorRepository, SensorHeartbeatRepository, VirtualSensorRepository) handle data access.
Controllers
| Controller | Route | Description |
|---|---|---|
SensorsController | api/sensors | Sensor CRUD and lifecycle |
VirtualSensorsController | api/virtual-sensors | Virtual sensor operations |
SensorHeartbeatController | api/sensor-heartbeat | Heartbeat processing |
Files:
API/Controllers/Sensors/SensorsController.csAPI/Controllers/Sensors/VirtualSensorsController.csAPI/Controllers/Sensors/SensorHeartbeatController.cs
Sensor Entity
File: Domain/Entities/Sensor.cs
Table: sensors
| Column | Type | Constraints | Description |
|---|---|---|---|
id | uuid | PK | Primary key (UUIDv7) |
name | varchar(255) | Required | Sensor display name |
organization_id | uuid | FK, Required | Owning organization |
status | varchar(50) | Required | unknown, off, on |
operational_status | varchar(50) | active, inactive, maintenance | |
sensor_ip | varchar(50) | Nullable | Sensor IP address |
sensor_port | int | Nullable | Sensor port number |
sudo_user | varchar(100) | Nullable | SSH sudo user for deployment |
hostname | varchar(255) | Nullable | Hostname reported by sensor agent |
os_info | varchar(255) | Nullable | Operating system information |
architecture | varchar(100) | Nullable | Reported CPU architecture |
agent_version | varchar(100) | Nullable | Installed host agent version |
enrollment_status | varchar(50) | Required | pending, enrolled |
management_interface | varchar(255) | Nullable | Management network interface |
last_heartbeat_at | timestamp | Nullable | Last agent heartbeat received |
last_inventory_at | timestamp | Nullable | Last interface inventory update |
created_at | timestamp | Creation timestamp | |
updated_at | timestamp | Last update timestamp |
Navigation Properties
[ForeignKey("OrganizationId")]
public Organization Organization { get; set; } = null!;
public ICollection<SensorHeartbeat> Heartbeats { get; set; }
public ICollection<VirtualSensor> VirtualSensors { get; set; }
public ICollection<SensorNetworkInterface> Interfaces { get; set; }
Status Model
Sensors have two status dimensions:
Status (connectivity):
| Value | Meaning |
|---|---|
unknown | Initial state, never connected |
off | Sensor is offline |
on | Sensor is online and reporting |
Operational Status (operational):
| Value | Meaning |
|---|---|
active | Normal operation |
inactive | Disabled by administrator |
maintenance | Under maintenance |
Virtual Sensor Entity
File: Domain/Entities/VirtualSensor.cs
Table: virtual_sensors
| Column | Type | Constraints | Description |
|---|---|---|---|
id | uuid | PK | Primary key (UUIDv7) |
sensor_id | uuid | FK, Required | Parent sensor |
name | varchar(255) | Required | Virtual sensor name |
interface1 | varchar(255) | Required | First network interface |
interface2 | varchar(255) | Required | Second network interface |
home_net | varchar(255) | Home network CIDR (default: 192.168.0.0/16) | |
status | varchar(50) | Required | Lifecycle status |
activation_status | varchar(50) | Nullable | Ansible activation status |
activation_error | text | Nullable | Error message from failed activation |
execution_id | varchar(255) | Nullable | Ansible execution ID |
deployment_path | varchar(255) | Nullable | Deployment target path |
created_at | timestamp | Creation timestamp | |
updated_at | timestamp | Last update timestamp |
Virtual Sensor Status Model
Status (lifecycle):
| Value | Meaning |
|---|---|
inactive | Created but not yet activated |
activating | Activation in progress via Ansible |
active | Successfully activated and running |
failed | Activation failed |
Activation Status (Ansible execution):
| Value | Meaning |
|---|---|
pending | Queued for activation |
running | Ansible playbook executing |
success | Activation completed successfully |
failed | Activation failed (see error) |
Sensor Heartbeat Entity
File: Domain/Entities/SensorHeartbeat.cs
Table: sensor_heartbeats
| Column | Type | Constraints | Description |
|---|---|---|---|
id | uuid | PK | Primary key (UUIDv7) |
sensor_id | uuid | FK, Required | Reporting sensor |
last_seen | timestamp | Nullable | Last seen timestamp |
isActive | bool | Nullable | Active flag |
created_at | timestamp | Creation timestamp | |
updated_at | timestamp | Last update timestamp |
Sensor Responsibilities
Registration and Enrollment
- Create a sensor draft with name and organization
- Generate a one-time install command from the UI
- Install one host agent on the physical sensor
- Enroll the sensor and persist host metadata plus a structured interface inventory
Activation Orchestration
- Activate or deactivate virtual sensors via Ansible service
- Track activation progress and status
- Deploy only Suricata and sensor-client for each virtual sensor
Heartbeat Processing
- Receive heartbeat reports from the host agent
- Update
last_heartbeat_atandlast_inventory_aton sensor - Store online status in InfluxDB
sensor_status - Store host CPU, memory, and eligible interface traffic in InfluxDB
sensor_metrics
Virtual Sensor Responsibilities
Lifecycle Management
- Create virtual sensors attached to a physical sensor
- Configure interface pairs (interface1, interface2) and home network
- Track deployment path and execution state
Activation via Ansible
- Submit activation request to Ansible service at
AnsibleSettings:ServiceUrl - Pass configuration including:
- Interface assignments
- InfluxDB connection details
- Data collector endpoint
- Docker registry credentials
- Backend URL for callbacks
Interface Allocation
- Interfaces are stored as structured records on the parent sensor
- Virtual sensor creation requires two explicit interfaces
- Only interfaces marked
isEligible=truecan be assigned - Assignment state is tracked on the interface record itself
External Dependencies
| Dependency | Config Key | Purpose |
|---|---|---|
| Ansible Service | AnsibleSettings:ServiceUrl | Virtual sensor deployment |
| Sensor API Key | SensorApiSettings:ApiKey | Sensor trigger authentication |
| Backend URL | BackendUrl | Callback URL for Ansible |
| Data Collector | DataCollector:Endpoint/Port | Sensor data destination |
| Docker Registry | DockerRegistry:Registry/Username/Password | Container image source |
| InfluxDB | InfluxDb:* | Metrics and heartbeat streams |
These settings are aggregated into SensorRuntimeOptions for convenience:
public sealed class SensorRuntimeOptions
{
public string BackendUrl { get; set; }
public string SensorApiKey { get; set; }
public string AnsibleServiceUrl { get; set; }
public string InfluxDbUrl { get; set; }
public string InfluxDbToken { get; set; }
public string InfluxDbOrg { get; set; }
public string InfluxDbBucket { get; set; }
public string DataCollectorEndpoint { get; set; }
public string DataCollectorPort { get; set; }
public string DockerRegistry { get; set; }
public string DockerUsername { get; set; }
public string DockerPassword { get; set; }
}
gRPC Healthcheck Service
File: Infrastructure/Services/SensorHealthcheckService.cs
Proto: Protos/sensor_healthcheck.proto
Provides a gRPC endpoint for sensors to report health status. Mapped in the middleware pipeline:
app.MapGrpcService<SensorHealthcheckService>();
Cascade Delete Rules
| Parent → Child | Behavior |
|---|---|
| Organization → Sensor | Cascade |
| Sensor → SensorHeartbeat | Cascade |
| Sensor → VirtualSensor | Cascade |
Key Risk Areas
| Area | Risk |
|---|---|
| Activation orchestration | Multi-step flow relies on external Ansible response contracts |
| No integration tests | Critical activation paths lack test coverage |
| MonitorExecutionAsync | Fire-and-forget Task.Run for monitoring Ansible execution status — no error reporting if monitoring fails |
Refactoring Status
Completed
- Split sensor orchestration service into focused services.
- Added dedicated
VirtualSensorsServicelifecycle handling. - Added resilient outbound execution path for Ansible interactions.
- Moved interface allocation logic into dedicated service.
- Kept controllers focused on transport and delegation.
Open
- Add integration tests for activation and rollback flows.