discovery fix and favicon

This commit is contained in:
Graham McIntire 2026-01-18 13:47:19 -06:00
parent 2324b75e47
commit 94311a48aa
No known key found for this signature in database
7 changed files with 102 additions and 52 deletions

View file

@ -204,7 +204,20 @@ defmodule Towerops.DeviceProfiles do
end
defp match_detection_rules?(rules, system_info) do
Enum.any?(rules, &match_rule?(&1, system_info))
# Group rules by rule_group (each group represents a detection block from YAML)
# OR logic between groups, AND logic within groups
rules
|> Enum.group_by(& &1.rule_group)
|> Enum.any?(fn {_group_id, group_rules} ->
# Within a group, ALL field types must match (AND logic)
# But we need to handle multiple values for the same field type (OR within field)
group_rules
|> Enum.group_by(& &1.rule_type)
|> Enum.all?(fn {_rule_type, type_rules} ->
# Within the same field type, ANY rule can match (OR logic)
Enum.any?(type_rules, &match_rule?(&1, system_info))
end)
end)
end
defp match_rule?(%DetectionRule{rule_type: "sysObjectID"} = rule, system_info) do

View file

@ -21,6 +21,7 @@ defmodule Towerops.DeviceProfiles.DetectionRule do
field :value, :string
field :pattern, :string
field :priority, :integer, default: 0
field :rule_group, :integer, default: 0
belongs_to :device_profile, DeviceProfile
@ -30,7 +31,16 @@ defmodule Towerops.DeviceProfiles.DetectionRule do
@doc false
def changeset(rule, attrs) do
rule
|> cast(attrs, [:rule_type, :oid, :operator, :value, :pattern, :priority, :device_profile_id])
|> cast(attrs, [
:rule_type,
:oid,
:operator,
:value,
:pattern,
:priority,
:rule_group,
:device_profile_id
])
|> validate_required([:rule_type, :device_profile_id])
|> foreign_key_constraint(:device_profile_id)
end

View file

@ -132,17 +132,21 @@ defmodule Towerops.DeviceProfiles.Importer do
end
defp create_detection_rules(profile_id, rules) when is_list(rules) do
Enum.each(rules, fn rule ->
parse_detection_rule(profile_id, rule)
# Each rule in the list is a separate detection block
# Uses OR logic between blocks, AND logic within blocks
rules
|> Enum.with_index()
|> Enum.each(fn {rule, group_index} ->
parse_detection_rule(profile_id, rule, group_index)
end)
:ok
end
defp parse_detection_rule(profile_id, rule) do
defp parse_detection_rule(profile_id, rule, group_index) do
# Handle sysObjectID rules
if Map.has_key?(rule, "sysObjectID") do
create_sys_object_id_rules(profile_id, rule["sysObjectID"])
create_sys_object_id_rules(profile_id, rule["sysObjectID"], group_index)
end
# Handle sysDescr rules
@ -150,67 +154,42 @@ defmodule Towerops.DeviceProfiles.Importer do
DeviceProfiles.create_detection_rule(%{
device_profile_id: profile_id,
rule_type: "sysDescr",
value: rule["sysDescr"]
value: rule["sysDescr"],
rule_group: group_index
})
end
# Handle sysDescr_regex rules
if Map.has_key?(rule, "sysDescr_regex") do
create_sys_descr_regex_rules(profile_id, rule["sysDescr_regex"])
create_sys_descr_regex_rules(profile_id, rule["sysDescr_regex"], group_index)
end
{:ok, :processed}
end
defp create_sys_object_id_rules(profile_id, oids) when is_list(oids) do
defp create_sys_object_id_rules(profile_id, oids, group_index) when is_list(oids) do
Enum.each(oids, fn oid ->
# Skip overly broad patterns that would match any enterprise OID
if is_overly_broad_oid?(oid) do
Logger.warning("Skipping overly broad sysObjectID pattern: #{oid}")
else
DeviceProfiles.create_detection_rule(%{
device_profile_id: profile_id,
rule_type: "sysObjectID",
pattern: oid
})
end
DeviceProfiles.create_detection_rule(%{
device_profile_id: profile_id,
rule_type: "sysObjectID",
pattern: oid,
rule_group: group_index
})
end)
{:ok, :created}
end
defp create_sys_object_id_rules(profile_id, oid) when is_binary(oid) do
# Skip overly broad patterns that would match any enterprise OID
if is_overly_broad_oid?(oid) do
Logger.warning("Skipping overly broad sysObjectID pattern: #{oid}")
else
DeviceProfiles.create_detection_rule(%{
device_profile_id: profile_id,
rule_type: "sysObjectID",
pattern: oid
})
end
defp create_sys_object_id_rules(profile_id, oid, group_index) when is_binary(oid) do
DeviceProfiles.create_detection_rule(%{
device_profile_id: profile_id,
rule_type: "sysObjectID",
pattern: oid,
rule_group: group_index
})
end
# Check if an OID pattern is too broad and would match too many devices
# Patterns like ".1.3.6.1.4.1" would match ANY enterprise OID
defp is_overly_broad_oid?(oid) do
# Normalize to dotted format
normalized = if String.starts_with?(oid, "."), do: oid, else: "." <> oid
# These are the only acceptable root OIDs that are specific enough
# .1.3.6.1.4.1 is the enterprise prefix - anything shorter is too broad
# Must have at least one more component after .1.3.6.1.4.1
case normalized do
".1.3.6.1.4.1" -> true
# Also reject other common roots that are too broad
".1.3.6.1.4" -> true
".1.3.6.1" -> true
_ -> false
end
end
defp create_sys_descr_regex_rules(profile_id, patterns) when is_list(patterns) do
defp create_sys_descr_regex_rules(profile_id, patterns, group_index) when is_list(patterns) do
Enum.each(patterns, fn pattern ->
# Remove leading/trailing slashes from regex pattern
clean_pattern = String.trim(pattern, "/")
@ -218,20 +197,22 @@ defmodule Towerops.DeviceProfiles.Importer do
DeviceProfiles.create_detection_rule(%{
device_profile_id: profile_id,
rule_type: "sysDescr_regex",
pattern: clean_pattern
pattern: clean_pattern,
rule_group: group_index
})
end)
{:ok, :created}
end
defp create_sys_descr_regex_rules(profile_id, pattern) when is_binary(pattern) do
defp create_sys_descr_regex_rules(profile_id, pattern, group_index) when is_binary(pattern) do
clean_pattern = String.trim(pattern, "/")
DeviceProfiles.create_detection_rule(%{
device_profile_id: profile_id,
rule_type: "sysDescr_regex",
pattern: clean_pattern
pattern: clean_pattern,
rule_group: group_index
})
end

View file

@ -4,6 +4,8 @@ defmodule ToweropsWeb.DeviceLive.Index do
alias Towerops.Devices
alias Towerops.Sites
alias Towerops.Snmp
alias Towerops.Workers.DiscoveryWorker
@impl true
def mount(_params, _session, socket) do
@ -26,4 +28,30 @@ defmodule ToweropsWeb.DeviceLive.Index do
defp apply_action(socket, :index, _params) do
socket
end
@impl true
def handle_event("force_rediscover_all", _params, socket) do
devices = socket.assigns.device
snmp_devices = Enum.filter(devices, & &1.snmp_enabled)
if Enum.empty?(snmp_devices) do
{:noreply, put_flash(socket, :error, "No SNMP-enabled devices found")}
else
Enum.each(snmp_devices, fn device ->
enqueue_discovery(device.id)
end)
count = length(snmp_devices)
{:noreply, put_flash(socket, :info, "Discovery started for #{count} device#{if count == 1, do: "", else: "s"}")}
end
end
defp enqueue_discovery(device_id) do
if Application.get_env(:towerops, :env) == :test do
Task.start(fn -> Snmp.discover_device(Devices.get_device!(device_id)) end)
else
{:ok, _job} = Exq.enqueue(Exq, "discovery", DiscoveryWorker, [device_id])
end
end
end

View file

@ -9,6 +9,15 @@
{@page_title}
<:subtitle>Monitor and manage all your network devices</:subtitle>
<:actions>
<%= if @device != [] do %>
<.button
type="button"
phx-click="force_rediscover_all"
data-confirm="This will trigger SNMP discovery for all SNMP-enabled devices. Continue?"
>
<.icon name="hero-magnifying-glass" class="h-4 w-4" /> Force Rediscover All
</.button>
<% end %>
<.button
:if={@has_sites}
navigate={~p"/devices/new"}

View file

@ -0,0 +1,9 @@
defmodule Towerops.Repo.Migrations.AddRuleGroupToDetectionRules do
use Ecto.Migration
def change do
alter table(:profile_detection_rules) do
add :rule_group, :integer, null: false, default: 0
end
end
end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 B

After

Width:  |  Height:  |  Size: 1.1 KiB