Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/NetworkOptimizer.Audit/Rules/CameraVlanRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,11 @@ public class CameraVlanRule : AuditRuleBase
if (network == null)
return null;

// Check if this is an NVR (allowed on Management VLAN)
var isNvr = detection.Metadata?.ContainsKey("is_nvr") == true;

// Check placement using shared logic
var placement = VlanPlacementChecker.CheckCameraPlacement(network, networks, ScoreImpact);
var placement = VlanPlacementChecker.CheckCameraPlacement(network, networks, ScoreImpact, isNvr: isNvr);

if (placement.IsCorrectlyPlaced)
return null;
Expand Down Expand Up @@ -159,7 +162,9 @@ public class CameraVlanRule : AuditRuleBase
}
}

var message = $"{detection.CategoryName} on {network.Name} VLAN - should be on security VLAN";
var message = isNvr
? $"NVR on {network.Name} VLAN - should be on management or security VLAN"
: $"{detection.CategoryName} on {network.Name} VLAN - should be on security VLAN";

return new AuditIssue
{
Expand Down
47 changes: 39 additions & 8 deletions src/NetworkOptimizer.Audit/Rules/VlanPlacementChecker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,34 +201,65 @@ public static PlacementResult CheckPrinterPlacement(

/// <summary>
/// Check if a camera/surveillance device is correctly placed on a Security VLAN.
/// NVRs are also accepted on Management VLANs since they are infrastructure devices.
/// </summary>
/// <param name="currentNetwork">The network the device is currently on</param>
/// <param name="allNetworks">All available networks</param>
/// <param name="defaultScoreImpact">Default score impact (cameras are always high-risk)</param>
/// <param name="isNvr">Whether this device is an NVR (allowed on Management VLAN)</param>
/// <returns>Placement result with recommendation</returns>
public static PlacementResult CheckCameraPlacement(
NetworkInfo? currentNetwork,
List<NetworkInfo> allNetworks,
int defaultScoreImpact = 8)
int defaultScoreImpact = 8,
bool isNvr = false)
{
// Cameras should only be on Security networks
var isCorrectlyPlaced = currentNetwork?.Purpose == NetworkPurpose.Security;
// NVRs are also accepted on Management VLANs (they are infrastructure devices)
var isCorrectlyPlaced = currentNetwork?.Purpose == NetworkPurpose.Security
|| (isNvr && currentNetwork?.Purpose == NetworkPurpose.Management);

// Find the Security network to recommend (prefer lower VLAN number)
// Find networks to recommend
var securityNetwork = allNetworks
.Where(n => n.Purpose == NetworkPurpose.Security)
.OrderBy(n => n.VlanId)
.FirstOrDefault();

var recommendedLabel = securityNetwork != null
? $"{securityNetwork.Name} ({securityNetwork.VlanId})"
: "Security VLAN";
string recommendedLabel;
NetworkInfo? recommendedNetwork;

// Cameras are always high-risk - always Critical severity
if (isNvr)
{
// For NVRs, recommend Management VLAN as the primary target
var managementNetwork = allNetworks
.Where(n => n.Purpose == NetworkPurpose.Management)
.OrderBy(n => n.VlanId)
.FirstOrDefault();

recommendedNetwork = managementNetwork ?? securityNetwork;

if (managementNetwork != null && securityNetwork != null)
recommendedLabel = $"{managementNetwork.Name} ({managementNetwork.VlanId}) or {securityNetwork.Name} ({securityNetwork.VlanId})";
else if (managementNetwork != null)
recommendedLabel = $"{managementNetwork.Name} ({managementNetwork.VlanId})";
else if (securityNetwork != null)
recommendedLabel = $"{securityNetwork.Name} ({securityNetwork.VlanId})";
else
recommendedLabel = "Management or Security VLAN";
}
else
{
recommendedNetwork = securityNetwork;
recommendedLabel = securityNetwork != null
? $"{securityNetwork.Name} ({securityNetwork.VlanId})"
: "Security VLAN";
}

// Cameras and NVRs are always high-risk - always Critical severity
return new PlacementResult(
IsCorrectlyPlaced: isCorrectlyPlaced,
IsLowRisk: false,
RecommendedNetwork: securityNetwork,
RecommendedNetwork: recommendedNetwork,
RecommendedNetworkLabel: recommendedLabel,
Severity: AuditSeverity.Critical,
ScoreImpact: defaultScoreImpact);
Expand Down
11 changes: 9 additions & 2 deletions src/NetworkOptimizer.Audit/Rules/WirelessCameraVlanRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,21 @@ public class WirelessCameraVlanRule : WirelessAuditRuleBase
if (network == null)
return null;

// Check if this is an NVR (allowed on Management VLAN)
var isNvr = client.Detection.Metadata?.ContainsKey("is_nvr") == true;

// Check placement using shared logic
var placement = VlanPlacementChecker.CheckCameraPlacement(network, networks, ScoreImpact);
var placement = VlanPlacementChecker.CheckCameraPlacement(network, networks, ScoreImpact, isNvr: isNvr);

if (placement.IsCorrectlyPlaced)
return null;

var message = isNvr
? $"NVR on {network.Name} VLAN - should be on management or security VLAN"
: $"{client.Detection.CategoryName} on {network.Name} VLAN - should be on security VLAN";

return CreateIssue(
$"{client.Detection.CategoryName} on {network.Name} VLAN - should be on security VLAN",
message,
client,
recommendedNetwork: placement.RecommendedNetwork?.Name,
recommendedVlan: placement.RecommendedNetwork?.VlanId,
Expand Down
22 changes: 14 additions & 8 deletions src/NetworkOptimizer.Audit/Services/DeviceTypeDetectionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,19 @@ private DeviceDetectionResult DetectDeviceTypeCore(
if (_protectCameras != null && !string.IsNullOrEmpty(client?.Mac) &&
_protectCameras.TryGetName(client.Mac, out var protectCameraName))
{
_logger?.LogDebug("[Detection] '{DisplayName}': UniFi Protect device '{CameraName}' (confirmed by controller)",
displayName, protectCameraName);
var isNvr = _protectCameras.IsNvr(client.Mac);
_logger?.LogDebug("[Detection] '{DisplayName}': UniFi Protect {DeviceType} '{CameraName}' (confirmed by controller)",
displayName, isNvr ? "NVR" : "device", protectCameraName);
var metadata = new Dictionary<string, object>
{
["detection_method"] = "unifi_protect_api",
["mac"] = client.Mac,
["protect_name"] = protectCameraName ?? ""
};
if (isNvr)
{
metadata["is_nvr"] = true;
}
return new DeviceDetectionResult
{
Category = ClientDeviceCategory.Camera, // All Protect security devices use Camera category for VLAN rules
Expand All @@ -154,12 +165,7 @@ private DeviceDetectionResult DetectDeviceTypeCore(
VendorName = "Ubiquiti",
ProductName = protectCameraName ?? "UniFi Protect",
RecommendedNetwork = NetworkPurpose.Security,
Metadata = new Dictionary<string, object>
{
["detection_method"] = "unifi_protect_api",
["mac"] = client.Mac,
["protect_name"] = protectCameraName ?? ""
}
Metadata = metadata
};
}

Expand Down
24 changes: 20 additions & 4 deletions src/NetworkOptimizer.Core/Models/ProtectCamera.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,17 @@ public sealed record ProtectCamera
/// </summary>
public string? ConnectionNetworkId { get; init; }

/// <summary>
/// Whether this device is an NVR (UNVR, UNVR-Pro, Cloud Key).
/// NVRs are infrastructure devices that can legitimately be on Management or Security VLANs.
/// </summary>
public bool IsNvr { get; init; }

/// <summary>
/// Create a ProtectCamera from MAC and name
/// </summary>
public static ProtectCamera Create(string mac, string name, string? connectionNetworkId = null)
=> new() { Mac = mac.ToLowerInvariant(), Name = name, ConnectionNetworkId = connectionNetworkId };
public static ProtectCamera Create(string mac, string name, string? connectionNetworkId = null, bool isNvr = false)
=> new() { Mac = mac.ToLowerInvariant(), Name = name, ConnectionNetworkId = connectionNetworkId, IsNvr = isNvr };
}

/// <summary>
Expand Down Expand Up @@ -60,9 +66,9 @@ public void Add(string mac, string name)
/// <summary>
/// Add a camera by MAC, name, and connection network ID
/// </summary>
public void Add(string mac, string name, string? connectionNetworkId)
public void Add(string mac, string name, string? connectionNetworkId, bool isNvr = false)
{
Add(ProtectCamera.Create(mac, name, connectionNetworkId));
Add(ProtectCamera.Create(mac, name, connectionNetworkId, isNvr));
}

/// <summary>
Expand Down Expand Up @@ -119,6 +125,16 @@ public bool TryGetNetworkId(string? mac, out string? networkId)
return false;
}

/// <summary>
/// Check if a MAC address belongs to an NVR device
/// </summary>
public bool IsNvr(string? mac)
{
if (string.IsNullOrEmpty(mac))
return false;
return _cameras.TryGetValue(mac, out var camera) && camera.IsNvr;
}

/// <summary>
/// Get all cameras in the collection
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/NetworkOptimizer.UniFi/UniFiApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -678,7 +678,7 @@ public async Task<ProtectCameraCollection> GetProtectCamerasAsync(CancellationTo
if (device.RequiresSecurityVlan)
{
var name = !string.IsNullOrEmpty(device.Name) ? device.Name : device.Model ?? "Protect Device";
result.Add(device.Mac, name, device.ConnectionNetworkId);
result.Add(device.Mac, name, device.ConnectionNetworkId, device.IsNvr);

var deviceType = device.IsCamera ? "camera" :
device.IsDoorbell ? "doorbell" :
Expand Down
8 changes: 5 additions & 3 deletions src/NetworkOptimizer.Web/Services/AuditService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1591,9 +1591,11 @@ private static string GetIssueTitle(string type, string message, Audit.Models.Au
? "IoT Device Allowed on VLAN"
: (isInformational ? "IoT Device Possibly on Wrong VLAN" : "IoT Device on Wrong VLAN"),
Audit.IssueTypes.CameraVlan or Audit.IssueTypes.WifiCameraVlan or "OFFLINE-CAMERA-VLAN" or "OFFLINE-CLOUD-CAMERA-VLAN" =>
message.StartsWith("Security System")
? (isInformational ? "Security System Possibly on Wrong VLAN" : "Security System on Wrong VLAN")
: (isInformational ? "Camera Possibly on Wrong VLAN" : "Camera on Wrong VLAN"),
message.StartsWith("NVR")
? (isInformational ? "NVR Possibly on Wrong VLAN" : "NVR on Wrong VLAN")
: message.StartsWith("Security System")
? (isInformational ? "Security System Possibly on Wrong VLAN" : "Security System on Wrong VLAN")
: (isInformational ? "Camera Possibly on Wrong VLAN" : "Camera on Wrong VLAN"),
Audit.IssueTypes.InfraNotOnMgmt => "Infrastructure Device on Wrong VLAN",

// Port security
Expand Down
Loading