Private/Link.ps1
|
# The Link primitive. # # A Link is one connection between two things on the network a Check observed: the adapter # the Target Machine has, the Subnet that adapter is in, the DNS server it uses, the share a # mapped drive leads to and the Server that share is on. It is what the Network Map is # assembled from (Private/NetworkMap.ps1), beside the Measurements and Timed Events that # name the same Servers. See ADR-0012. # # Not a Finding: it has no Severity. It says how things are connected, not whether they # work - "dc01 is a controller of contoso.example" is as true of a dc01 that did not answer. # Where a Finding is about the thing it starts from, and that thing is no Server, Service or # Namespace a Finding's Subject could name - a printer - it names that Finding, as a # Measurement does, and the Report draws the thing in its colour. A Judge returns it among its Findings, as it does its Measurements # and Timed Events, and the Run tells the four apart by type. # # The kinds are a closed list, each valid only between given kinds of things, for the # reason a Measurement's Metric is one: a misspelt kind would be a connection nobody draws, # and a Subnet linked as the controller of a Domain a picture nobody can read. # The things a Link may name, each identified by one rule (New-LinkEnd). A thing exists in # the Map by being named; there is no record per thing. $script:LinkThings = @('Machine', 'Adapter', 'Subnet', 'Server', 'Domain', 'Site', 'Namespace', 'Share', 'Service', 'Drive', 'App', 'WindowsService', 'Printer') # Each kind of Link, and between which things it is valid, as From>To. The Target Machine # stands at the start: where a Check knows which adapter an answer came over, the adapter # uses the server; where it does not, the Target Machine does. $script:LinkKinds = [ordered]@{ HasAdapter = @('Machine>Adapter') InSubnet = @('Adapter>Subnet') UsesGateway = @('Adapter>Server', 'Machine>Server') UsesDns = @('Adapter>Server', 'Machine>Server') UsesDhcp = @('Adapter>Server', 'Machine>Server') ControllerOf = @('Server>Domain') InSite = @('Server>Site') NamespaceOf = @('Namespace>Domain') LeadsTo = @('Namespace>Server', 'Namespace>Share', 'Drive>Namespace', 'Drive>Share') OnServer = @('Share>Server', 'Service>Server') # A Windows service the Target Machine has, as the Service Kind looked at it. HasService = @('Machine>WindowsService') # A printer the Target Machine prints to, and what it prints through: the share of a print # Server, or the printer's own port. PrintsTo = @('Printer>Share', 'Printer>Service') # An App uses a Service, or a Windows service: added by the Run where it stamps a Check's # App on a Link that names one (New-CheckOutcome), never by a Judge, which does not know # its App. Uses = @('App>Service', 'App>WindowsService') } # The kinds of Link the Run adds itself, by Kind:From, each with the Kind:From whose Links it # adds them to: known where that one is (Get-NetworkMapCoverageView). $script:LinkObservationsOfTheRun = @{ 'Uses:App' = 'OnServer:Service' } # Each kind of Link by the thing it starts from, as Kind:From - LeadsTo:Namespace, # LeadsTo:Drive: what a Kind declares its Judge observes ($script:<Kind>Links), and what the # coverage of a Network Map says is unknown. By the thing it starts from, because two Kinds # return one kind of Link: the Domain Kind where a Namespace leads, the Network Kind where a # drive does, and a Run without the one must not read the other's as known. $script:LinkObservations = @($script:LinkKinds.GetEnumerator() | ForEach-Object { $kind = $_.Key $_.Value | ForEach-Object { '{0}:{1}' -f $kind, ($_ -split '>')[0] } } | Select-Object -Unique) function New-Link { <# .SYNOPSIS One connection between two things a Check observed, as the Network Map holds it. Pure. .DESCRIPTION Validated here, like a Measurement: a kind not on the list, or one between things it does not join, throws where the Judge is written, and says what is allowed. The Check that observed it is stamped on by the Run, as on a Finding (Set-CheckOrigin). .PARAMETER Kind HasAdapter (Machine to Adapter), InSubnet (Adapter to Subnet), UsesGateway, UsesDns, UsesDhcp (Adapter or Machine to Server), ControllerOf (Server to Domain), InSite (Server to Site), NamespaceOf (Namespace to Domain), LeadsTo (Namespace to Server or Share, Drive to Namespace or Share), OnServer (Share or Service to Server), HasService (Machine to WindowsService), PrintsTo (Printer to Share or Service), Uses (App to Service or WindowsService, added by the Run). .PARAMETER From Where it starts, as New-LinkEnd names it; or such an end as it arrived from the Elevated Part, which is named again by the same rule. .PARAMETER To Where it leads, likewise. .PARAMETER Finding The Finding about the thing it starts from, or its Id as it arrived from the Elevated Part: the default printer's for the Link from it. Nothing for most Links. #> [CmdletBinding()] [OutputType([psobject])] param( [Parameter(Mandatory)][AllowEmptyString()][string]$Kind, [AllowNull()]$From, [AllowNull()]$To, [AllowNull()]$Finding ) $kindValue = @($script:LinkKinds.Keys | Where-Object { $_ -eq $Kind } | Select-Object -First 1) if (-not $kindValue.Count) { throw ("'{0}' is no kind of Link. Permitted: {1}." -f $Kind, (@($script:LinkKinds.Keys) -join ', ')) } $kindValue = $kindValue[0] if ($null -eq $From) { throw ('A Link {0} needs its From: what it starts at.' -f $kindValue) } if ($null -eq $To) { throw ('A Link {0} needs its To: what it leads to.' -f $kindValue) } $start = ConvertTo-LinkEnd -End $From $end = ConvertTo-LinkEnd -End $To $pairs = $script:LinkKinds[$kindValue] if ($pairs -notcontains ('{0}>{1}' -f $start.Thing, $end.Thing)) { throw ("A Link {0} joins {1}; not {2} to {3}." -f $kindValue, (($pairs | ForEach-Object { $_ -replace '>', ' to ' }) -join ', '), $start.Thing, $end.Thing) } [pscustomobject]@{ PSTypeName = 'Gutcheck.Link' Kind = $kindValue From = $start To = $end Finding = ConvertTo-FindingId -Finding $Finding -Of 'a Link' # Stamped on by the Run, as on a Finding. See Set-CheckOrigin. CheckName = '' App = '' } } function New-LinkEnd { <# .SYNOPSIS A thing on the network as a Link names it, with the Key it is known by. Pure. .DESCRIPTION One rule per thing, so that two Checks naming the same thing name it the same: - Machine: the Target Machine, "machine". - Adapter: by its interface on the Target Machine, keyed as Windows matches the alias - in lower case, "ethernet" - and called as Windows writes it, "Ethernet" (Name). - Subnet: by its network address and prefix, "192.0.2.0/24", from any address in it. - Server: by the identity module (ConvertTo-ServerName), its name or else its address. Which Server several names and addresses are is the Map's to decide, by the same module, from every record that names one. - Domain: by its DNS name, normalised as a Server's name is. - Site: by its name within its Domain, "contoso.example/berlin". - Namespace: by its root as a UNC path, "\\contoso.example\public". - Share: by its Server and share name, "\\fs01.contoso.example\daten". - Service: by its Server and port, "fs01.contoso.example:445", or by its URL. - Drive: by where it is mapped on the Target Machine, "Z:". - App: by its name as its Check Definition gives it, "Outlook". - WindowsService: a Windows service on the Target Machine, by its service name, keyed as Windows matches it - in lower case, "spooler" - and called as Windows writes it. - Printer: a printer the Target Machine prints to, by its name as Windows lists it, "\\ps01.contoso.example\Buero" - keyed in lower case, as Windows matches it. A thing its rule cannot name throws where the Judge is written. .PARAMETER Thing Which of the things above. .PARAMETER Name The adapter's interface alias, the Server's name, the Domain's DNS name, the site's name, a share's name beside -Server, the drive's letter, the printer's name; for a Service what its Check Definition calls it, which its Key leaves out. .PARAMETER Address A Server's address; for a Subnet any address in it; for a share or a Service the address of the Server it is on, where it is known. .PARAMETER PrefixLength A Subnet's prefix length. .PARAMETER Server The Server a share or a Service is on. .PARAMETER Domain The Domain a site is of. .PARAMETER Path A Namespace's path, or a share's, as a UNC path: "\\fs01\daten\Projekte" is the share daten on fs01. .PARAMETER Port A Service's port. .PARAMETER Url A Service's URL, where it is reached by one. #> [CmdletBinding()] [OutputType([psobject])] param( [Parameter(Mandatory)][AllowEmptyString()][string]$Thing, [AllowNull()][AllowEmptyString()][string]$Name = '', [AllowNull()][AllowEmptyString()][string]$Address = '', [AllowNull()]$PrefixLength, [AllowNull()][AllowEmptyString()][string]$Server = '', [AllowNull()][AllowEmptyString()][string]$Domain = '', [AllowNull()][AllowEmptyString()][string]$Path = '', [AllowNull()]$Port, [AllowNull()][AllowEmptyString()][string]$Url = '' ) $thingValue = @($script:LinkThings | Where-Object { $_ -eq $Thing } | Select-Object -First 1) if (-not $thingValue.Count) { throw ("'{0}' is no thing a Link names. Permitted: {1}." -f $Thing, ($script:LinkThings -join ', ')) } $thingValue = $thingValue[0] $end = [ordered]@{ Thing = $thingValue; Key = ''; Name = "$Name".Trim(); Address = ''; PrefixLength = $null Server = ''; Domain = ''; Port = $null; Url = '' } $serverAddress = ConvertTo-ServerAddress -Address $Address switch ($thingValue) { 'Machine' { $end.Name = '' $end.Key = 'machine' } 'Adapter' { if (-not $end.Name) { throw 'An adapter is named by its interface: the alias Windows gives it.' } # Called as Windows writes the alias, and keyed as Windows matches it: ignoring # case. Every other part compares the Key as it is. $end.Key = $end.Name.ToLowerInvariant() } 'Subnet' { $at = ConvertTo-NetworkAddress -Address $Address if ($null -eq $at) { throw ("A Subnet is named by an address in it and its prefix; '{0}' is no address." -f $Address) } $length = ConvertTo-Number $PrefixLength $bits = $at.GetAddressBytes().Length * 8 if ($null -eq $length -or $length -lt 0 -or $length -gt $bits -or $length -ne [math]::Floor($length)) { throw ("'{0}' is no prefix length for {1}: a Subnet is named by its address and prefix." -f $PrefixLength, $Address) } $network = (Get-SubnetAddress -Address $at -PrefixLength ([int]$length)).ToString().ToLowerInvariant() $end.Address = $network $end.PrefixLength = [int]$length $end.Key = '{0}/{1}' -f $network, [int]$length } 'Server' { $name = ConvertTo-ServerName -Name $end.Name # An address given as the name is the address: a DNS server is named by nothing else. if ($name -and (Test-NetworkAddress -Text $name)) { if (-not $serverAddress) { $serverAddress = ConvertTo-ServerAddress -Address $name } $name = '' } if (-not $name -and -not $serverAddress) { throw 'A Server is named by its name or its address, and this one by neither.' } $end.Name = $name $end.Address = $serverAddress $end.Key = $(if ($name) { $name } else { $serverAddress }) } 'Domain' { $name = ConvertTo-ServerName -Name $end.Name if (-not $name -or (Test-NetworkAddress -Text $name)) { throw ("A Domain is named by its DNS name; '{0}' is none." -f $Name) } $end.Name = $name $end.Key = $name } 'Site' { $domainName = ConvertTo-ServerName -Name $Domain if (-not $end.Name) { throw 'A site is named by its name within its Domain, and this one has none.' } if (-not $domainName) { throw ("The site '{0}' is named within its Domain, and none was given." -f $end.Name) } $end.Domain = $domainName $end.Key = '{0}/{1}' -f $domainName, $end.Name.ToLowerInvariant() } 'Namespace' { $root = Get-LinkUncRoot -Path $Path if (-not $root) { throw ("A Namespace is named by its root as a UNC path; '{0}' is none." -f $Path) } $end.Server = $root.Server $end.Name = $root.Share $end.Key = '\\{0}\{1}' -f $root.Server, $root.Share } 'Share' { $serverName = ConvertTo-ServerName -Name $Server $share = "$Name".Trim().ToLowerInvariant() if ($Path) { $root = Get-LinkUncRoot -Path $Path if (-not $root) { throw ("A share is named by its Server and its name; '{0}' is no UNC path." -f $Path) } $serverName = $root.Server $share = $root.Share } if (-not $serverName -or -not $share) { throw 'A share is named by its Server and its share name, and this one lacks one of them.' } $end.Server = $serverName $end.Name = $share $end.Address = $serverAddress $end.Key = '\\{0}\{1}' -f $serverName, $share } 'Service' { if ("$Url".Trim()) { $uri = $null if (-not [uri]::TryCreate("$Url".Trim(), [UriKind]::Absolute, [ref]$uri) -or -not $uri.Host) { throw ("A Service reached by a URL is named by it; '{0}' is none." -f $Url) } $end.Url = "$Url".Trim() $end.Server = ConvertTo-ServerName -Name $uri.Host $end.Address = $serverAddress $end.Key = $end.Url break } $serverName = ConvertTo-ServerName -Name $Server $number = ConvertTo-Number $Port if (-not $serverName) { throw 'A Service is named by its Server and port, or by its URL, and this one by neither.' } if ($null -eq $number -or $number -lt 1 -or $number -gt 65535 -or $number -ne [math]::Floor($number)) { throw ("A Service on {0} is named by its port; '{1}' is none." -f $serverName, $Port) } $end.Server = $serverName $end.Port = [int]$number $end.Address = $serverAddress $end.Key = '{0}:{1}' -f $serverName, [int]$number } 'Drive' { if (-not $end.Name) { throw 'A mapped drive is named by where it is mapped: its letter.' } $end.Name = $end.Name.ToUpperInvariant() $end.Key = $end.Name } 'App' { if (-not $end.Name) { throw 'An App is named by its name, as its Check Definition gives it.' } $end.Key = $end.Name } 'WindowsService' { if (-not $end.Name) { throw 'A Windows service is named by its service name, as Windows gives it.' } $end.Key = $end.Name.ToLowerInvariant() } 'Printer' { if (-not $end.Name) { throw 'A printer is named by its name, as Windows lists it.' } $end.Key = $end.Name.ToLowerInvariant() } } $object = [pscustomobject]$end $object.PSObject.TypeNames.Insert(0, 'Gutcheck.LinkEnd') $object } function New-ServiceLink { <# .SYNOPSIS A Service on the Server it lies on: by its Server and port, or by its URL, at the address it was reached at. Nothing for what its rule cannot name. Pure. .DESCRIPTION What a Kind that reaches an application's Servers returns, so that the Network Map joins the App to them once the Run stamps it on (New-CheckAppLink, ADR-0012). A host a Check was given that no rule can name costs one Link, never the Judge. .PARAMETER Address Where it answered; empty where it did not, or where a proxy answered for it. .PARAMETER Name What the Service is called, where the Check Definition names its port: "SwyxServer- Client-API". Never part of its Key: two Checks that call one port differently name one Service. #> [CmdletBinding()] [OutputType([psobject])] param( [AllowNull()][AllowEmptyString()][string]$Server = '', [AllowNull()]$Port, [AllowNull()][AllowEmptyString()][string]$Url = '', [AllowNull()][AllowEmptyString()][string]$Address = '', [AllowNull()][AllowEmptyString()][string]$Name = '' ) try { $service = if ("$Url".Trim()) { New-LinkEnd -Thing Service -Url $Url -Address $Address -Name $Name } else { New-LinkEnd -Thing Service -Server $Server -Port $Port -Address $Address -Name $Name } New-Link -Kind OnServer -From $service -To (New-LinkEnd -Thing Server -Name $service.Server -Address $Address) } catch { return } } function ConvertTo-LinkEnd { <# .SYNOPSIS An end of a Link as New-LinkEnd made it; one that came from elsewhere - the Elevated Part, a test - named again by the same rule. Pure. .DESCRIPTION Named again rather than trusted: a field the other side added stays behind, and a Key the other side computed by another rule is computed again here. #> [CmdletBinding()] [OutputType([psobject])] param([Parameter(Mandatory)]$End) if ($End.PSObject.TypeNames -contains 'Gutcheck.LinkEnd') { return $End } $arguments = @{ Thing = "$(Get-DataProperty $End 'Thing')" } foreach ($field in 'Name', 'Address', 'Server', 'Domain', 'Url') { $value = "$(Get-DataProperty $End $field)" if ($value) { $arguments[$field] = $value } } foreach ($field in 'PrefixLength', 'Port') { $value = Get-DataProperty $End $field if ($null -ne $value -and "$value" -ne '') { $arguments[$field] = $value } } # A Namespace and a share are named by the path they were met by; across the Parts that # is their Key, which is that path. if ($arguments.Thing -in 'Namespace') { $arguments['Path'] = "$(Get-DataProperty $End 'Key')" } New-LinkEnd @arguments } function Select-UniqueLink { <# .SYNOPSIS Each Link once: the first of those of one kind between the same two things, in the order they came. Pure. .DESCRIPTION For a Kind whose data names one connection more than once - a namespace entry per target, a route per next hop. The things are compared by their Key, in any casing, as Windows compares an adapter's alias. Two Kinds' Links are not this to merge: the same Link from two Checks is two pieces of evidence (ADR-0012). #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()][AllowEmptyCollection()]$Link) # A hashtable compares its keys in any casing. $said = @{} foreach ($one in @($Link | Where-Object { $_ })) { $key = '{0}|{1}:{2}|{3}:{4}' -f $one.Kind, $one.From.Thing, $one.From.Key, $one.To.Thing, $one.To.Key if ($said.ContainsKey($key)) { continue } $said[$key] = $true $one } } function Get-LinkUncRoot { # The Server and the first part of a UNC path, each as the Map names it, read by the # identity of a Server (Split-UncPath); nothing for what is no UNC path or has no share. # Pure. [CmdletBinding()] [OutputType([psobject])] param([AllowNull()][AllowEmptyString()][string]$Path) $unc = Split-UncPath -Path $Path if (-not $unc) { return $null } $server = ConvertTo-ServerName -Name $unc.Host $share = $unc.Share.Trim().ToLowerInvariant() if (-not $server -or -not $share) { return $null } [pscustomobject]@{ Server = $server; Share = $share } } |