The problem
Some dangers in generated code are visible without running it: an import of a module that can touch the system, a call to eval, a trick to escape a restricted namespace. Python's ast module parses source code into an abstract syntax tree (the structure of the program, as a tree of nodes) that a program can walk. You will write a scanner that lists the risks of a piece of code, line by line.
What to flag
- Imports of a dangerous module:
import X,import X.yorfrom X import ...where the top-level packageXis one ofos,subprocess,socket,shutil,sys,ctypes,pickle. Message:"import <X>". - Calls of a dangerous builtin by name:
eval,exec,compile,open,__import__,getattr,globals. Message:"call <name>". Only direct calls by name count (eval(...)), not methods (obj.eval(...)). - Dunder attribute access: any attribute whose name starts and ends with
__(a "dunder", for double underscore), such as().__class__orx.__subclasses__. Message:"dunder <name>".
If the source does not parse, return [(0, "syntax error")].
Example. This source, one statement per line:
import os x = eval(input()) y = ().__class__ from subprocess import run obj.eval(1)The findings are
[(1, "import os"), (2, "call eval"), (3, "dunder __class__"), (4, "import subprocess")]. Line 5 is not flagged:evalis called as a method ofobj, not by name. The scan never runs the code.
def f(:does not parse:[(0, "syntax error")].
What to implement
find_risks(source): a list of (line, message) findings, sorted by line. Within a line, imports come first, then calls, then dunders, each in source order.
Requirements
- Use
ast.parseandast.walk; do not run the code. - Sort findings by line with a stable sort, so that the order within a line is kept.
Hint
The node types are ast.Import (.names[i].name), ast.ImportFrom (.module), ast.Call with .func an ast.Name (.id), and ast.Attribute (.attr). Every node has .lineno. ast.walk(tree) visits every node.