News

Vérifier un code python sans l'éxécuter.

Le 7 Février 2019

Pour des raisons de sécurité, vous pouvez être amené à devoir contrôler des morceaux de code Python sans pour autant l'éxecuter. Par exemple, si ce code provient d'un autre organisme ou carrément d'internet, il contient peut être des bugs ou des failles.

Vous souhaitez donc tout contrôler avant de le charger dans votre infrastructure.

Dans ce post, nous allons utiliser des noms farfelus mais il faudra bien entendu les remplacer. Supposons donc que tous les scripts que vous devez contrôler suivent la même structure : Ils doivent fournir une classe appelée 'MyCar' qui hérite d'une classe abstraite ('GlobalCar') que vous avez créée dans le module 'myproject.cars' :


from myproject.cars import GlobalCars

class MyCar(GlobalCars):
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def drive(self, args):
        # script custom code here
        
    def park(self):
        # script custom code here 

Pour passer les tests de contrôle, le script :
  • ne doit pas contenir d'erreurs
  • doit fournir la classe 'MyCar' qui hérite de 'GlobalCars'
  • doit fournir les méthodes 'drive' et 'park'

  1. Compiler le script

  2. La première chose à faire est de compiler le script, cela ne l'execute pas et relèvera les erreurs de compilation (syntaxe ou type par exemple) :

    try : 
        carcode = compile( open('path/to/script.py','r').read() ,'compiledFile' , 'exec' )
    except Exception as e :
        print('build error : {m}'.format( m = str(e) ) )
                        

    Nous pouvons profiter de l'objet compilé pour faire quelques vérifications sur les noms utilisés par le code :

    for n in ['GlobalCars', 'myproject.cars','MyCar']:
        if n not in carcode.co_names : print('error : script must import GlobalCars from myproject.cars')
                    

    Mais cela reste limité car si les noms sont présents, rien ne prouve qu'ils soient correctement utilisés. Nous n'avons aucune information sur les relations entre les classes et les méthodes. Pour cela, nous allons devoir utiliser le module AST (pour Abstract Syntax Tree) :

  3. Analyser le script avec AST

  4. AST permet de construire une arborescence du code sans l'executer

    import ast
    carast = ast.parse( open('path/to/script.py','r').read())
    

    Nous pouvons désormais contrôler les imports en explorant les classes 'body' de types 'ImportFrom' ( par exemple 'import myproject.cars' ) et 'Import' ( par exemple 'from myproject.cars import GlobalCars' ).

    Etant donné que le script peut importer plusieurs classes, nous devons faire des boucles pour récupérer celles qui nous intéressent.

    Vérifions donc si le script importe bien 'myproject.cars', et pas seulement que le nom soit présent :
    if not (  len([ x for x in carast.body if type(x) is ast.ImportFrom and x.module == 'myproject.cars' ]) > 0 or 
          True in [ y.name == 'myproject.cars' for x in carast.body if type(x) is ast.Import for y in x.names ] ):
        print('script must import myproject.cars')
    


    Dans la même idée, nous pouvons aussi vérifier que MyCar est bien une sous-classe de GlobalCars en explorant les attribus 'body' de type 'ClassDef' et en remontant par les attribus 'bases' :
    if True not in [ [ y for y in x.bases if y.id == 'GlobalCars' ] != [] for x in carast.body if type(x) is ast.ClassDef and x.name == "MyCar" ] :
        print('script must provide a class MyCar which inherits from GlobalCar ')
    

    Pour terminer, nous pouvons extraire les méthodes fournies par la classe 'MyCar' en explorant l'attribu 'body' de cette dernière
    methods = [ z.name for x in carast.body if type(x) is ast.ClassDef and x.name == "MyCar" for y in x.bases if y.id == 'GlobalCars' for z in x.body ]
    for n in ['run', 'rollback']:
        if n not in methods:
            print('MyCar must provide a "{}" method').format(n)
    

  5. Conclusion

  6. L'utilisation de compile() permet de vérifier que le code ne contient pas d'erreur mais AST est un outil complémentaire très puissant permettant de naviguer dans la structure du script.

    La combinaison de ces deux outils permet une étude approfondie d'un code Python