Marvin Esse: Wie kann ich am sinnvollsten den Text durchsuchen?

Hallo,

ich hoffe ihr verzeiht mir, mir ist kein besserer Betreff eingefallen 😟

Ich habe folgenden Text:

23.01.2018 08:00:47 crow [91.200.12.153] authentication failure
23.01.2018 08:01:12 alex1 [91.200.12.13] authentication failure
23.01.2018 08:01:12 Spam [91.200.12.82] authentication failure
23.01.2018 08:01:13 Alcala [91.200.12.13] authentication failure
23.01.2018 08:01:14 Rita [91.200.12.13] authentication failure
23.01.2018 08:03:47 Exmerge [91.200.12.82] authentication failure
23.01.2018 08:05:34 Heredia [91.200.12.13] authentication failure
23.01.2018 08:05:37 Test1 [91.200.12.82] authentication failure
23.01.2018 08:05:39 Traffic [91.200.12.13] authentication failure

Diesen würde ich gerne mittels PHP wie folgt durchsuchen:

  • Welche IP-Adresse taucht mindestens 5 Mal auf?
  • Welche IP-Adresse hat mehr als 2 Einträge innerhalb von 3 Sekunden?
  • Die angezeigten Namen spielen dabei keine Rolle

Habt ihr eine Idee, wie ich das am geschicktesten umsetzen kann?

Momentan schwebt mir das so vor: Zuerst würde ich die Einträge in ein 2-dimensionales Array packen, Element 1 ist der Zeitstempel, Element 2 die IP-Adresse. Dann würde ich versuchen das Array nach IP-Adresse und innerhalb nach Zeitstempel zu sortieren. In einer Schleife (mit Gruppenwechsel) würde ich schonmal die Häufigkeit der IP-Adressen zählen können. Über diese Schleife könnte ich womöglich auch zählen, wieviele Einträge innerhalb eines Zeitintervalls auftreten. Alternativ könnte ich die Einträge in einer Hilfs-Tabelle speichern, dann wäre das Sortieren für mich zumindest einfacher (ein einfaches Order By ipadresse,zeitstempel) und ich hätte keine mehrdimensionalen Arrays.

Ist das ok oder doch zu kompliziert/umständlich gedacht?

LG Marvin

  1. Tach!

    • Welche IP-Adresse taucht mindestens 5 Mal auf?
    • Welche IP-Adresse hat mehr als 2 Einträge innerhalb von 3 Sekunden?

    Habt ihr eine Idee, wie ich das am geschicktesten umsetzen kann?

    Trenne beide Aufgabe und erledige sie separat. Die erste kann man vielleicht im Vorbeigehen während der zweiten erledigen. Aber dann fällt dir irgendwann noch eine dritte und eine vierte ein, und dann wird es irgendwann unübersichtlich und unnötig komplex. Um die Liste nur einmal durchgehen zu müssen kannst du ja eine Liste der Aufgabe erstellen und für jeden Log-Eintrag diese Aufgabenliste abarbeiten. Die Aufgaben wäre eigene Funktionen. Werte, die zwischen den Aufrufen gespeichert werden müssen, kann man global anlegen (unschön), oder als statische Variable innerhalb der Funktion. Da gibts dann nur ein Problem, das Endergebnis aus der Funktion rauszubekommen. Besser wäre ein Objekt mit innerer Datenhaltung und zwei Methoden. Die eine sammelt die Daten, über die andere kann man für das Ergebnis abfragen. Diese Methoden kann man dann wie unter Callables - Typ 3 beschrieben aufrufen. Vielleicht ist das Konstrukt am Ende weniger performant als eine Alles-in-einem-Lösung (messen!). Aber das musst du dann einschätzen, ob dir Geschwindigkeit oder Wartbarkeit wichtiger ist.

    Der erste Teil ist einfach eine Liste mit IP-Adressen, in die einzelnen Einträge je IP-Adresse hochgezählt oder mit Zählerstand 1 hinzugefügt werden.

    Für die zweite würde ich ein Array mit Timestamps (die Uhrzeit als Strings geht ebenso) als Keys anlegen und mit einem Array als Wert, in dem die IP-Adressen zu diesem Zeitpunkt abgelegt sind. Für jede neue Zeile aus dem Logfile wirfst du die Einträge am Anfang des Arrays raus, die älter als die drei Sekunden sind. Mit key() gibts die Zeit, mit array_unshift() wird ein Datensatz am Anfang entfernt. Das ganze solange, bis key() einen Wert innerhalb der Zeitspanne liefert. Durch das Rest-Array durchlaufen und zählen, ob die aktuelle IP aus dem Logfile mehrfach enthalten ist.

    dedlfix.

    1. @@dedlfix

      Für die zweite würde ich ein Array mit Timestamps (die Uhrzeit als Strings geht ebenso) als Keys anlegen und mit einem Array als Wert, in dem die IP-Adressen zu diesem Zeitpunkt abgelegt sind.

      Wär’s nicht besser andersrum? Die IP-Adressen als Keys mit einem Array von Timestamps/Strings als Wert:

      $ipAddresses = array(
      	'91.200.12.153' => [
      		'23.01.2018 08:00:47',
      	],
      	'91.200.12.13' => [
      		'23.01.2018 08:00:47',
      		'23.01.2018 08:01:13',
      		'23.01.2018 08:01:14',
      		'23.01.2018 08:05:34',
      		'23.01.2018 08:05:39',
      	],
      	'91.200.12.82' => [
      		'23.01.2018 08:01:12',
      		'23.01.2018 08:03:47',
      		'23.01.2018 08:05:37',
      	],
      );
      

      Dann kann man mit

      foreach ($ipAddresses as $ipAddresse=>$timestamps)
      {
      	if (count($timestamps) >= 5)
      	{
      		// do something
      	}
      }
      

      für jede IP-Adresse einfach prüfen, ob sie 5 oder mehr Mal auftritt.

      (Das könnte man vielleicht auch gleich während der Erzeugung des Arrays mit erledigen, aber da steht auch wieder Performanz gegen Wartbarkeit. Eine Methode sollte nur ein Ding tun …)

      Und für jede IP-Adresse prüfen, ob 3 (oder mehr) Timestamps innerhalb von 3 Sekunden liegen. Wozu es sicherlich ratsam ist, bei der Erzeugung des Arrays die Zeit-Strings in Timestamps umzuwandeln.

      LLAP 🖖

      PS: Code ungetestet; seht ihn als Pseudocode an.

      --
      “When UX doesn’t consider all users, shouldn’t it be known as ‘Some User Experience’ or... SUX? #a11y” —Billy Gregory
      1. Tach!

        Für die zweite würde ich ein Array mit Timestamps (die Uhrzeit als Strings geht ebenso) als Keys anlegen und mit einem Array als Wert, in dem die IP-Adressen zu diesem Zeitpunkt abgelegt sind.

        Wär’s nicht besser andersrum? Die IP-Adressen als Keys mit einem Array von Timestamps/Strings als Wert:

        $ipAddresses = array(
        	'91.200.12.153' => [
        		'23.01.2018 08:00:47',
        	],
        	'91.200.12.13' => [
        		'23.01.2018 08:00:47',
        		'23.01.2018 08:01:13',
        		'23.01.2018 08:01:14',
        		'23.01.2018 08:05:34',
        		'23.01.2018 08:05:39',
        	],
        	'91.200.12.82' => [
        		'23.01.2018 08:01:12',
        		'23.01.2018 08:03:47',
        		'23.01.2018 08:05:37',
        	],
        );
        

        Wenn du das so aufbaust, muss als Zwischenschritt immer die komplette Struktur in ein Array eingelesen werden. Benötigt wird aber für die beiden Aufgaben lediglich ein Zähler sowie die Einträge der jüngsten drei Sekunden.

        Und für jede IP-Adresse prüfen, ob 3 (oder mehr) Timestamps innerhalb von 3 Sekunden liegen. Wozu es sicherlich ratsam ist, bei der Erzeugung des Arrays die Zeit-Strings in Timestamps umzuwandeln.

        Eigentlich hatte ich gedacht, dass ein sortierbarer String reichen würde und man sich das Parsen sparen kann. Für die Sortierbarkeit hat aber das Datum das falsche Format. Außerdem muss man eine Sonderregel für den Vergleich beim Stundenwechsel berücksichtigt werden. Und dann waren da auch noch die gesetzlich verordneten Zeitzonenwechsel. Alles unschön, also doch lieber parsen und mit Unix-Timestamps arbeiten.

        dedlfix.

        1. Benötigt wird aber für die beiden Aufgaben lediglich ein Zähler sowie die Einträge der jüngsten drei Sekunden.

          Während ein einziger Durchlauf genügt, die Zeitstempel auf die IPs zu pushen, braucht man zum Ermitteln von Differenzen jedoch 2 Werte. Wenn je IP alle Zeitstempel in einem Array vorliegen, lässt sich das recht einfach umsetzen, indem man vor dem Schleifendurchlauf das erste Arrayelement rauszieht (shift).

          MfG

      2. hi,

        PS: Code ungetestet; seht ihn als Pseudocode an.

        funktioniert, ich würds genauso machen, Datenstruktur und Ergebnis:

        $VAR1 = {
                  '91.200.12.13' => [
                                      '28872',
                                      '28873',
                                      '28874',
                                      '29134',
                                      '29139'
                                    ],
                  '91.200.12.153' => [
                                       '28847'
                                     ],
                  '91.200.12.82' => [
                                      '28872',
                                      '29027',
                                      '29137'
                                    ]
                };
        
        91.200.12.13  => 5, 2 in 3s 
        91.200.12.82  => 3, 0 in 3s 
        91.200.12.153 => 1, 0 in 3s 
        

        MfG

  2. Das sieht (bei größeren Dateien) nach "teuer" (Rechenleistung) aus.

    Ist es ein von Dir verwalteter (virtueller) Linux/*BSD/Unix-Server?

    Wenn Du nämlich auf "Welche IP-Adresse hat mehr als 2 Einträge innerhalb von 3 Sekunden?" verzichten kannst sieht das nach einer Aufgabe aus, die - inklusive einer konfigurierbaren Reaktion (Beliebt: Sperrung des Datenverkehrs mit der IP in der Firewall für eine einstellbare Zeit!) - typischerweise fail2ban erledigt.

    Allerdings müsstest Du bei einem eigenen Logfile dann auch eigene Einstellungsdateien hinterlegen. Aber wenn Du dann schon mal dabei bist, dann kannst Du das Logfile (um welches es offensichtlich geht) auch von logrotate behandeln lassen. Dann musst Du Dich nicht selbst aufwendig um das Löschen/Archivieren kümmern.

    1. Hallo,

      ich habe es jetzt in einer Schleife gelöst, unter Mithilfe einer kleinen MySQL-Tabelle. Das Logfile enthält durchschnittlich ca. 1.000.000 Einträge, die Abfrage zur Ermittlung aller aktuellen Einträge (immer letzte 5 Minuten) mit dem gewünschten Suchbegriff (authentication failure), das Speichern in der Tabelle und Auswerten der Ergebnisse dauert weniger als 1 Sekunde. Da fail2ban nur die Firewall des betroffenen Servers selber pflegt, ich aber die IPs schon auf der ersten Firewall sperren möchte, musste ich mir selber etwas bauen.

      Vielen Dank für Eure Unterstützung und Anregungen, die mir weitergeholfen haben.

      LG Marvin

      1. Da fail2ban nur die Firewall des betroffenen Servers selber pflegt, ich aber die IPs schon auf der ersten Firewall sperren möchte, musste ich mir selber etwas bauen.

        Wenn Du Dir mal die Konfigurationsdateien unterhalb von /etc/fail2ban/action.d/ ansiehst findest Du immer folgende Zeilen:

        actionstart = <iptables> -N f2b-<name>
        actionstop = …
        actioncheck = …
        actionban = …
        actionunban = …
        

        Dahinter stehen Befehle mit Variablen. Da könnte also auch

        actionstart = ssh -i /foo/bar/f2b.key f2b@firewall <iptables> -N f2b-<name>
        

        stehen. Du musst da gar nicht so viel selbst bauen:

        Rezept:

        1. /etc/fail2ban/jail.conf nach jail.local kopieren und diese ändern. (Damit ein Update die Änderungen nicht überschreibt.)

        2. in /etc/fail2ban/jail.local alle banactions in "banaction_allports" abändern.

        3. /etc/fail2ban/action.d/iptables-allports.conf nach iptables-allports.local kopieren, bei den den o.g. Punkten einen kleinen ssh-Befehl notieren welcher mittels eines geeigneten Benutzers, einer Schlüsseldatei ohne Passwort und einem geeigneten entfernten Benutzer (also einem, der iptables aufrufen und ausführen kann (setfacl, suid-Bit, ...) auf dem Server die Blockierung setzt.

        Wenn Du statt iptables eine andere Firewall verwendest wirst Du mit dem gleichen Vorgehen bei ipfw.conf oder firewallcmd-*.conf, ipfilter.conf nfttables-*.conf und so weiter glücklich.

        Das Logfile enthält durchschnittlich ca. 1.000.000 Einträge

        Wie ich schon schrieb. Logrotate hilft hier gegen

        dauert weniger als 1 Sekunde

        denn das "weniger als" klingt für mich nach "zu lange".