feat: Phase 8 — Network Discovery + Windows Dev-Setup-Skripte
Network Discovery:
- Go Agent: internal/scanner/scanner.go mit TCP-Sweep (Port 445/80/22/443),
ARP-Tabellen-Parser (Windows: arp -a, Linux: /proc/net/arp), Reverse-DNS,
50 gleichzeitige Goroutines mit Semaphore
- Go Agent main.go: COMMAND_TYPE_NETWORK_SCAN Case → scanner.Scan() → JSON stdout
- Backend: NetworkDevice Model (Id, AgentId, IpAddress, MacAddress, Hostname,
Vendor, IsManaged, FirstSeen, LastSeen)
- Backend: EF Migration AddNetworkDevices + Index auf IpAddress + MacAddress
- Backend: NetworkDevicesController GET /api/v1/network-devices + DELETE /{id}
- Backend: AgentGrpcService.ProcessNetworkScanResultAsync — upsert via MAC,
IsManaged=true wenn IP einem bekannten Agent entspricht
- Frontend: NetworkPage.tsx mit Scan-Panel, Device-Tabelle, Filter, Delete
- Frontend: App.tsx — 'Netzwerk' Nav-Eintrag mit Network Icon
Windows Dev-Setup:
- dev-start.ps1 — Startet Docker/Postgres, EF-Migrationen, Backend+Frontend
in separaten PowerShell-Fenstern; Voraussetzungen-Check (docker/dotnet/node/go)
- dev-stop.ps1 — Stoppt alle NexusRMM-Prozesse + PostgreSQL Container
- build-agent.ps1 — Baut nexus-agent.exe (Windows) + optional nexus-agent-linux
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NexusRMM.Infrastructure.Data;
|
||||
|
||||
namespace NexusRMM.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/v1/network-devices")]
|
||||
public class NetworkDevicesController : ControllerBase
|
||||
{
|
||||
private readonly RmmDbContext _db;
|
||||
public NetworkDevicesController(RmmDbContext db) => _db = db;
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetAll([FromQuery] Guid? agentId)
|
||||
{
|
||||
var query = _db.NetworkDevices.AsQueryable();
|
||||
|
||||
if (agentId.HasValue)
|
||||
query = query.Where(d => d.AgentId == agentId.Value);
|
||||
|
||||
var devices = await query
|
||||
.OrderBy(d => d.IpAddress)
|
||||
.Select(d => new
|
||||
{
|
||||
d.Id,
|
||||
d.AgentId,
|
||||
d.IpAddress,
|
||||
d.MacAddress,
|
||||
d.Hostname,
|
||||
d.Vendor,
|
||||
d.IsManaged,
|
||||
d.FirstSeen,
|
||||
d.LastSeen
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(devices);
|
||||
}
|
||||
|
||||
[HttpDelete("{id:int}")]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
{
|
||||
var device = await _db.NetworkDevices.FindAsync(id);
|
||||
if (device is null)
|
||||
return NotFound();
|
||||
|
||||
_db.NetworkDevices.Remove(device);
|
||||
await _db.SaveChangesAsync();
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -114,12 +114,12 @@ public class AgentGrpcService : AgentService.AgentServiceBase
|
||||
public override async Task<CommandResultResponse> ReportCommandResult(CommandResult request, ServerCallContext context)
|
||||
{
|
||||
var taskId = Guid.Parse(request.CommandId);
|
||||
var task = await _db.Tasks.FindAsync(taskId)
|
||||
var taskItem = await _db.Tasks.FindAsync(taskId)
|
||||
?? throw new RpcException(new Status(StatusCode.NotFound, "Task not found"));
|
||||
|
||||
task.Status = request.Success ? CoreTaskStatus.Completed : CoreTaskStatus.Failed;
|
||||
task.CompletedAt = DateTime.UtcNow;
|
||||
task.Result = JsonSerializer.SerializeToElement(new
|
||||
taskItem.Status = request.Success ? CoreTaskStatus.Completed : CoreTaskStatus.Failed;
|
||||
taskItem.CompletedAt = DateTime.UtcNow;
|
||||
taskItem.Result = JsonSerializer.SerializeToElement(new
|
||||
{
|
||||
request.ExitCode,
|
||||
request.Stdout,
|
||||
@@ -129,6 +129,11 @@ public class AgentGrpcService : AgentService.AgentServiceBase
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
if (taskItem.Type == NexusRMM.Core.Models.TaskType.NetworkScan && request.Success && !string.IsNullOrWhiteSpace(request.Stdout))
|
||||
{
|
||||
await ProcessNetworkScanResultAsync(request.AgentId, request.Stdout);
|
||||
}
|
||||
|
||||
// SignalR: Command-Ergebnis an agent-Gruppe pushen
|
||||
await _hub.Clients.Group($"agent-{request.AgentId}")
|
||||
.CommandResultUpdated(request.CommandId, request.AgentId, request.Success, request.ExitCode);
|
||||
@@ -136,6 +141,73 @@ public class AgentGrpcService : AgentService.AgentServiceBase
|
||||
return new CommandResultResponse();
|
||||
}
|
||||
|
||||
private async Task ProcessNetworkScanResultAsync(string agentIdStr, string stdout)
|
||||
{
|
||||
try
|
||||
{
|
||||
var scannedDevices = JsonSerializer.Deserialize<List<ScanDevice>>(stdout, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
|
||||
if (scannedDevices is null || scannedDevices.Count == 0)
|
||||
return;
|
||||
|
||||
Guid? agentId = Guid.TryParse(agentIdStr, out var parsedId) ? parsedId : null;
|
||||
|
||||
var knownAgentIps = await _db.Agents
|
||||
.Select(a => a.IpAddress)
|
||||
.Where(ip => ip != null && ip != string.Empty)
|
||||
.ToHashSetAsync();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
foreach (var scanned in scannedDevices)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(scanned.Mac))
|
||||
continue;
|
||||
|
||||
var existing = await _db.NetworkDevices
|
||||
.FirstOrDefaultAsync(d => d.MacAddress == scanned.Mac);
|
||||
|
||||
if (existing is not null)
|
||||
{
|
||||
existing.LastSeen = now;
|
||||
existing.IpAddress = scanned.Ip ?? existing.IpAddress;
|
||||
existing.IsManaged = knownAgentIps.Contains(scanned.Ip ?? string.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
_db.NetworkDevices.Add(new NexusRMM.Core.Models.NetworkDevice
|
||||
{
|
||||
AgentId = agentId,
|
||||
IpAddress = scanned.Ip ?? string.Empty,
|
||||
MacAddress = scanned.Mac,
|
||||
Hostname = scanned.Hostname ?? string.Empty,
|
||||
Vendor = scanned.Vendor ?? string.Empty,
|
||||
IsManaged = knownAgentIps.Contains(scanned.Ip ?? string.Empty),
|
||||
FirstSeen = now,
|
||||
LastSeen = now
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to process NetworkScan result for agent {AgentId}", agentIdStr);
|
||||
}
|
||||
}
|
||||
|
||||
private class ScanDevice
|
||||
{
|
||||
public string? Ip { get; set; }
|
||||
public string? Mac { get; set; }
|
||||
public string? Hostname { get; set; }
|
||||
public string? Vendor { get; set; }
|
||||
}
|
||||
|
||||
private static CommandType MapTaskType(NexusRMM.Core.Models.TaskType type) => type switch
|
||||
{
|
||||
NexusRMM.Core.Models.TaskType.Shell => CommandType.Shell,
|
||||
|
||||
Reference in New Issue
Block a user