feat: report what listens, and which Windows service it is
One script: Get-NetTCPConnection -State Listen, Get-NetUDPEndpoint, the processes holding them and Win32_Service's PIDs. A process running one service names it; a shared svchost runs several, and which of them holds the socket Windows does not say, so it names none. http.sys sockets (WinRM, IIS, SMB) belong to the System process. The fixture is synthetic: no Windows host was at hand. tools/harvest.py records the real output. For netOrk#673.
This commit is contained in:
1 parent
1ce42ef099
commit
a6814ad92e
7 files changed
+185
-2
No files matched your search
@@ -24,7 +24,7 @@ from typing import Any
|
||||
|
||||
from napalm.base.exceptions import ConnectionClosedException
|
||||
from napalm_device_types import FingerprintRule, OSDriver, PortSpec
|
||||
from napalm_device_types.models import ServiceDict
|
||||
from napalm_device_types.models import ListeningSocketDict, ListeningSocketsDict, ServiceDict
|
||||
|
||||
from napalm_windows.transport import PowerShellError, PsrpTransport
|
||||
|
||||
@@ -134,6 +134,42 @@ ConvertTo-Json -Compress -Depth 3 -InputObject @(
|
||||
)
|
||||
"""
|
||||
|
||||
# Every listening TCP and bound UDP socket, the processes holding them and the
|
||||
# services those processes run. Sockets owned by http.sys (WinRM, IIS) belong to
|
||||
# the System process; a shared svchost runs several services in one process.
|
||||
_PS_LISTENING = r"""
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$tcp = @(Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue)
|
||||
$udp = @(Get-NetUDPEndpoint -ErrorAction SilentlyContinue)
|
||||
$ids = @(@($tcp) + @($udp) | ForEach-Object { [int]$_.OwningProcess } | Sort-Object -Unique)
|
||||
ConvertTo-Json -Compress -Depth 3 -InputObject ([pscustomobject]@{
|
||||
tcp = @($tcp | ForEach-Object {
|
||||
[pscustomobject]@{
|
||||
address = [string]$_.LocalAddress
|
||||
port = [int]$_.LocalPort
|
||||
pid = [int]$_.OwningProcess
|
||||
}
|
||||
})
|
||||
udp = @($udp | ForEach-Object {
|
||||
[pscustomobject]@{
|
||||
address = [string]$_.LocalAddress
|
||||
port = [int]$_.LocalPort
|
||||
pid = [int]$_.OwningProcess
|
||||
}
|
||||
})
|
||||
processes = @(if ($ids.Count) {
|
||||
Get-Process -Id $ids -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
[pscustomobject]@{ pid = [int]$_.Id; name = $_.ProcessName }
|
||||
}
|
||||
})
|
||||
services = @(
|
||||
Get-CimInstance -ClassName Win32_Service -Filter 'ProcessId <> 0' | ForEach-Object {
|
||||
[pscustomobject]@{ pid = [int]$_.ProcessId; name = $_.Name }
|
||||
}
|
||||
)
|
||||
})
|
||||
"""
|
||||
|
||||
_SERVICE_COMMANDS = {
|
||||
"start": "Start-Service -Name {name}",
|
||||
"stop": "Stop-Service -Name {name}",
|
||||
@@ -198,6 +234,27 @@ def _is_real_neighbour_mac(mac: str) -> bool:
|
||||
return not int(mac.split(":", 1)[0], 16) & 1
|
||||
|
||||
|
||||
def _listening_socket(
|
||||
proto: str, entry: dict[str, Any], names: dict[int, str], owners: dict[int, list[str]]
|
||||
) -> ListeningSocketDict:
|
||||
"""One socket of ``_PS_LISTENING``. A process running one service names it; a
|
||||
shared svchost runs several, and which of them holds the socket Windows does
|
||||
not say. PID 0 is no process."""
|
||||
pid = int(entry.get("pid") or 0)
|
||||
address, _, zone = str(entry.get("address") or "").partition("%")
|
||||
services = owners.get(pid, [])
|
||||
return {
|
||||
"proto": proto,
|
||||
"address": address,
|
||||
"port": int(entry.get("port") or 0),
|
||||
"interface": zone or None,
|
||||
"process": names.get(pid) if pid else None,
|
||||
"pid": pid or None,
|
||||
"unit": services[0] if len(services) == 1 else None,
|
||||
"container_id": None,
|
||||
}
|
||||
|
||||
|
||||
def _is_noise_route(destination: str) -> bool:
|
||||
"""Multicast, limited broadcast and loopback — present on every host."""
|
||||
try:
|
||||
@@ -444,6 +501,24 @@ class WindowsDriver(OSDriver):
|
||||
)
|
||||
return services
|
||||
|
||||
def get_listening_sockets(self) -> ListeningSocketsDict:
|
||||
"""Every listening TCP and bound UDP socket, with the process and the
|
||||
Windows service behind it (``Get-NetTCPConnection``, ``Get-NetUDPEndpoint``,
|
||||
``Win32_Service``). WinRM needs an administrator, who sees every
|
||||
process, so the reading is ``attributed``."""
|
||||
raw = self._run_ps(_PS_LISTENING) or {}
|
||||
names = {int(p["pid"]): str(p["name"]) for p in _as_list(raw.get("processes"))}
|
||||
owners: dict[int, list[str]] = {}
|
||||
for service in _as_list(raw.get("services")):
|
||||
owners.setdefault(int(service["pid"]), []).append(str(service["name"]))
|
||||
sockets = [
|
||||
_listening_socket(proto, entry, names, owners)
|
||||
for proto in ("tcp", "udp")
|
||||
for entry in _as_list(raw.get(proto))
|
||||
]
|
||||
sockets.sort(key=lambda s: (s["proto"], s["port"], s["address"], s["interface"] or ""))
|
||||
return {"attributed": True, "sockets": sockets}
|
||||
|
||||
def manage_service(self, name: str, action: str) -> dict[str, Any]:
|
||||
template = _SERVICE_COMMANDS.get(action)
|
||||
if template is None:
|
||||
|
||||
Reference in new issue
Block a user