Kavita Update

In einen meiner letzten Beiträge habe ich niedergeschrieben, wie Kavita, ein E-Book Server, installiert werden kann. Natürlich muss dieser immer wieder Updates erhalten. Auch wenn Kavita selbst eine Update-Seite hat, so habe ich dies dennoch etwas anders gehandhabt. Vorteil meiner Kavita Update Variante ist, dass ich ein schnell verfügbares Backup habe, auf das ich direkt zurückgreifen kann, wenn das Update fehlschlägt.
Wer Kavita wie in meinem Beitrag installiert hat, kann hier einfach folgen. Andere müssten gegebenenfalls kleinere Anpassungen treffen.

Zuerst müssen wir den Dienst stoppen, was wir mit systemctl schnell erledigt haben und gehen in das Elternverzeichnis von Kavita.

systemctl stop kavita
cd /opt

in /opt sollte ein Verzeichnis namens Kavita existieren. In diesem Ordner sind alle benötigten Dateien für unseren E-Book Server. Diesen moven wir weg und holen uns die neuste Version von der Kavita Seite, zum Zeitpunkt dieses Beitrages ist es die Version 0.8.2.

mv Kavita Kavita-
wget https://github.com/Kareadita/Kavita/releases/download/v0.8.2/kavita-linux-x64.tar.gz

Sobald wir das gezippte Archiv heruntergeladen und entpackt haben, sollte ein neues Kavita Verzeichnis unter /opt existieren. In diesem müssen wir das config Verzeichnis löschen, damit wir unsere alten Einstellungen und die Datenbank übernehmen können. Ich nehme schonmal vorweg, das es nicht über ein Copy & Paste hinausläuft.

tar xfvz kavita-linux-x64.tar.gz
rm -rf Kavita/config/
cp -pr Kavita-/config/ Kavita

Das Update ist soweit schon durch, fehlen nur noch die E-Books, die wir vorher auf den Server hatten. Diese können ebenfalls mit einem copy in unsere neue Kavita Umgebung abgelegt werden. Bei mir es nur das „books“ Verzeichnis, es mag bei euch etwas anders aussehen.

cp -p Kavita-/books/ Kavita

Zum Schluss vergeben wir noch die korrekten Berechtigungen, damit unser kavita User den Server starten und auf die weiteren Dateien bzw. Ordner zugreifen kann, gefolgt vom Start des Servers.

chmod 754 kavita:kavita
systemctl start kavita
systemctl status kavita

externe USB-SSD in Proxmox einbinden

In diesem Beitrag zeige ich euch, wie ihr eine externe USB-SSD in Proxmox einbinden könnt. Nur ein einfaches „Plug and Play“ ist es in diesem Fall nicht. Die Disk muss vorweg korrekt formatiert und gemountet werden, damit Proxmox die neue Platte finden kann – Proxmox kann dies nicht out of the box.

Als erstes ermitteln wir die USB-SSD mit fdisk. Meist ist sie als eine /dev/sda, wie in meine Fall, oder ähnlich eingebunden.

fdisk -l

Disk /dev/sda: 1.82 TiB, 2000365371904 bytes, 3906963617 sectors
Disk model: Extreme 55AE
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 1048576 bytes
Disklabel type: gpt
Disk identifier: 5623AD37-4AA9-4134-BC1A-565998A00786

Device     Start        End    Sectors  Size Type
/dev/sda1   2048 3906961568 3906959521  1.8T Linux filesystem

/dev/sda1 ist die Partition und diese formatieren wir in eine ext4 Dateisystem.

mkfs.ext4 /dev/sda1

mke2fs 1.47.0 (5-Feb-2023)
Discarding device blocks: done
Creating filesystem with 488369940 4k blocks and 122093568 inodes
Filesystem UUID: c1408055-3603-45c5-8e7a-ca793faa6011
Superblock backups stored on blocks:
        32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208,
        4096000, 7962624, 11239424, 20480000, 23887872, 71663616, 78675968,
        102400000, 214990848

Allocating group tables: done
Writing inode tables: done
Creating journal (262144 blocks): done
Writing superblocks and filesystem accounting information: done

Verläuft die Formatierung erfolgreich, mounten wir die Platte in ein angelegtes Verzeichnis hinein. Den Mount-Point könnt ihr euch dabei selber aussuchen.

mkdir /mnt/ext_ssd
mount /dev/sda1 /mnt/ext_ssd/

Nachdem die Platte gemountet ist, ist es nun über Proxmox möglich, diese einzubinden. Jedoch würde der Mount nach einem Neustart wieder verschwinden. Abhilfe schafft ein Eintrag in die /etc/fstab. Dazu ermitteln wir die UUID unserer neu erzeugten Partition

root@pve:/mnt/ext_ssd# blkid | grep sda1
/dev/sda1: UUID="c1408055-3603-45c5-8e7a-ca793faa6011" BLOCK_SIZE="4096" TYPE="ext4" PARTLABEL="Extreme SSD" PARTUUID="367c946e-fc02-4fd0-be7d-085323bbc320"

root@pve:/mnt/ext_ssd# systemctl daemon-reload

Haben wir die UUID ausfindig gemacht, setzen wir einen neuen Eintrag in /etc/fstab. Ihr könnt dabei die Zeile von unten kopieren, müsst allerdings die UUID und den Mount-Point, hier /mnt/ext_sdd, an eure Gegebenheiten anpassen. Falls ihr ein anderes Dateisystem als ext4 genommen habt, müsst ihr auch diese anpassen.

root@pve:/mnt/ext_ssd# vi /etc/fstab
UUID=c1408055-3603-45c5-8e7a-ca793faa6011 /mnt/ext_ssd ext4 auto,nofail,sync,users,rw   0   0

Nach dem die Zeile gesetzt ist, überprüft, ob die Partition gemountet werden kann

root@pve:/mnt/ext_ssd# mount -a

Sollte kein Fehler kommen und im besten Falle nur die nachfolgende Meldung, könnt ihr den daemon neu laden und die Partition sollte bei jedem Neustart mit gemountet werden.

mount: (hint) your fstab has been modified, but systemd still uses<br>the old version; use 'systemctl daemon-reload' to reload.

Nach der relativ einfachen Initialisierung der USB-SSD auf Betriebssystemebene, machen wir die Platte jetzt noch Proxmox bekannt, damit wir dort Images, ISOs, etc. ablegen können. Geht dazu auf oberste Ebene „Rechenzentrum“ und fügt einen neuen Storage vom Typ „Verzeichnis“ hinzu. Setzt eine ID und in das Verzeichnis-Feld den Mount-Point, sowie die Labels.

Auf euren Knoten sollte der neu eingefügte Storage sichtbar sein. Dort könnt ihr sehen, wieviel Speicher noch frei ist und andere wichtige Informationen

Damit könnt ihr eure externe SSD als Datenspeicher in Proxmox benutzen.

Weblog Auswertung

Es gibt genügend Weblog Dateien, die einem ungefilterte Informationen zu bestimmten Dingen geben. Eine davon ist die von meiner emslandmap auf unser-schoenes-emsland. Auch wenn ich die Logs in diesem Fall selber schreiben lasse, so ist die Masse nicht immer zielführend für Auswertungen und ein simples durchforsten ist meist aufwendig. Zum Glück gibt es Möglichkeiten die Logs mit einer Shell zielgenauer zu filtern. Dies hab ich mit dem Skript aus diesem Beitrag gemacht.

Am Anfang stehen ein paar Variablen, die vorher gesetzt werden. Das Skript sucht nach Zeilen mit dem Schlagwort „REMOTE IP“. Wie solch eine Zeile aussieht seht ihr hier:

2024-01-18T19:51:55+01:00 [INFO] : [get.php][0c0a67801091f35ceb028810110daf3a44ddaebc]REMOTE IP: 8.8.8.8

Neben der IP lasse ich noch ebenfalls das Datum + Uhrzeit rausziehen. Das passiert mit cut -s -d: -f1-3,6. Der Delimiter wurde auf „:“ geändert. Ausgegeben werden die ersten 3 Felder für das Datum + Uhrzeit, sowie die 6 für die IP. Mittels uniq -s 34 werden gleiche IP Adressen zusammengezählt. Diese Info geht in die .CSV Datei.

Als nächstes ermittle ich die Anzahl der eindeutigen IP Adressen und schreibe es in die Variabel uniqip. Wir nehmen Teile des ersten Befehls mit dem Unterschied, dass die IPs sortiert und mit wc -l gezählt sind. Auch diese Info kommt in die .CSV Datei.

Die jetzt vollständige Datei geht mit einer Mail an die angegebene Adresse. Zum Schluss noch ein paar Aufräumarbeiten, in der wir die .CSV Datei löschen und die Weblog umbenennen.

#!/bin/bash

logfile=/pfad/name.log
archivfile=/pfad/name_$(date +"%d_%m_%Y").log
textfile=emslandmap_$(date +"%d_%m_%Y").csv
receiver=receiver@tld.de
sender=sender@tld.de

grep "REMOTE IP" ${logfile} | cut -s -d: -f1-3,6 | uniq -s 34 >> ${textfile}
uniqip=$( grep "REMOTE IP" ${logfile} | cut -s -d: -f6| sort -n | uniq | wc -l )
echo "Einzigartige IP-Adressen: ${uniqip}" >> ${textfile}

echo "Auswertung " | mail -r ${sender} -s "Betreff Webaufrufe" -a ${textfile} ${receiver}

rm -f ${textfile}
mv ${logfile} ${archivfile}

exit 0

Das Skript speichern wir ab und legen es in einem Verzeichnis, um es per Cronjob automatisch ausführen zu lassen.

Virtualbox – Internes Netzwerk

Virtualbox stellt einige Netzwerkmodis bereit, unteranderem das „Internes Netzwerk“. Dieses Netzwerk bietet den Vorteil, dass die virtuellen Maschinen, die dem selben Netzwerk zugeordnet sind, nur untereinander kommunizieren können. Keine Verbindung nach draußen oder dem Host, ein komplett für sich gekapseltes Netz. Für mein Pentest Labor wollte ich genau solch ein Netzwerk, sodass die Maschinen von vulnhub, etc. nicht auf andere Maschinen zugreifen können. Das neu aufgebaute Netz sollte später wie im Schaubild agieren. Über eine virtuelle Switch sind die 2 VMs verbunden. Der Host befindet sich in einem anderen Netzbereich und hat somit keinen Zugriff.

Aufbau des internen Netzes von Virtualbox

Die untere Tabelle zeigt euch, was jeweils mit dem gewählten Netzwerkmodus möglich ist. Das „Internes Netzwerk“ zeigt hier ganz klar, dass nur mit anderen VMs kommuniziert werden kann.

ModeVM -> HOSTVM <- HOSTVM1 < – > VM2VM -> Net/LANVM <- Net/LAN
Host-only+++
Internes Netzwerk+
Netzwerkbrücke+++++
NAT+Port forward+Port forward
NATservice+Port forward++Port forward

Um eure VM dafür vorzubereiten, muss der Netzwerkadapter abgeändert werden. Wählt dazu den Typ „Internes Netzwerk“. Den Namen könnt ihr frei wählen oder nehmt den Default „intnet“. Ich habe meines „hacking“ genannt.

Das wars eigentlich schon für die VM. Wer mir bis hierhin gefolgt ist und die Maschinen schon gestartet hat, wird aber schnell feststellen, dass das neue Interface keine IP Adresse hat und somit die VMs eben nicht miteinander kommunizieren können. Es gibt nun zwei Möglichkeiten, statische IP Adressen vergeben oder einen DHCP Server mittels Virtualbox aufsetzen.

Da der DHCP Server die komfortabelste Lösung ist, habe ich einen neuen erstellt, der für das interne Netwerk „hacking“ arbeitet.

c:\Program Files\Oracle\VirtualBox>VBoxManage.exe dhcpserver add --network=hacking --server-ip=192.168.180.2 --netmask=255.255.255.0 --lower-ip=192.168.180.50 --upper-ip=192.168.180.100 --enable

Startet jetzt die VMs. Sie sollten nun eine Adresse im definierten Bereich erhalten haben. Damit hättet ihr in VirtualBox ein Netzwerk von Typ „Internes Netzwerk“ angelegt.

Weitere optionale VirtualBox Befehle

Mit diesem Befehl zeigt ihr alle verfügbaren DHCP Server an.

VBoxManage.exe list dhcpservers

Mit intnets werden alle Netze angezeigt, die vom Typ „internes Netzwerk“ sind.

VBoxManage.exe list intnets

Kavita auf dem Raspberry Pi installieren

Wer viele eBooks besitzt und diese gerne zentralisiert speichern möchte, hat vielleicht schon etwas von Kavita gehört. Es ist ein open-source Buchserver der viele eBook-Formate lesen kann. Dabei werden unterschiedliche Plattformen wie Linux, Windows und Docker unterstützt. Auch für den Raspberry Pi gibt es eine ARM Version, die ich für meinen Raspberry PI 4 benötige.
Die Installation ist relativ einfach. Ladet mit wget die richtige Version für euch herunter. Nicht zwingend erforderlich aber doch von Vorteil ist die Anlage eines neuen Benutzers, der die erforderlichen Rechte auf dem Verzeichnis hat.

cd /opt/
sudo wget https://github.com/Kareadita/Kavita/releases/download/v0.7.8/kavita-linux-arm.tar.gz
sudo tar xfvz kavita-linux-arm.tar.gz
sudo useradd kavita
sudo passwd kavita
sudo chown -R kavita:kavita Kavita/

Wer Kavita nicht immer manuell starten möchte, kann auch einen eigenen systemd Service generieren. Legt dafür einfach eine neue kavita.service Datei unter /etc/systemd/system an und kopiert den nachfolgenden Block hinein.

[Unit]
Description=Kavita Server
After=network.target

[Service]
User=kavita
Group=kavita
Type=simple
WorkingDirectory=/opt/Kavita
ExecStart=/opt/Kavita/Kavita
TimeoutStopSec=20
KillMode=process
Restart=on-failure

[Install]
WantedBy=multi-user.target

Danach könnt ihr die Systemd Konfigurationsdateien und Units neu laden. Die zweite Codezeile startet Kavita und die dritte fügt es zum Startup hinzu.

sudo systemctl daemon-reload
sudo systemctl start kavita.service
sudo systemctl enable kavita.service

Falls ihr keinen neuen Service definieren wollt, könnt ihr Kavita wie folgt, über den vorher neu erstellten User, starten.

/opt/Kavita# sudo su kavita
kavita@raspberrypi4:/opt/Kavita$ ./Kavita
[Kavita] [2023-10-12 20:34:23.500 +02:00  1] [Information] API.Services.TaskScheduler Scheduling reoccurring tasks
[Kavita] [2023-10-12 20:34:23.686 +02:00  1] [Debug] API.Services.TaskScheduler Scheduling Scan Library Task for daily
[Kavita] [2023-10-12 20:34:24.433 +02:00  1] [Debug] API.Services.TaskScheduler Scheduling Backup Task for daily
[Kavita] [2023-10-12 20:34:24.607 +02:00  1] [Information] API.Services.TaskScheduler Scheduling Auto-Update tasks
[Kavita] [2023-10-12 20:34:24.615 +02:00  1] [Debug] API.Services.TaskScheduler Scheduling stat collection daily
[Kavita] [2023-10-12 20:34:24.880 +02:00  4] [Information] API.Program Running Migrations
[...]

Öffnet nun die Seite mit http://raspberrypi4:5000 in eurem Browser. Kavita lauscht standardmäßig auf den Port 5000. Sollte dies nicht klappen, überprüft, ob der Prozess überhaupt gestartet ist. Im nächsten Codeblock zeige ich einmal den Weg mittels systemctl und den ohne. Mit Systemctl sollte ein Active (running) vorhanden sein, sowie eine PID. Unabhängig, ob ihr das mit systemctl macht, könnt ihr auch mit „ps“ überprüfen, ob ein Prozess läuft. Mittels „netstat“ überprüft ihr letztendlich, ob ein Service auf den Port 5000 lauscht.

]pi@raspberrypi4:/opt/Kavita# sudo systemctl status kavita.service
● kavita.service - Kavita Server
   Loaded: loaded (/etc/systemd/system/kavita.service; enabled; vendor preset: enabled)
   Active: active (running) since Thu 2023-10-12 20:37:07 CEST; 2s ago
 Main PID: 27434 (Kavita)
    Tasks: 16 (limit: 4915)
   CGroup: /system.slice/kavita.service
           └─27434 /opt/Kavita/Kavita

Okt 12 20:37:07 raspberrypi4 systemd[1]: Started Kavita Server.


[Do Okt 12]pi@raspberrypi4:/opt/Kavita# ps aux | grep Kavita
kavita   27434  103  3.4 900748 134900 ?       Rsl  20:37   0:13 /opt/Kavita/Kavita
pi       27585  0.0  0.0   7356   552 pts/0    S+   20:37   0:00 grep --color=auto 27434


[Do Okt 12]pi@raspberrypi4:/opt/Kavita# sudo netstat -tapen | grep 27434
tcp6       0      0 :::5000                 :::*                    LISTEN      1001       75764786   27434/Kavita
tcp6       0      0 192.168.170.33:35342    140.82.121.5:443        VERBUNDEN   1001       75766833   27434/Kavita
[Do Okt 12]pi@raspberrypi4:/opt/Kavita#

Sollte ein Prozess existieren und dieser auf Port 5000 lauschen, ist die Installation abgeschlossen und ihr solltet auf die Startseite kommen. Legt zuerst einen neuen User an und definiert danach eine neue Library, welches auf euer Verzeichnis mit den eBooks zeigt. Das Einlesen könnte, je nach Anzahl eurer eBooks, etwas dauern. Ihr habt die Wahl eure eBooks direkt im Browser zu lesen oder diese lokal auf eure Geräte herunterzuladen.

Hier findet ihr noch die Github Seite des Projekts: https://github.com/Kareadita/Kavita

Wer ein Update machen möchte findet hier den richtigen Beitrag: Kavita Update

Zahlen erkennen mit Python

In diesem Artikel möchte ich zeigen, wie man Zahlen mit Python erkennen kann. Das Touchpad vom Laptop benutze ich, um Zahlen zu zeichnen. Dafür verwende ich keras und Tensorflow und einen eigenen Datensatz zum Trainieren und Validieren.

Datensatz erstellen

Für den Datensatz verwende ich folgenden Python Code. Die Ordnerstruktur muss vorher angelegt sein:

import cv2 as cv
import pyautogui
import numpy as np
import keyboard
import string
import random
from time import sleep


def find_minmax(pic):
    # find the min,max of x,y
    colored = np.where(pic != [0,0,0])
    freespace = 10
    min_y = np.amin(colored[0]) - freespace
    max_y = np.amax(colored[0]) + freespace
    min_x = np.amin(colored[1]) - freespace
    max_x = np.amax(colored[1]) + freespace
    
    return min_y,max_y,min_x,max_x

def main():
    screenWidth, screenHeight = pyautogui.size()
    prevmouseX = prevmouseY = 0
    dim=(28,28)

    for n in range(0,10,1):
        for d in range(0,1,1):
            print(f'Zahl: {n} \nAnzahl: {d}')
            # create np array with a size of max windows resolution
            y = np.zeros([screenHeight,screenWidth,3],dtype=np.uint8)
            # make array image black
            pic = np.full_like(y,[0,0,0])
            while not keyboard.is_pressed('esc'):
                # get mouse position
                mouseX, mouseY = pyautogui.position()
                #print(mouseX,mouseY)
                # make the drawing lines bigger
                for i in range(-5,5):
                    pic[mouseY ,mouseX + i] = [255,255,255]
                    pic[mouseY + i, mouseX] = [255,255,255]
                      
            sleep(0.1)
            # find min and max of each axis(x,y)
            min_y,max_y,min_x,max_x = find_minmax(pic)
            picresized = pic[min_y:max_y,min_x:max_x]
            picresized = cv.resize(picresized,dim)
            randomstring = ''.join(random.choices(string.ascii_lowercase,k=8))
            #print(f"create file {n}/{d}{randomstring}.png")
            #cv.imwrite(f'./{n}/{d}{randomstring}.png',picresized)
            print(f"create file Test/{d}{randomstring}.png")
            cv.imwrite(f'./Test/{d}{randomstring}.png',picresized)
        


if __name__ == '__main__':
    main()

Für jede einzelne Ziffer gibt es mindestens 50 Datensätze, insgesamt habe ich 554 Bilder erstellt. Dabei könnte man die Prozedur in 3 Teile gliedern. Die erste Prozedur besteht darin die Zahl zu zeichnen. Dabei entsteht ein Bild von der Größe des Displays, bei mir 1920×1080. Dieses Bild wird dann reduziert auf den Zahlenbereich, bevor es dann auf eine Größe von 28×28 reduziert wird. Damit hätten wir einen vollständigen Datensatz, mit dem wir unser Model trainieren können

Das Model erstellen und trainieren

Für die Erstellung des Models verwende ich Tensorflow und Keras.Sequential. Dabei bin ich nach dem Tutorial von tensorflow.org vorgegangen.

import numpy as np
import matplotlib.pyplot as plt
import PIL
import tensorflow as tf

from tensorflow import keras
from tensorflow.keras import layers
from tensorflow.keras.models import Sequential

import pathlib

def visualize_training_results(history,epochs):
    acc = history.history['accuracy']
    val_acc = history.history['val_accuracy']

    loss = history.history['loss']
    val_loss = history.history['val_loss']

    epochs_range = range(epochs)

    plt.figure(figsize=(8, 8))
    plt.subplot(1, 2, 1)
    plt.plot(epochs_range, acc, label='Training Accuracy')
    plt.plot(epochs_range, val_acc, label='Validation Accuracy')
    plt.legend(loc='lower right')
    plt.title('Training and Validation Accuracy')

    plt.subplot(1, 2, 2)
    plt.plot(epochs_range, loss, label='Training Loss')
    plt.plot(epochs_range, val_loss, label='Validation Loss')
    plt.legend(loc='upper right')
    plt.title('Training and Validation Loss')
    plt.show()

data_dir = pathlib.Path('C:/Users/Willkommen/Desktop/handdigits/Modelcreation')

image_count = len(list(data_dir.glob('*/*.png')))
print(image_count)

batch_size = 32
img_height = 28
img_width = 28

# create dataset
train_ds = tf.keras.utils.image_dataset_from_directory(
    data_dir,
    validation_split=0.2,
    subset='training',
    seed=123,
    image_size=(img_height, img_width),
    batch_size=batch_size
)

val_ds = tf.keras.utils.image_dataset_from_directory(
    data_dir,
    validation_split=0.2,
    subset='validation',
    seed=123,
    image_size=(img_height, img_width),
    batch_size=batch_size
)

class_names = train_ds.class_names
print(class_names)

# Configure dataset for performance
AUTOTUNE = tf.data.AUTOTUNE

train_ds = train_ds.cache().shuffle(20).prefetch(buffer_size=AUTOTUNE)
val_ds = val_ds.cache().prefetch(buffer_size=AUTOTUNE)

# Standardize the data
normalization_layer = layers.Rescaling(1./255)

normalized_ds = train_ds.map(lambda x, y: (normalization_layer(x), y))
image_batch, labels_batch = next(iter(normalized_ds))
first_image =  image_batch[0]
print(np.min(first_image), np.max(first_image))

num_classes = len(class_names)

# Create the model
model = Sequential([
    layers.Rescaling(1./255, input_shape=(img_height, img_width, 3)),
    layers.Conv2D(16,3, padding='same', activation='relu'),
    layers.MaxPooling2D(),
    layers.Conv2D(32,3, padding='same', activation='relu'),
    layers.MaxPooling2D(),
    layers.Conv2D(64,3, padding='same', activation='relu'),
    layers.MaxPooling2D(),
    layers.Flatten(),
    layers.Dense(128, activation='relu'),
    layers.Dense(num_classes)
])
# compile model
model.compile(
    optimizer='adam',
    loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    metrics=['accuracy']
)

model.summary()

## Train the model

epochs = 10
history = model.fit(
    train_ds,
    validation_data = val_ds,
    epochs=epochs
)

## Visualize Training results
visualize_training_results(history, epochs)

#Predict on new data
data_dir = pathlib.Path('C:/Users/Willkommen/Desktop/handdigits/Test/0nzzlfidh.png')
img = tf.keras.utils.load_img(
    data_dir, target_size=(img_height, img_width)
)

img_array = tf.keras.utils.img_to_array(img),
img_array = tf.expand_dims(img_array[0], 0) # Create a batch
predictions = model.predict(img_array)
score = tf.nn.softmax(predictions[0])

print(
    "This number most likely belongs to {} with a {:.2f} percent confidence."
    .format(class_names[np.argmax(score)], 100 * np.max(score))
)

# save model
model.save('C:/Users/Willkommen/Desktop/handdigits/ownmodel.h5')

Wir laden die Daten und splitten die Datensätze im Verhältnis 80% zu 20% in Trainings- und Validierungsdaten. Der Klassenname bezieht sich auf den Ordnername, also die Zahlen von 0 – 9. Die Datensätze ziehen wir uns von der Festplatte in den Memory Cache. Dies hat den Vorteil das etwaige I/O auf der Platte nicht zu einem Flaschenhals führen, wenn wir das Model trainieren.
Da der RGB Channel Werte zwischen 0 – 255 hat, normalisieren wir diese noch, sodass ein Wert zwischen 0 und 1 besteht.
Nach der Normalisierung erstellen wir unser Keras Sequential Model mit drei convolution Blöcken. Jeder hat zusätzlich ein max pooling layer.
Zum Kompilieren benutzen wir den Adamoptimizer und die SparseCategoricalCrossentropy loss Funktion.
In insgesamt 10 Epochen trainieren wir unser Model. Zur Visualisierung des Trainingsergebnisses mittels matplotlib gibt es die Methode visualize_training_results.
Um unser gerade neu erstelltes Model zu testen, benutzen wir das Model auf neu erstellte Testdaten.

Nachfolgend könnt ihr die Validierung vom Training sehen. Das Model scheint dabei gut zu performen.

Modelvalidierung

Mit Touchpad zeichnen

Das Model für die Zahlenidentifizierung haben wir nun und wir könnten die Zahlen mit Python erkennen. Fehlt lediglich das Zeichnen mit dem Touchpad. Dafür verwende ich die Bibliothek „keyboard“. Diese ermittelt die aktuelle Mausposition und liefert X,Y Koordinaten. Jene X,Y Koordinaten ändern die Werte im Numpy Array und haben statt dem Wert [0,0,0] den Wert [255,255,255]. Wie auch beim Trainieren des Models, entspricht das erste Bild der Gesamtgröße des Bildschirms. Das zweite ist nur der Zahlenbereich und das dritte und finale Bild, der Zahlenbereich auf die Größe 28,28 reduziert.
Nachdem wir nun das 28×28 Bild-Array haben, rufen wir die Methode predict_num(img) auf. In der Methode wird das Bild noch einmal auf die korrekte Größe gebracht, ehe es dann durch das Model bewertet wird.

import cv2 as cv
import keyboard
import pyautogui
import numpy as np
import tensorflow as tf
from keras.models import load_model

model = load_model('ownmodel.h5')

def predict_num(img):
    # reshape for model compatibility
    img = img.reshape(1,28,28,3)
    #predicting the class
    img = tf.expand_dims(img[0], 0) # Create a batch
    predictions = model.predict(img)
    score = tf.nn.softmax(predictions[0])

    print(
    "You draw number {} with a {:.2f} percent confidence."
    .format(np.argmax(score), 100 * np.max(score))
    )


def find_minmax(pic):
    # find the min,max of x,y
    colored = np.where(pic != [0,0,0])
    freespace = 10
    min_y = np.amin(colored[0]) - freespace
    max_y = np.amax(colored[0]) + freespace
    min_x = np.amin(colored[1]) - freespace
    max_x = np.amax(colored[1]) + freespace
    
    return min_y,max_y,min_x,max_x
 
def main():
    screenWidth, screenHeight = pyautogui.size()
    prevmouseX = prevmouseY = 0
    firstrun = True
    dim=(28,28)

    # create np array with a size of max windows resolution
    y = np.zeros([screenHeight,screenWidth,3],dtype=np.uint8)
    # make array image black
    pic = np.full_like(y,[0,0,0])
    
    while not keyboard.is_pressed('esc'):
        if firstrun:
            print("Draw your number now and confirm with ESC")
            firstrun=False

        # get mouse position
        mouseX, mouseY = pyautogui.position()
        # make the drawing lines bigger
        for i in range(-5,5):
            pic[mouseY ,mouseX + i] = [255,255,255]
            pic[mouseY + i, mouseX] = [255,255,255]

    cv.imwrite('./original.png',pic)

    # find number and reduce picturesize to a minimum
    min_y,max_y,min_x,max_x = find_minmax(pic)
    picresized = pic[min_y:max_y,min_x:max_x]
    cv.imwrite('./originalcut.png',picresized)

    # resize image to (28,28)
    picresized = cv.resize(picresized,dim)
    cv.imwrite('./resized.png',picresized)

    #predict
    predict_num(picresized)

if __name__ == '__main__':
    main()

Testbeispiel

Hier findet ihr ein Beispiel, bei dem ich die 3 über das Touchpad gezeichnet habe. Folgend findet ihr das original Bild, nur den Zahlenbereich und den Zahlenbereich auf 28×28 herunterskaliert. Das Model hat dabei folgendes ausgegeben:

Draw your number now and confirm with ESC
1/1 [==============================] - 0s 417ms/step
You draw number 3 with a 99.95 percent confidence.

Und hier jetzt einmal die Bilder

Den ganzen vollständigen Code findet ihr bei mir auf github: https://github.com/stevieWoW/neuronalnetworkmousepadnumber

opencv template matching

Viele von euch kennen wahrscheinlich die „Where’s Waldo“, bzw. auf deutsch, „Wo ist Walter“ Bilder. Bei den Bildern geht es einzig darum Walter unter den Unmengen anderer Personen zu finden. Teils gar nicht so einfach. Ich habe hier ein Beispiel, womit wir Walter anhand opencv template matching finden und ihn kenntlich machen. Dazu habe ich 2 Bilder vorbereitet. Das erste ist das Gesicht vom Walter.

Wo ist Walter Gesicht

Nach diesem kleinen Schnipsel wird opencv nachher das komplette Bild absuchen.

Wo ist Walter

Wer natürlich Lust hat, kann Walter vorher suchen. Diese Arbeit musste ich vorab schon machen, um das Gesicht als Bild zu haben, die Arbeit habt ihr euch also schon gespart :).

Voraussetzungen

An sich gibt es nicht viele Voraussetzungen. Lediglich opencv, matplotlib und numpy müssen installiert sein.

pip install opencv-python
pip3 install matplotlib
pip3 install numpy

template matching

Für das template matching gibt es in opencv die Funktion cv.matchTemplate(). Diese erwartet das komplette Bild, den Schnipsel und eine Vergleichsmethode als Parameter übergeben. Vergleichsmethoden können TM_SQDIFF, TM_SQDIFF_NORMED, TM_CCORR, TM_CCORR_NORMED, TM_CCOEFF und TM_CCOEFF_NORMED sein. Jede Methode verwendet einen unterschiedlichen Berechnungsweg, um an das Ziel zu gelangen. Das Ergebnis ist ein weiteres numpy.ndarray der Größe (W-w +1) x (H -h +1). W/H ist die Größe des vollständigen Bilds und w/h die des Schnipsels. Hier einmal als Vergleich.

vollständige Bild(H x W): 
print(img_rgb.shape)
-> (1760, 2800, 3)

Schnipsel(h x w): 
print(face.shape)
-> (22, 22)

Ergebnis (H - h + 1) x (W - w + 1): 
print(res.shape)
-> (1739, 2779)

Die Vergleichsmethode schiebt das Schnipsel über das große Bild und vergleicht jeweils die gerade übereinanderliegenden Stellen von der Größe w x h und schreibt den Wert in das res numpy.ndarray. Bei allen Vergleichsmethoden außer TM_SQDIFF oder TM_SQDIFF_NORMED gilt, je höher der Wert desto mehr sieht sich die gerade überlappende Stelle ähnlich. Für diesen Beitrag habe ich TM_CCOEFF_NORMED verwendet, spielt letzten Endes aber keine große Rolle.

import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt

face = cv.imread('waldoface.jpg',0)
img_rgb = cv.imread('whereiswaldo.jpg')
img_gray = cv.imread('whereiswaldo.jpg',0)
w,h = face.shape[::-1]

res = cv.matchTemplate(face,img_gray,cv.TM_CCOEFF_NORMED)

Nachdem wir nun das Ergebnis der Vergleichsmethode haben, können wir uns den besten Treffer mit der Funktion cv2.minMaxLoc() aus dem ndarray extrahieren.

min_val, max_val, min_loc, max_loc = cv.minMaxLoc(res)
print(min_val, max_val, min_loc, max_loc)
-> -0.5772402882575989 0.9950668215751648 (912, 212) (1169, 298)

cv2.minMaxLoc() sucht sich den kleinsten und größten Wert mit jeweils dem Index raus. Da unser ndarray aus mehr als 1000 Zeilen und Spalten besteht, verdeutliche ich die funktionsweise von cv2.minMaxLoc() kurz anhand eines kleineren Arrays.

array = [
    [1,4,6,8,9], #0 Dimension
    [2,5,4,6,5], #1
    [6,4,3,6,5]  #2
    #0,1,2,3,4
]

min_val,max_val,min_indx,max_indx=cv2.minMaxLoc(array)
print(min_val,max_val,min_indx,max_indx)
-> 1.0, 9.0, (0, 0), (4, 0)

Anhand dieses Beispiels können wir sehen, dass der kleinste Wert (=1.0) das 0 Element im der 0 Dimension ist. Ein Array startet üblicherweise immer mit 0. Der größte Wert(=9.0) ist das 4 Element in der 0 Dimension.
Genau das gleiche machen wir mit unserem res ndarray. Unser Waldo müsste dementsprechend beim Index (1169, 298) liegen. Wir gehen also zu diesem Punkt im Bild und zeichnen von dort an ein Rechteck der Größe 22 x 22 (w x h).

top_left = max_loc
bottom_right = (top_left[0] + w, top_left[1] + h)
print(top_left, bottom_right)
-> (1169, 298) (1191, 320)

cv.rectangle(img_rgb,top_left,bottom_right, (0, 255, 0), 2)
cv.putText(img_rgb,'Waldo', bottom_right, cv.FONT_HERSHEY_TRIPLEX,3, (0,255,0),3)
plt.imshow(img_rgb)
plt.suptitle('Ergebnis')
plt.show()

Das Ergebnis zeigen wir uns dann mittels matplotlib an. Wer das Bild noch abspeichern möchte kann dies mittels opencv machen.

cv.imwrite('Waldogefunden.jpg', img_rgb)

Damit haben wir Waldo mittels template matching im Bild gefunden und kenntlich gemacht.

template Matching Waldo gefunden

Den vollständigen Code könnt ihr auf meinem Github finden.

Nabu Vogelliste als Pandas Dataframe

Die Nabu hält alljährlich eine Gartenvögelzählung ab, bei denen unter anderem private Personen gesichtete Vögel an einem Tag zählen und übermitteln können. Diese Liste stellt die Nabu dann auf deren Internetpräsenz zur Verfügung. Angezeigt wird die Vogelart, die Anzahl, die Vögel pro Garten und ein Vergleich zum Vorjahr. Filtern könnt ihr nach Jahr, Bundesland, Vogelart und Landkreis/Stadt.
Dies vorab als kleine Beschreibung, um was für eine Liste es sich handelt, die wir uns zu eigen machen. Am einfachsten lässt sich die Nabu Vogelliste als Pandas Dataframe darstellen, wenn wir BeautifulSoup benutzen. Mit Hilfe dieser Bibliothek lassen sich XML und HTML Dokumente parsen. Für die Abarbeitung benötigen wir also eine URL, die wir für unseren Get-Request benutzen können. Mitinbegriffen sind in der unten angegebenen URL schon die Filtereinstellungen. So sind nur die Ergebnis für das Jahr 2021 enthalten, die im Bundesland Niedersachsen und dem Ort Emsland(034540000000) vorkommen. Wer die komplette Liste haben möchte oder sich seine eigene URL generieren möchte, kann dies mit diesem Link machen.

https://www.nabu.de/tiere-und-pflanzen/aktionen-und-projekte/stunde-der-gartenvoegel/ergebnisse/15767.html?jahr=2021&bundesland=Niedersachsen&vogelart=&ort=034540000000

In unseren Python Code importieren wir die benötigten Bibliotheken. Eventuell müsst ihr BeautifulSoup noch nachträglich installieren, darauf gehe ich hier nicht weiter drauf ein.

from bs4 import BeautifulSoup
import requests
import pandas as pd

Zuerst holen wir uns das HTML Dokument und initialisieren BeatifulSoup. Danach suchen wir nach einem <tbody> im HTML DOM und gehen die einzelnen Tabellenfelder durch. Das Ergebnis schreiben wir vorerst in eine Liste. Diese brauchen wir später für unseren Dataframe, da wir aus der Liste das Dataframe erzeugen.

tbody = soup.find('tbody')
vogel_df = pd.DataFrame()
vogelliste = list()

for i in tbody:
   
    try:
        vogelliste.append([
            i.find_next('tr').find_next('td').find_next('td').getText(),
            i.find_next('tr').find_next('td').find_next('td').find_next('td').getText(),
            i.find_next('tr').find_next('td').find_next('td').find_next('td').find_next('td').getText(),
            i.find_next('tr').find_next('td').find_next('td').find_next('td').find_next('td').find_next('td').getText(),
            i.find_next('tr').find_next('td').find_next('td').find_next('td').find_next('td').find_next('td').find_next('td').getText(),
            i.find_next('tr').find_next('td').find_next('td').find_next('td').find_next('td').find_next('td').find_next('td').find_next('td').getText()
        ])

    except:
        pass

vogel_df = pd.DataFrame(vogelliste, columns=['Vogelart','Anzahl','Prozent_der_Gärten','Vögel_pro_garten','Vergleich_zum_Vorjahr','Vergleich_zum_Vorjahr(Prozent)'])

Eine Ausgabe des Dataframe würde folgendes ausgeben:

          Vogelart Anzahl Prozent_der_Gärten Vögel_pro_garten Vergleich_zum_Vorjahr Vergleich_zum_Vorjahr(Prozent) 
0     Haussperling   1500             78,79%             5,68                + 0,19                           + 4%  
1            Amsel    885             95,45%             3,35                + 0,21                           + 7%  
2        Kohlmeise    815             85,98%             3,09                + 0,68                          + 28%   
3        Blaumeise    700             81,82%             2,65                + 0,59                          + 28%     
4      Ringeltaube    599             61,74%             2,27                + 0,22                          + 11%  
..             ...    ...                ...              ...                   ...                            ...
79       Sturmmöwe      1              0,38%             0,00                     -                              0
80    Birkenzeisig      1              0,38%             0,00                  0,00                          + 27%   
81      Feldlerche      1              0,38%             0,00                  0,00                          + 27%   
82   Braunkehlchen      1              0,38%             0,00                     -                              0   
83  Waldbaumläufer      1              0,38%             0,00                     -                              0 

Das war es schon, um die Nabu Vogelliste als Pandas Dataframe zu erhalten. Relativ simpel und doch funktionstüchtig.

LED-Streifen mit offline Sprachassistenten steuern

Da ich mittlerweile eine eigene kleine Sprachbox in meinem Beitrag Offline Sprachassistent mit Python erstellt habe, sollte diese natürlich auch Aktionen ausführen. Als erste Aktion wollte ich meine LED-Streifen mit dem offline Sprachassistenten steuern. Dafür habe ich in dem oben genannten Beitrag schon viel Arbeit davon erklärt. Anstatt des Klemmbretts habe ich jetzt eine kleine Platine benutzt und die benötigten Komponenten verlötet. Die Platine habe ich dann in einem alten Raspberry PI Gehäuse verbaut. Bei den LED-Streifen habe ich darauf geachtet, das diese über USB und dementsprechend 5V laufen. Zum Einsatz kommen die Govee LED Streifen mit 3m, die ich bei dem Amazon Prime Days günstig geschossen hatte. Mit meiner Steuerung kann ich den Steifen an und aus schalten. Die Farbe kann ich entweder mit der App oder der Fernbedienung ändern.

Automatischer Start beim Boot

Damit ich die Sprachsteuerung nicht dauerhaft eigenhändig starten muss, habe ich mich dazu entschlossen, dass überprüft wird, ob das Startup Skript läuft und falls nicht, dass es automatisch gestartet wird. Dafür existieren die beiden Skripte run_speech.sh und speech.sh. Beide liegen im /etc/ Verzeichnis und sind ausführbar.

run_speech.sh ist als Cronjob angelegt, der jede 5 Minuten überprüft, ob speech.sh läuft. Sollte dieses aus irgendeinen Grund nicht laufen, startet run_speech.sh speech.sh und schiebt es in Hintergrund.

run_speech.sh
#!/bin/bash

proc='speech.sh'

pid=`ps aux | grep -w ${proc} | grep -v grep`
rc=$?

if [ ! $rc -eq 0 ]
then
        echo "starte speech"
        /etc/speech.sh &>> /var/log/speech/speech`date +%V`.log &
fi

exit

In speech.sh läuft eine Dauerschleife, die dauerhaft überprüft, ob das Python Skript für den Sprachassistenten läuft. Auch hier ist es der Fall, dass speech.py gestartet wird, falls es nicht läuft.

speech.sh
#!/bin/bash

name="speech.py"
waittime=5

if [ ! -d "/var/log/speech" ]
then
                mkdir "/var/log/speech"
fi

while true
do
        process=`ps aux | grep "${name}" | grep -v grep`
        rc=$?

        if [ ! $rc -eq 0 ]
        then
                echo "`date +%Y-%m-%d\ %T` INFO `basename $0` :: starting speech.py..." >> /var/log/speech/speech`date +%V`.log
                cd /home/pi/vosk-api/python/example
                /usr/bin/python3 /home/pi/vosk-api/python/example/speech.py &>> /var/log/speech/speech-detail`date +%V`.log &
                echo "`date +%Y-%m-%d\ %T` INFO `basename $0` :: speech.py was started" >> /var/log/speech/speech`date +%V`.log
        fi
        sleep ${waittime}
done

Den Quellcode, für den Sprachassistenten findet ihr im oben genannten Beitrag.

Video

Zum Schluss noch ein Video, das genau zeigt, wie ich die LED-Streifen mit dem offline Sprachassistenten steuern kann. Klar zu sehen ist, dass der Assistent nur dann reagiert, wenn er das Codewort herausgehört hat. In der speech-detail.log schreibt der Prozess die erkannten Wörter/Sätze nieder. Der Sprachassistenten ist soweit aufgebaut, dass dieser das Codewort nicht aus einem Satz herausfiltert. Dies macht ihn ein wenig anfällig auf im Hintergrund getätigte Gespräche, die das Mikrofone eventuell mit aufzeichnet.

Kali Linux 2021.4 wurde veröffentlich

Mit dem beinahen Ende von 2021 wurde Kali Linux 2021.4 als letztes Release für dieses Jahr veröffentlicht. Seit dem 09.12 ist die Version nun verfügbar und beinhaltet folgende Änderungen:

  • Verbesserte Apple M1 Unterstützung
  • Größere Samba Kompatibilität
  • Package Manager Mirror konfigurieren
  • Kaboxer Themen Unterstützung
  • Neue Tools
  • Desktop & Themen Erweiterungen
  • Kali NetHunter Neuerungen
  • Kali ARM Neuerungen

Verbesserte Apple M1 Unterstützung

Seit Kali 2021.1ist Kali auf dem Apple Silicon Macs lauffähig. Mit 2021.4 unterstützt es nun auch die VMware Fusion Public Tech Preview, dank des 5.14 Kernels, der die Module für die virtuelle GPU bereitstellt. Ebenfalls sind die open-vm-tools auf dem neusten Stand. Der Kali Installer erkennt automatisch die Installation unter VMware und installiert direkt das open-vm-tools-desktop Package. Zurzeit ist dies unter Beobachtung von VMware, es könnten also weiterhin Fehler auftreten.
Virtuelle Maschinen auf dem Apple Silicon sind, wie zuvor, auf ARM64 Installationen limitiert.

Größere Samba Kompatibilität

Mit Kali 2021.4 ist der Samba Client nun zu so gut wie mit allen Samba Servern kompatibel, egal welche Version vom Protokoll benutzt wird. Diese Änderung sollte es vereinfachen, Schwachstellen in Samba Servern zu finden, ohne dafür Kali zu konfigurieren. Mittels den kali-tweaks unter der Rubrik Hardening könnt ihr die Änderungen wieder zurück auf die Default Einstellungen setzen. In diesem Fall werden nur noch die neusten Versionen des Samba Protokolls unterstützt.

Package Manager Mirror konfigurieren

Normalerweise, wenn das Kali System mit apt geupdated wird, kommt eine Verbindung zu einem jeweiligen Community Mirror in der Nähe zustande. Es ist aber schon seit langem möglich, Packages von CloudFlare CDN herunterzuladen. Mit kali-tweaks kann nun zwischen den Community Mirrors und der Cloudflare CDN gewechselt werden.

Kaboxer Themen Unterstützung

Mit dem neusten Kaboxer Update, sieht jenes nicht mehr altbacken aus, da es Unterstützung für die Windows Themen und Icon Themen anbietet. Somit können Programme sich an die Desktopumgebung orientieren und verhindern eher unschöne fallback Themen. Als Beispiel Zenmap mit dem Standard Kali Dark Themen und dem alten Erscheinungsbild.

Quelle: https://www.kali.org/blog/kali-linux-2021-4-release/images/kaboxer-theme-support.png

Neue Tools

Es wäre kein Kali Release, wenn es nicht auch neue Tools gibt:

  • Dufflebag – Durchsuchen von Elastic Block Storage(EBS)
  • Mayam – Open-source Intelligence (OSINT) Framework
  • Name-That-Hash – Hashtyp ermitteln
  • Proxmark3 – RFID hacking
  • Reverse Proxy Grapher – Veranschaulichung des Reverse Proxy Flows
  • S3Scanner – Scannt offenen S3 buckets
  • Spraykatz – Benutzerdatenermittlung für Windows Maschinen und große AD Umgebungen
  • truffleHog – Durchsuchen von git Repositories nach String und anderen Secrets
  • Web of trust grapher (wotmate) – implementiert den veralteten PGP-Pfadfinder neu

Desktop & Themen Erweiterungen

Dieses Release bringt Neuerungen zu allen 3 Desktops, die sind Xfce, GNOME und KDE. Ein Update kommt für alle Desktops und das ist das neue Windows Button Design.

Quelle: https://www.kali.org/blog/kali-linux-2021-4-release/images/new-window-buttons.png
Xfce

Für Xfce gibt es etwas mehr horizontalen Platz, sodass 2 neue Widges: CPU Usage und VPN IP, Platz finden. VPN IP bleibe versteckt, solange es keine VPN Verbindung gibt. Zusätzlich hat sich der Taskmanager verändert und zeigt jetzt nur noch „icons only“ an. Die Panelhöhe wurde ebenfalls leicht erhöht.
Für das Terminal Dropdown Menu steht eine Powershell Verknüpfung zur Verfügung. Ihr könnt nun zwischen dem regulären Terminal, dem Root Terminal und Powershell wählen.

GNOME 41

GNOME hat indes 2 Versionssprünge gemacht. Seit dem letzten großen GNOME Update in Kali ist seither 1 Jahr vergangen. GNOME 40 und GNOME41 sind in der Zwischenzeit released worden. Alle Themen und Erweiterungen unterstützen die neue Shell.

KDE 5.23

Das KDE Team hat dieses Jahr ihr 25 Jähriges Bestehen gefeiert und veröffentlichten die 5.23 vom Plasma Desktop. Dieses Update ist nun in Kali verfügbar und bringt ein neues Design für das Breeze Themen mit.

Das Kali Themen update

All diese Themenänderungen werden nicht automatisch bei einem Kali Upgrade aktiviert. Dies ist aufgrund dessen, dass die Themen Settings unter dem Home Ordner des Users gespeichert sind, als dieser erzeugt wurde. Bei einem Kali upgrade wird das Betriebssystem upgedated aber nicht die persönlichen Dateien. Um die neuen Themen zu erhalten musst du entweder

  • Kali neu installieren
  • einen neuen User erzeugen und mit dem weiterarbeiten
  • das Desktopumgebungsprofil für den Benutzer löschen und ein reboot veranlassen. Hier ein Beispiel anhand von Xfce
kali@kali:~$ mv ~/.config/xfce4{,-$(date +%Y.%m.%d-%H.%M.%S)}
kali@kali:~$
kali@kali:~$ cp -rbi /etc/skel/. ~/
kali@kali:~$
kali@kali:~$ xfce4-session-logout --reboot --fast

Kali NetHunter Neuerungen

Für die Kali NetHunter App steht mit der 2021.4 ein Social-Engineer Toolkit zur Verfügung. Es können nun eigene Facebook, Messenger oder Twitter Emails versendet werden.

Kali ARM Neuerungen

  • Alle Images benutzen nun ext4 für deren root Dateisystem.
  • Support für den Raspberry Pi Zero 2 W. Wie bei dem Raspberry Pi 400 gibt es keine Nexmon Unterstützung
  • Raspberry Pi Images unterstützen nun out of the box das Booten vom USB, da das root device nicht länger hardgecoded ist.

Quelle: https://www.kali.org/blog/kali-linux-2021-4-release/ 

Das Update

Um eine bestehende Installation auf 2021.4 upzudaten, führt folgende Befehle aus

sudo apt update
sudo apt -y full-upgrade
sudo reboot

mit dem nächsten Befehl könnt ihr überprüfen, ob die Version 2021.4 installiert ist

cat /etc/os-release 

PRETTY_NAME="Kali GNU/Linux Rolling"
NAME="Kali GNU/Linux"
ID=kali
VERSION="2021.4"
VERSION_ID="2021.4"
VERSION_CODENAME="kali-rolling"
ID_LIKE=debian
ANSI_COLOR="1;31"
HOME_URL="https://www.kali.org/"
SUPPORT_URL="https://forums.kali.org/"
BUG_REPORT_URL="https://bugs.kali.org/"