diff --git a/lib/towerops/device_profiles.ex b/lib/towerops/device_profiles.ex index 54b9b5ea..83249a2d 100644 --- a/lib/towerops/device_profiles.ex +++ b/lib/towerops/device_profiles.ex @@ -82,6 +82,30 @@ defmodule Towerops.DeviceProfiles do |> Repo.insert() end + @doc """ + Upserts a device profile by OS name. + + If a profile with the given OS exists, it updates it and deletes all associated + definitions (detection rules, sensors, etc.) to allow re-import from scratch. + Otherwise, creates a new profile. + """ + def upsert_profile(attrs \\ %{}) do + os = attrs[:os] || attrs["os"] + + case get_profile(os) do + nil -> + # Create new profile + create_profile(attrs) + + existing_profile -> + # Delete all associated definitions to get a clean slate + delete_profile_associations(existing_profile.id) + + # Update the profile + update_profile(existing_profile, attrs) + end + end + @doc """ Updates a device profile. """ @@ -154,6 +178,22 @@ defmodule Towerops.DeviceProfiles do # Private functions + defp delete_profile_associations(profile_id) do + # Delete in order to respect foreign key constraints + Repo.delete_all( + from s in SensorState, + join: sd in SensorDefinition, + on: s.sensor_definition_id == sd.id, + where: sd.device_profile_id == ^profile_id + ) + + Repo.delete_all(from s in SensorDefinition, where: s.device_profile_id == ^profile_id) + Repo.delete_all(from p in ProcessorDefinition, where: p.device_profile_id == ^profile_id) + Repo.delete_all(from m in MemPoolDefinition, where: m.device_profile_id == ^profile_id) + Repo.delete_all(from o in OSDefinition, where: o.device_profile_id == ^profile_id) + Repo.delete_all(from d in DetectionRule, where: d.device_profile_id == ^profile_id) + end + defp list_profiles_with_detection_rules do Repo.all( from p in DeviceProfile, diff --git a/lib/towerops/device_profiles/importer.ex b/lib/towerops/device_profiles/importer.ex index d16f5cef..2a6ad96f 100644 --- a/lib/towerops/device_profiles/importer.ex +++ b/lib/towerops/device_profiles/importer.ex @@ -118,9 +118,11 @@ defmodule Towerops.DeviceProfiles.Importer do priority: data["priority"] || 100 } - case DeviceProfiles.create_profile(attrs) do + # Upsert profile - updates if exists, creates if new + # Deletes all associated definitions before update for clean re-import + case DeviceProfiles.upsert_profile(attrs) do {:ok, profile} -> - # Create detection rules + # Create detection rules (associations were deleted in upsert) :ok = create_detection_rules(profile.id, data["discovery"] || []) {:ok, profile}