Archiv der Kategorie: PowerShell-Praxis

Systemdienste vom Typ Kernel Driver auflisten und Spezialfall npcap

Das Auflisten von Systemdiensten ist mit der PowerShell eigentlich sehr einfach, doch wie sooft lauern auch kleine „Stolperfallen“. Insbesondere der Netzwerkdienste npcap, der u.a. von Wireshark verwendet wird, fällt so richtig aus der Rolle und kann einem „gestressten“ Admin Nerven und vor allem Zeit kosten.

Eigentlich ist alles ganz einfach. Ein

Get-Service
listet alle vorhanden Systemdienste auf, die Eigenschaft ServiceType gibt an, ob es sich um einen prozessbasierten Dienst oder einen „Kernel Service“ handelt.

Der folgende Aufruf gibt für den Dienst npcap (der über Nmap isntalliert wird) auch die korrekte Bezeichnung zurück.

Get-Service npcap | Select-Object Name, ServiceType

Npcap wird als Dienst vom Typ „KernelDriver“ ausgegeben.

Die erste „Überraschung“ entsteht beim Gruppieren aller Dienste nach der ServiceType-Property.

Get-Service | Group-Object ServiceType

Der Grund für die Überraschung: „KernelDriver“ erscheint nicht als Gruppenname, wird also nicht abgefragt. Der Dienst ist auch in keiner der Gruppen enthalten. Stattdessen erscheinen als Gruppennamen Zahlen wie 224, 208 oder 240, bei denen es sich um zusammengesetzte Zweierpotenzen handelt (eventuell setzt sich der Wert für ServiceType bei einigen Diensten aus mehreren Zahlenwerten zusammen – dafür spricht, dass für den ServiceType dieser Dienste bei einer WMI-Abfrage per Win32_Service als „Unknown“ eingetragen wird).

Es bleibt mysteriös, denn die folgende Abfrage gibt nichts zurück;

get-service npca*

Auf einmal gibt es keinen Dienst mit dem Namen npcap mehr (der Platzhalter funktioniert natürlich beim Name-Parameter von

Get-Service
). Dies könnte eventuell mit der Art und Weise zu tun haben, wie npcap in der Registry unter HKey_Local_Machine\System\CurrentControlSet\Services eingetragen wurde.

Eine Abfrage der Systemdienste funktioniert etwas besser per WMI:

Get-CimInstance Win32_SystemDriver -Filter "Name='npcap'"

Hier ist auch die Verwendung von Platzhaltern kein Problem:

Get-CimInstance Win32_SystemDriver -Filter "Name like'npca%'"

Warum aber ein get-service npca* nichts zurückgibt, bleibt im Moment ein (weiteres) ungelöstes Rätsel (genauso, dass sich der Dienst auf meinem PC mit Windows 8.1 nicht starten lässt).

Praxistipp: Abfrage der zuletzt installierten Systemdienste

Anders als bei Anwendungen gibt es bei Diensten keinen Installationszeitpunkt, der sich über den Dienst selber abfragen ließe. Wenn man herausbekommen möchte, ob eine Anwendung auch einen Systemdienst installiert hat, geht dies sehr einfach über die Abfrage des System-Ereignisprotokolls. Am besten mit „Service Controll Manager“ als Quelle und eventuell auch gleich der InstanceId 1073748869.

Da ich vor einiger Zeit wieder einmal Wireshark intalliert hatte, wollte ich herausfinden, welcher Systemdienst während der Installation installiert wurde (ok, es ist natürlich Npcap, aber Kontrolle ist bekanntlich besser…)

Der folgende Befehl liefert schon ein erstes Ergebnis:

get-eventlog -LogName System -InstanceId 1073748869 -Newest 3

Für die Praxis wichtige Details zur Installationsort liefert die Eigenschaft ReplacementStrings:

get-eventlog -LogName System -InstanceId 1073748869 -Newest 3 | Select -ExpandProperty ReplacementStrings

PowerShell 7.0 unter Ubuntu – dank snap alles ganz einfach

Die PowerShell unter Linux zu installieren, ist grundsätzlich kein Problem, auf der Downloadseite ist die Installation bestens für alle Versionen und Varianten dokumentiert und gelingt daher auch Unix-NOOBS wie mir (wobei ich Unix seit den 80er Jahren kenne und eine noch ausgepackte Box mit Linux Suse 4.2 bei mir im Regal steht – das Interesse war also immer da;).

Dank snap gelingt die Installation inzwischen mit einem Aufruf – es muss kein Repo mehr registriert werden.

Der Aufruf sieht wie folgt aus:

sudo snap install powerShell --classic

Die Angabe „–classic“ ist aus für mich nicht mehr nachvollziehbaren Gründen offenbar erforderlich. Anschließend kann die PowerShell 7.0 (bzw. eine aktuellere Version) durch Eingabe von

pwsh
gestartet werden.

Auch bei der neuen PowerShell muss die Hilfe per Update-Help aktualisiert werden. Hier kommt es aber offenbar auf den Parameter -UICulture mit en-us als Parameterwert an. Anschließend zeigt die Hilfe dann auch die Beispiele an und alles sieht richtig gut aus. Auch wenn ich natürlich etwas voreingenommen bin, bin ich mir sicher, dass die PowerShell eine Bereicherung im (gigantischen) Linux-Ökosystem ist.

Praxistipp: Feststellen, ob alle Dateien mit einer Zahlenendung vorhanden sind

Die Anforderung: Ein Verzeichnis enthält viele Dateien, die zuvor (einzeln) aus dem Internet heruntergeladen wurde. Jede Datei endet mit einer aus drei Ziffern bestehenden Zahl, also z.B. Datei1.zip.001, Datei1.zip.002, Datei1.zip.003 usw. Da die Dateien alle per Mausklick aus einem per OneDrive Business (Sharepoint) freigegebenen Webverzeichnis heruntergeladen wurden, soll sichergestellt werden, dass tatsächlich alle Dateien heruntergeladen wurden. Dazu muss geprüft werden, ob alle Zahlen von 1..n am Ende des Dateinamens in der Zahlenfolge von 1..n enthalten sind.

Das ist natürlich eine ideale Gelegenheit, die eigenen PowerShell-Kenntnisse zu überprüfen.

Da in dem Befehl nicht nur eine Hashtable, sondern auch ein (Mini-) Regex vorkommen, ist die Schreibweise am Anfang vielleicht etwas speziell, aber wie immer ist sie eigentlich ganz einfach.

Das folgende Beispiel geht davon aus, dass sich im Verzeichnis F:\VIS 2020 Dateien mit den Endungen .001 bis .242 befinden. Sollte eine Datei fehlen, wird sie ausgegeben.

$DateiNummern = (dir 'F:\VIS 2020\' -File | Select-Object -Expand Name | 
 Select-String ".*(\d{3})" | Select-Object @{n="Nr";e={[Int]$_.Matches[0].Groups[1].Value}}).Nr
 
(1..242).Where{$DateiNummern -notcontains $_}

In der ersten Version meiner „genialen Idee“ hatte ich noch einen Denkfehler eingebaut. Jetzt sollte das Ergebnis aber stimmen und es werden nur die Nummern ausgegeben, für die es keine Datei gibt.

PowerShell 7-0 und SSH

PowerShell Remoting per SSH ist seit der ersten Version der PowerShell Core möglich und damit nichts Neues mehr. Wirklich gebraucht habe ich es bislang aber noch nicht. Der Newsletter von PowerShell-Experte Adam Betram („Adam the Automator“) hat mich dazu veranlasst, es nach langer Zeit wieder einmal selber auszuprobieren:

https://adamtheautomator.com/ssh-with-powershell/

Das ganze Thema ist sehr gut dokumentiert. Voraussetzung auf der Server-Seite ist natürlich SSH. Bei Linux und MacOS ist SSH fest eingebaut (ich gehe einmal davon aus, dass dies für jede Linux-Distribution gilt), bei Windows 10/Windows Server muss es in Gestalt von Open SSH for Windows eventuell nachträglich hinzugefügt werden:

https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_server_configuration

Der SSH-Server läuft als Systemdienst mit dem Namen „Sshd“, so dass sich sein Status per Get-Service einfach abfragen lässt. Durch die Installation des SSH Servers wird automatisch eine Firewall-Regel für den Port 22 angelegt.

Der SSH-Client ist im Folgenden eine PowerShell 7.0, die unter Ubuntu 16.04 läuft. Hier muss nichts konfiguriert werden.

Damit eine Verbindung mit dem SSH-Server möglich ist, muss auf dem Computer ein Eintrag zur Konfigurationsdatei sshd_config (ohne Erweiterung) im Verzeichnis C:\ProgramData\ssh hinzugefügt werden.

In einer als Administrator gestarten PowerShell wird die Datei wie folgt geöffnet:

notepad $env:programdata\ssh\sshd_config

Der Eintrag wird unterhalb von „override default of no subsystems“ eingefügt:

Subsystem powershell pwsh.exe -sshs -NoLogo -NoProfile
Abb. 1: PowerShell wird als Subsystem zum SSH Server hinzugefügt

Mehr ist nicht zu tun. Jetzt kann von einem Remote-Computer per PowerShell eine Remote-Verbindung per SSH hergestellt werden:

enter-pssession -Hostname minipc -SSHTransport

Der Parameter SSHTransport sorgt dafür, dass anstelle von WsMan SSH verwendet wird. „minipc“ ist in diesem Beispiel der Name des Windows-PCs mit SSH Server. Auch eine SSH-Session wird per exit verlassen.

Insgesamt ist es erfreulich, wie einfach sich eine PowerShell-Remoting-Session per SSH anlegen lässt.

Generische Methoden aufrufen

Eine generische Methode ist eine Methode, der neben den (optionalen) Parameterwerten beim Aufruf auch ein Typ übergeben wird. Alle generischen Datentypen, die innerhalb der Methode verwendet werden inklusive dem Rückgabewert (sofern vorhanden), verwenden dann diesen Typ. Damit wird mehr Flexibilität erreicht, da die Methode nicht für jeden in Frage kommenden Datentyp überladen werden muss.

Das klassische Beispiel für eine generische Methode void swap<T>(T arg1, T arg2), welche zwei als Referenz übergebene Werte vertauscht. Mit ihr lassen sich Werte eines beliebigen Datentyps vertauschen, da dieser beim Aufruf der Methode ebenfalls angegeben wird:

swap(out a1, out a2)

In PowerShell kann man keine generischen Functions definieren und es würde auch keinen Sinn ergeben. Generische Methoden kommen immer dann ins Spiel, wenn eine Funktionalität einer externen Assembly-Bibliothek genutzt werden soll.

Dabei muss zwischen dem Aufruf einer statischen Methode und einer Instanzmethode unterschieden werden.

Es gibt im Internet (seit 2007) natürlich sehr viele Lösungen, von denen die meisten auch gut funktionieren. Eine sehr gute Lösung ist das Skript von David Wyatt, das dieser über die TechNet Script Gallery zur Verfügung stellt:

https://gallery.technet.microsoft.com/Invoke-Generic-Methods-bf7675af/view/Discussions#content

Mir ist die Lösung etwas überdimensioniert. Für einfache Anforderungen ist sehr viel weniger formaler Aufwand erforderlich. Der Aufruf einer generischen Methode läuft darauf, dass man zuerst die Methode per GetMethods() als Objekt holt, daraus per MakeGenericMethod() eine generische Methode macht, die dann per Invoke() aufgerufen wird. Eine Parameterübergabe ist natürlich kein Problem.

Die folgenden zwei Functions stelle ich ohne weitere Erläuterungen vor.

Aufruf einer generischen Instanzenmethode

function Invoke-GenericMethod
{
    param([Object]$InputObject,
          [Type]$GenericType,
          [String]$MethodName,
          [Object]$ArgumentList)
    # Wichtig: Das Filterkriterium reicht eventuell nicht immer aus -
    # es darf immer nur eine generische Methode zurückgegeben werden!
    $Method = $InputObject.GetType().GetMethods() | Where-Object { $_.Name -eq $MethodName -and $_.IsGenericMethod -eq $true}
    $MethodCall = $Method.MakeGenericMethod($GenericType)
    $MethodCall.Invoke($InputObject, $ArgumentList)
}

Aufruf einer generischen statischen Methode

function Invoke-GenericMethod
{
    param([Type]$Type,
          [Type]$GenericType,
          [String]$MethodName,
          [Object]$ArgumentList)
    $Method = $Type.GetMethod($MethodName)
    $MethodCall = $Method.MakeGenericMethod($GenericType)
    $MethodCall.Invoke($null, $ArgumentList)
}

PowerPoint-Folien mit PowerShell in Text konvertieren

Dieser Beitrag ist etwas praxisorientierter. Zwei Dinge vorweg: Das Thema ist alles andere als nue und die vorgestellte Lösung ist alles andere als perfekt. Es geht ausschließlich darum, wie sich mit verhältnismäßig wenig Aufwand der Text aus den Folien einer PowerPoint-Datei per PowerShell extrahieren lässt. Sozusagen von Power zu Power (sollte ich im Folgenden einmal PowerShell mit PowerPoint verwechseln, bitte ich dies zu entschuldigen. Das passiert mir sogar in meinen Schulungen hin und wieder;).

Ausgangspunkt ist die Assembly DocumentFormat.OpenXml.dll, die Teil des Open XML SDKs von Microsoft ist. Es gibt sie in verschiedenen Varianten, sowohl für .NET 4.6 als auch für .NET Core. Ich verwende die Variante für .NET 4.6. Ich gebe mit Absicht keinen Download-Link an, da man sie an verschiedenden Stellen findet. Entwickler nutzen natürlich das Nuget-Package, das sich theoretisch auch per PowerShell und Install-Package hinzufügen lässt. Aus der Package-Datei (einfach ein .Zip anhängen) muss dann die Dll-Datei herauskopiert werden. Der Rest des Package kann gefahrlos „entsorgt“ werden.

Alle folgenden Beispiele gehen davon aus, dass sich die Dll-Datei im selben Verzeichnis befindet wie die Ps1-Datei.

Bevor es losgeht, noch ein Tipp. Der programm- bzw. skriptgesteuerte Umgang mit den „modernen“ Microsoft Office-Dokumentformaten ist sehr gut dokumentiert. Wie sich die Texte aus einer PowerPoint-Datei extrahieren lassen, z.B. unter der folgenden Adresse:

https://docs.microsoft.com/en-us/office/open-xml/how-to-get-all-the-text-in-all-slides-in-a-presentation

Die Beispiele sind natürlich alle in C# oder VB verfasst. Höchstwahrscheinlich gibt es bereits fertige PowerShell-Module für den Umgang mit PowerPoint-Präsentationen. Ich war zu bequem, um danach zu suchen. Im Folgenden geht es daher in erster Linie um das Ausprobieren mit dem Ziel, die Hintergründe besser verstehen zu können.

Schritt 1: Als erstes wird die Assembly per Add-Type geladen. Dabei kommt einmal mehr das sehr praktische using namespace zum Einsatz.

using&nbsp;namespace&nbsp;System.Text
using namespace DocumentFormat.OpenXml.Presentation
using namespace DocumentFormat.OpenXml.Packaging
 
$AssPfad = Join-Path -Path $PSScriptRoot -ChildPath "DocumentFormat.OpenXml.dll"
Add-Type -Path $AssPfad

Schritt 2: Im nächsten Schritt wird die Pptx-Datei geöffnet und das PresentationPart-Objekt angesprochen.

$PptxPfad = Join-Path -Path $PSScriptRoot -ChildPath "DiePräsentation.pptx"
$pptxDoc = [PresentationDocument]::Open($PptxPfad, $False)
$presentationPart = $pptxDoc.PresentationPart

Schritt 3: Ausgabe der Anzahl der Folien

Als kleine „Warm-Up“-Übung wird die Anzahl der Folien (Slides) ausgegeben.

$slideCount = @($presentationPart.SlideParts).Count
"Slide Count=$slideCount"

Hier tritt eine typische PowerShell-Besonderheit zum Tragen, über die ich auch nach > 16 Jahren PowerShell immer wieder „stolpere“. Ohne die explite Array-Klammerung per @() werden alle SlidePart-Objekte einzeln angesprochen und die Count-Eigenschaft liefert für jedes SlidePart-Objekt eine 1. Solche Besonderheiten findet man immer durch Ausprobieren heraus.

Schritt 4: Holen der SlideIds

Für das Durchlaufen aller Slides wird die SlideIdList und ihre ChildElements benötigt.

$slideIds = $presentationPart.Presentation.SlideIdList.ChildElements

Schritt 5: Durchlaufen aller Slides

Wir nähern uns dem Finale, wenngleich es noch ein Zwischenhügel erklommen werden muss. Abe die foreach-Schleife darf man bereits eingeben.

for($slideIndex = 1;$slideIndex -lt $slideCount;$slideIndex++)
{
 
 
}

Schritt 6: Holen des SlidePart

Jetzt wird es ein wenig komplizierter. Um an den Folieninhalt zu kommen, wird der SlidePart benötigt. Dazu wird die RelationShipId benötigt, um über diese über den allgemeinen PresentationPart den SlidePart zu holen.

$relId = ($slideIds[$slideIndex] -as [SlideId]).RelationshipId
$slide = ($presentationPart.GetPartById($relId) -as [SlidePart])

Schritt 7: Zusammenstellen des Textes des SlidePart

Liegt das SlidePart-Objekt vor, geht es „nur noch“ darum, den Text zu erhalten. Das SlidePart-Objekt besitzt, wie alle Inhaltsobjekte, eine praktische Descendants<T>-Methode, die alle Abkömmlinge des Knotens des angegebenen Typs holt. Da sich diese C#-Schreibweise aber nicht 1:1 übertragen lässt, kann die Methode daher nicht direkt aufgerufen werden.

Es gibt zwei Alternativen:

>Aufruf von Descendants() ohne Parameter. In diesem Fall werden alle Abkömmlinge zurückegeben. Die Text-Elemente werden dann per Where-Object gefilter.

>Aufruf der generischen Descendants<T>()-Methode mit Hilfe von etwas „Reflection-Magic“, die aber einfach und überschaubar ist (die Invoke-GenericMethod-Fuction stelle ich in einem anderen Blog-Beitrag vor).

Im Folgenden verwende ich die zweite Variante.

 
$slideText = [StringBuilder]::new()
$t = [DocumentFormat.OpenXml.Drawing.Text]
$TextDescendants = Invoke-GenericMethod -InputObject $slide.Slide -MethodName Descendants -GenericType $t 
foreach($Text in $TextDescendants)
{
   [void]$slideText.Append($Text.Text + "`n")
}
$slideText.toString()

Alle Textelemente eines Slide werden zu einem StringBuilder-Objekt hinzugefügt, dessen Inhalt am Ende ausgegeben wird. Warum einen StringBuilder und nicht einfach +=? Bei größeren Textmengen ist diese Variante deutlich schneller.

Der Vollständigheit halber stelle ich die Invoke-GenericMethod-Function ohne weiteren Erläuterungen vor. Nur soviel: Damit es nicht zu kompliziert wird, funktioniert der Methodenaufruf nur dann, dass die über die Abfrage von Name und IsGenericMethod tatsächlich nur die eine Methode resultiert, die auch aufgerufen werden soll.

Ansonsten deckt das Skript von Dave Wyatt alle Eventualitäten ab, ist dafür aber auch „etwas“ umfangreicher:

https://gallery.technet.microsoft.com/Invoke-Generic-Methods-bf7675af/view/Discussions#content

 
function Invoke-GenericMethod
{
    param([Object]$InputObject,
          [Type]$GenericType,
          [String]$MethodName,
          [Object]$ArgumentList)
    $Method = $InputObject.GetType().GetMethods() | Where-Object { $_.Name -eq $MethodName -and $_.IsGenericMethod -eq $true}
    $MethodCall = $Method.MakeGenericMethod($GenericType)
    $MethodCall.Invoke($InputObject, $ArgumentList)
}

Das war alles. Die vorgestellte Befehlsfolge sollte den Textinhalt einer beliebigen Pptx-Datei ausgeben.

Tipp des Tages: Dienste entfernen auch ohne PowerShell

Triviale Aufgaben können ihre Tücken haben. Beispiel: Wie ich entferne ich einen Systemdienst, der sich auch per WMI nicht mehr entfernen lässt, da z.B. im onStop-Event eine Exception auftritt? Ganz einfach durch Beenden der Exe-Datei. Doch welche Exe-Datei ist es genau? Eine Kombination aus Sc.exe und Taskkill.exe löst das kleine Problem.

Warum Sc.exe und nicht Get-Service? Weil Ersteres mit dem queryex-Parameter auch die Prozess-ID mit ausgibt.

sc.exe queryex VISEFService

Anschließend genügt der Aufruf von

Taskkill /pid 1234 /F

wenn 1234 die Prozess-ID ist.

Natürlich geht das alles auch per PowerShell, aber inzwischen bin ich an dem Punkt angekommen wo es in erster Linie darum geht, dass ich nicht zu viel tippen muss (man wird älter und bequemer;). Und geht es nur darum, eine bestimmte Aufgabe zu erledigen, sind die alten Kommandozeilentools immer noch eine sehr gute Wahl.

Per PowerShell sähe der Aufruf in etwa so aus:

stop-process (Get-CimInstance Win32_Service -Filter "Name='VISEFService'").ProcessId

Es ist nicht unbedingt der Umfang der Zeile, sondern eher der andere Denkansatz, an den man sich erst gewöhnen muss. Im Moment habe ich eher das Gefühl, dass wir uns wieder zur klassischen „Kommandozeilen-Denkweise“ zurückbewegen.

Tipp des Tages: Verzeichnisse aus der path-Umgebungsvariablen entfernen

Hin und wieder kann es erforderlich sein, einzelne Verzeichnisse aus der path-Umgebungsvariablen zu entfernen. Was sich zunächst schwierig anhören könnte, ist in der Praxis dann doch ein relativ einfacher Befehl.

Der folgende Befehl entfernt alle Verzeichnisse, die ein Python36 enthalten, aus der Path-Umgebungsvariablen.

$env:path = $env:path.split(";").where{$_ -notmatch "Python36"} -join ";"

Die Änderung gilt natürlich nur für die aktuelle PowerShell-Sitzung. Soll sie dauerhaft sein, muss der neue Wert der Umgebungsvariablen über die SetEnvironmentVariable()-Methode der Environment-Klasse für den aktuellen Benutzer oder systemweit gespeichert werden.

Praxistipp: Das Anmeldekonto für einen Systemdienst aktualisieren

Zu den wenigen Einstellungen, die für einen Systemdienst nicht per Set-Service vorgenommen werden können gehört das Ändern des System- oder Benutzerkontos, unter dem der Dienst ausgeführt werden soll. Diese Einstellung muss per WMI, der Klasse Win32_Service und ihrer Change()-Methode vorgenommen werden. Benutzername und Kennwort werden dabei im Klartext übergeben. Eine kleine Herausforderung besteht darin, dass für alle nicht verwendeten Parameter ein $null übergeben werden muss.

Bill Stewart hat ein schönes Skript erstellt ( Set-ServiceCredential.ps1), mit dem das Systemkonto für einen Systemdienst aktualisiert werden kann:

https://gist.github.com/Bill-Stewart/ab3a228903c5d6fb3c12dc1d92d3d1e8

Mir ist das Skript aber etwas zu umfangreich und formal. Die folgende Function erledigt die Einstellung etwas kürzer:

function Set-ServiceAccount
{
  [CmdletBinding()]
  param([String]$Servicename, [String]$Username, [String]$Password)
  Stop-Service -Name $ServiceName
  $service = Get-WmiObject Win32_Service -Filter "Name='$ServiceName'"
  $ret = $service.change($null,$null,$null,$null,$null,$null,$Username,$Password,$null,$null,$null)
  if ($ret.ReturnValue -eq 0)
  {
      Write-Verbose "Kontoinformation wurde erfolgreich aktualisiert."
      Start-Service -Name $ServiceName
  }
  else
  {
      Write-Warning "Die Kontoinformation konnte nicht aktualisiert werden - ReturnValue=$($ret.ReturnValue)"
  }
}

Ging alles gut, ist der ReturnValue = 0. Ansonsten eine Zahl, die einen Fehlercode darstellt. 22 bedeutet z.B., das das Konto nicht stimmt (in dem Skript von Bill Stewart werden alle Fehlercodes abgefragt und durch Fehlermeldungen ersetzt).