Surveillance de fichiers

#1
Pas toujours évident de s'apercevoir que des pages ou des scripts ont été modifiés pas des méchants, surtout quand il s'agit de modifications invisibles qui n'apparaissent que dans la source d'une page. J'ai déjà goûté.

Voici un petit script de surveillance qu'une tâche cron pourra exécuter. Elle liste simplement la liste des fichiers modifiés depuis X et s'il y en a, un email est envoyé pour prévenir.

Les éléments à renseigner :

USERCPANEL = Votre utilisateur CPANEL

nbminutes = Nombre de minutes en arrière à vérifier. Exemple '-120' = 2 heures. Le script listera les fichiers modifiés dans les dernière 2 heures.

from_address = Email expéditeur (compte sur le serveur).
mailpass = Mot de passe du email (pour permettre l'envoi)
to_address = Email de destination
emailsubject = Sujet du email

dossiers = les dossiers à surveiller. Si vous n'avez qu'un site principal sur votre hébergement World, c'est tout probablement public_html. Le script se charge d'ajouter /home/USERCPANEL/ au début.

Vous pourriez ajouter public_ftp aussi, ça ne mange pas de pain.

Si vous avez des domaines supplémentaires, ils sont généralement dans /home/USERCPANEL/domainesup.com, ajouter simplement domainesup.com

dossiersexclus = Les dossiers à exclure de la surveillance. Utile pour éviter de recevoir des emails parce que le cache de votre site s'est regénéré. Mettre simplement /LEDOSSIERAEXCLURE
Le script se chargera d'ajouter /home/USERCPANEL/DOSSIERSURVEILLE/DOSSIEREXCLUS

C'est tout....

Le code
Code:
#!/usr/bin/python
# -*- coding: iso-8859-15 -*-

import smtplib
from email.mime.text import MIMEText

########################## VARIABLES

# USERCPANEL
USER = 'VOTREUTILISATEURCPANEL'

# -120 = 120 minutes - cherche les changements effectués dans les 2 heures précédentes
nbminutes = '-120'

# Email Expéditeur
from_address = '[email protected]'

# SI BESOIN - Mot de passe du email expéditeur (pour l'envoi)
mailpass     = 'motdepasseemail'

# Email Destination
to_address   = '[email protected]'

# Sujet
emailsubject = 'Changements dans les fichiers'

# Dossiers à surveiller - public_html = site principal, DOMAINE1 = site supplémentaire.
dossiers = ['public_html',
            'DOMAINE1.com',
            'DOMAINE2.com',
]

# Dossiers à exclure.
# Garder en mémoire que le chemin correspondra à /home/UTILISATEURCPANEL/DOSSIERSURVEILLE/DOSSIEREXCLUS
# Par exemple, pour exclure /home/UTILISATEURCPANEL/public_html/wp-content/cache mettre seulement '/wp-content/cache'
dossiersexclus = ['/wp-content/cache',
                  '/assets/cache',
                  '/cache',
                  '/templates_c',
                  '/.ftpquota',
                  '/.well-known',
]

# FIN DES VARIABLES - NE PAS MODIFIER LE RESTE
########################################

# Fonction email
def send_mail(from_address, to_address, subject, text):
   msg = MIMEText(text)
   msg['subject'] = subject
   msg['From'] = from_address
   msg['To'] = to_address

   s = smtplib.SMTP('localhost')
   #s = smtplib.SMTP('localhost:587')
   #s.ehlo()
   #s.starttls()
   #s.login(from_address, mailpass)
   s.sendmail(from_address, to_address, msg.as_string())
   s.quit

import os
import subprocess
import re
import string

# Fonction lisant les lignes de la commande find,
def check_lines(output, userpath):
   new_output = ""
   # Sépare ligne par ligne
   lines = output.split('\n')
   for line in lines:
      exclude_it = False
      # Exclusions
      #
      for ex_path in dossiersexclus:
         #if (re.match(userpath + ex_path , line) != None):
         matchObj = re.match(r'(.*)'+ex_path+'(.*?).*' , line, re.M|re.I)
         if matchObj:
            exclude_it = True
            break
      # Exclus les fichiers error_log
      if (re.match(r'(.*error_log$)', line) != None):
         exclude_it = True
      if (exclude_it == False) and (line !=''):
         new_output += line + '\n'
   return new_output


message = ""
# Excution de la commande find sur les éléments à surveiller
for fwatch in dossiers:
   userpath = "/home/" + USER + "/" + fwatch
   # Commande find
   p = subprocess.Popen(["find", userpath, "-name", "*", "-mmin", nbminutes, "-print"], stdout = subprocess.PIPE)
   output, err = p.communicate()
   if (output != ""):
      processed_output = check_lines(output, userpath)
      if (processed_output != ''):
         message += "Dossier: " + fwatch + '\n'
         message += processed_output + '\n'

# Si changement, envoi du email - sinon, rien.
if (message != ""):
  send_mail(from_address, to_address, emailsubject, message)

# FIN
Sauvegardez le fichier au nom (exemple) surveillance.sh et envoyez sur votre serveur en ftp (identifiant principal) dans un dossier (exemple) /backups ou /scripts

Modifiez les permission en 744.

Dans votre CPANEL, ajoutez une tâche cron
Code:
0 */2 * * * /home/USERCPANEL/backups/surveillance.sh >/dev/null 2>&1
Toutes les 2 heures, le fichier sera exécuté et s'il y a eu des changements non prévus, un email vous sera envoyé avec la liste des fichiers concernés.

Testé sur un World avec des domaines supplémentaires.
 
Dernière édition:
#2
J'ai mis en place ce truc aujourd'hui et déjà une découverte.

J'ai reçu un email parce qu'une image d'un de mes wordpress avait été modifiée dans les 2 dernières heures. Particulièrement bizarre.

wp-includes/images/icon-download.png

Pour m'apercevoir que cette image ne fait pas partie de Wordpress et que c'était en fait un fichier texte créé par un bout de code inséré dans le fichier
wp-includes/images/user.php

En renommant le icon-download.png en icon-download.txt et en voyant le contenu, ça faisait peur.

Pourtant, mon bidule était SUPPOSEMENT à jour. (Il y a tellement de trous dans ce machin que ça devrait être interdit).

Heureusement, j'ai pu corriger avant de me faire harakiri par Planethoster. :D
 
#3
Ton script examine les fichiers du ou des dossiers spécifiés uniquement, ou est-ce qu'il analyse les sous-dossiers aussi ?
 
#5
Il ne consomme pas trop de ressources ?
Je le testerai bien mais entre mon domaine principal et mes domaines supplémentaires..... lol
 
#6
C'est un simple "find". C'est comme faire un dir ou un ls mais avec une période limitée. Il ne lit pas le contenu des fichiers, simplement la liste des fichiers modifiés depuis X .

Pour 17 domaines (la plupart avec peu de choses dedans mais certains à plus de 500Mo de données et quelques milliers de fichiers), c'est presque instantané.

En comparaison, nos scripts de backup sont des usines à gaz. :)
 
#7
Alors je vais tester cela ces prochains jours...
D'autant plus que je reçois beaucoup d'alertes de tentatives brute force sur mes sites...
Autant vérifier que rien n'est modifié
 
#8
Par contre pourquoi le mot de passe smtp ?
Tu peux détailler cette partie et son fonctionnement ?
 
#9
Je ne suis moi-même pas un grand fan de cette section même si elle est logique. (A noter que je n'ai pas écrit tout le script, seulement quelques sections pour les besoins).

En gros, une fonction de mail est créée (def send_mail) et utilise le serveur pour envoyer le mail. Puisque le serveur requiert les identifiants pour les emails sortants....

Le serveur peut sûrement s'en passer pour un simple email mais n'étant pas expert en python, faudra que quelqu'un d'autres y jette un oeil.
 
#10
Moi non plus...

Bon en regardant cette ligne :
Code:
s = smtplib.SMTP('localhost:587')
On voit que l'envoi se fera via le port 587 du SMTP, qui requiert une authentification certes, mais n'est pas sécurisé (identique au port 25 pour faire simple)

A tester avec le port 465 qui lui est sécurisé en SSL (d'aprés la configuration donnée par le cPanel)

Je testerai cela aprés avoir créé une adresse email spécifique pour ce script
 
#11
Est-ce qu'il y a un moyen de vérifier le bon fonctionnement du script et de son exécution ?
Je l'ai mis en place juste aprés mon précédent message mais je n'ai aucun email... Alors peut être est-ce normal mais dans le doute.....
 
#12
C'est normal s'il ne trouve aucun fichier modifié dans les dernière X minutes.

Pour le tester, comme n'importe quel scripts qu'on peut exécuter via ssh,

Se connecter en ssh, naviguer au fichier et l'exécuter.
Exemple
Code:
$ cd /scripts
$ ./watchfiles.sh
Pour voir s'il envoi un email, peut-être commenter le if (message......) :
Code:
# Si changement, envoi du email - sinon, rien.
# if (message != ""):
  send_mail(from_address, to_address, emailsubject, message)
Il enverra un email vide.

Ou faire n'importe quel changement (modifier un fichier ou uploader)..
Et rééxécuter...

J'ai ajouté dans la liste des exclusions :
Code:
                 '/*/.ftpquota',
 
#13
Etant en mutualisé je n'ai pas accès au SSH
Je vais ajouter un fichier et voir ce que ça donne
 
#14
Oh... bah en théorie, si des scripts peuvent être exécutés... ça devrait fonctionner.

Peut-être qu'il serait bon de ne pas mettre
Code:
....... >/dev/null 2>&1
dans la tâche cron pendant le test. S'il y a une erreur, elle sera envoyée par email.
 
#16
Bon en réalité, j'ai testé sur mon "World" et il n'a pas besoin du mot de passe pour envoyer le mail.

J'ai laissé les lignes mais en commentaire. (Premier post modifié).
Code:
# Fonction email
def send_mail(from_address, to_address, subject, text):
   msg = MIMEText(text)
   msg['subject'] = subject
   msg['From'] = from_address
   msg['To'] = to_address

   #s = smtplib.SMTP('localhost:587')
   s = smtplib.SMTP('localhost')
   #s.ehlo()
   #s.starttls()
   #s.login(from_address, mailpass)
   s.sendmail(from_address, to_address, msg.as_string())
   s.quit
 
#17
ok alors j'ai une erreur étrange...
Code:
/bin/bash: /home/user/surveillance/surveillance.sh: /usr/bin/python^M: bad interpreter: No such file or directory
Pourtant le fichier est bien là où c'est mis....
 
#18
Bon alors en cherchant un peu j'ai pu fixer l'erreur... En passant le fichier au format Unix et en l'encodant en UTF8 ;)

Par contre j'ai encore quelques questions :D

Dans l'email reçu j'ai ça :
Code:
username: public_html
Suivi d'une liste de dossiers et fichiers ayant comme racine "public_html"
Est-ce normal ce "username" au début de l'email ? Il s'agit du 1er dossier du début de boucle ? Si c'est bien le cas peut être le renommer en "Domaine analysé" ou un truc du genre...

Ensuite dans ton fichier d'exemple, pour les exclusions tu mets :
Code:
dossiersexclus = ['/wp-content/cache',
                  '/assets/cache',
                  '/cache',
                  '/templates_c',
                  '/*/.ftpquota',
]
Le "/*/.ftpquota" signifie que le script va exclure tous les dossiers ".ftpquota" quelque soit l'endroit où il se trouve ?
On peut donc faire de même avec les dossiers "cache" et "tmp" ?
Du style :
Code:
dossiersexclus = ['/*/cache',
                  '/*/templates_c',
                  '/*/tmp',
]
Comme dans ton exemple tu as plusieurs dossiers "cache" que tu prends soin de bien spécifier, je me pose la question :oops::oops:
 
#19
Effectivement, ça devrait être "Path" ou "Dossier" et non "username". Le script original était conçu pour une utilisation par utilisateur.

Le .ftpquota est un fichier que je vois à la racine de tous mes domaines. Visiblement installé automatiquement par Planethoster/CPANEL. J'avais l'intention de demander plus tard à quoi il servait. Sur de l'illimité, je me pose la question de l'intérêt.

Lorsque le script passe, ce fichier apparaît régulièrement comme étant modifié (surtout après des transferts par ftp). J'ai donc rajouté cette exclusion pour arrêter de le voir dans la liste. le /*/.ftpquota est pour ne pas le réécrire pour tous les domaines ET sous-domaines (domaine.com/sous_domaine).

En théorie, effectivement, le résultat devrait être le même avec ce que tu as mis pour les dossiers :
Code:
dossiersexclus = ['/*/cache',
                 '/*/templates_c',
                  '/*/tmp',
]
Le script original ne semblait pas supposer qu'on puisse avoir plusieurs dossiers à différents étages.
 
#20
Je vais tenter cette modification... Elle semble logique après tout :rolleyes:
Sous Joomla, par exemple, tu as un dossier cache à la racine et dans le sous-dossier de l'administration....
 
#21
Bon alors aprés essai, cette modification pour les dossiers exclus ne marchent pas :(
Il faut donc mettre un à un les dossiers à exclure même si il y en a plusieurs :(
 
#22
Script modifié...

Changer :
Code:
         if (re.match(userpath + ex_path , line) != None):
            exclude_it = True
            break
pour
Code:
         #if (re.match(userpath + ex_path , line) != None):
         matchObj = re.match(r'(.*)'+ex_path+'(.*?).*' , line, re.M|re.I)
         if matchObj:
            exclude_it = True
            break
(Premier post modifié).

Les exclusions peuvent maintenant être :
Code:
dossiersexclus = ['/wp-content/cache',
                  '/cache',
                  '/template_c',
                  '/tmp',
                  '/.ftpquota',
]
Ils seront exclus peu importe leur position dans l'arborescence. ;)
 
#23
Au top, tu as pu le tester ?
J'essaie cette modification dès ce soir dans ce cas ;)
Ça peut être super intéressant dans le cas de plusieurs domaines avec des CMS qui utilisent des systèmes de cache similaires (même intitulé de dossier)
 
#24
Oui, testé!

Au lieu de chercher l'adresse complète (exemple : /home/tralala/x/y/cache), il cherche */cache* dans l'adresse /home/tralala/x/y/cache

Il reste un seul minuscule petit bug (qui ne l'empêche pas de fonctionner normalement). C'est que des fois, je reçois un email avec la racine d'un domaine comme si ce dernier avait été modifié.

Dossier :
/home/cedomaine.tld

Je soupçonne qu'en voyant le .ftpquota (parce que cela arrive après des transferts), même s'il est exclu, il affiche la racine juste avant. Mais à part ça, cela semble fonctionner comme on le souhaite.
 
#25
Oui j'avais remarqué la même chose sur des dossiers n'ayant pas ftpquotat
Après peut-être que le dossier racine à sa date de modification qui change ponctuellement sur certaines modifications....
 
#26
J'ai changé dans le mien

Code:
      if (exclude_it == False) and (line !=''):
pour
Code:
      if (exclude_it == False) and (line !='') and os.path.isdir(line) == False:
De cette façon, il n'affichera QUE les fichiers modifiés et non un dossier parce qu'à la limite, à moins d'un changement de permission sur un dossier...

Mais je ne pense pas que ce soit une très grande idée. Si on ajoute un dossier vide, il n'apparaîtra pas. En même temps, ajouter un dossier vide, c'est complètement con donc... Sauf s'il est ajouté par un tiers mais s'il le fait, ce n'est pas pour le laisser vide non plus.

A déterminer.
 
#28
Grâce à ton script je viens de voir qu'à la racine de tous mes domaines a été ajouté
Code:
/.well-known/acme-challenge
Une idée de ce que c'est ????
 
#29
Créé par le système automatique de certificat SSL. Tu peux l'enlever mais il reviendra au renouvellement du certificat.
 
Haut