Coverage for src / codeaudit / issuevalidations.py: 100%
85 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-09 09:33 +0200
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-09 09:33 +0200
1"""
2License GPLv3 or higher.
4(C) 2025 Created by Maikel Mardjan - https://nocomplexity.com/
6This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
8This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
10You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
12Issue validation functions for codeaudit
13"""
15import ast
16import warnings
17from collections import defaultdict
19from codeaudit.checkmodules import get_imported_modules
22def get_full_attr_name(node):
23 """Recursively builds full dotted name from Attribute/Name nodes."""
24 parts = []
25 while isinstance(node, ast.Attribute):
26 parts.append(node.attr)
27 node = node.value
28 if isinstance(node, ast.Name):
29 parts.append(node.id)
30 return ".".join(reversed(parts))
33def find_constructs(source_code, constructs_to_detect):
34 """
35 Detects specified constructs (e.g., 'os', 'os.access', 'eval') in the code.
36 Handles aliases, from-imports, deep nesting, and avoids double-counting.
37 """
38 with warnings.catch_warnings(): # Suppression of warnings
39 warnings.simplefilter("ignore", category=SyntaxWarning)
40 tree = ast.parse(source_code)
41 results = defaultdict(list)
42 seen = set() # (construct, lineno) pairs already counted
44 # step 0: Create a module list - needed for some checks
45 imported_modules = get_imported_modules(source_code)
46 core_modules = imported_modules[
47 "core_modules"
48 ] # Only interested in core modules that are imported
50 # Step 1: Build alias map
51 alias_map = {}
52 for node in ast.iter_child_nodes(tree):
53 if isinstance(node, ast.Import):
54 for alias in node.names:
55 alias_map[alias.asname or alias.name] = alias.name
56 elif isinstance(node, ast.ImportFrom):
57 module = node.module
58 for alias in node.names:
59 full_name = f"{module}.{alias.name}"
60 alias_map[alias.asname or alias.name] = full_name
62 # Step 2: Walk the AST
63 for node in ast.walk(tree):
64 lineno = getattr(node, "lineno", None)
65 construct = None
67 if isinstance(node, ast.Call):
68 func = node.func
69 if isinstance(func, ast.Attribute):
70 full = get_full_attr_name(func)
71 prefix = full.split(".")[0]
72 resolved_prefix = alias_map.get(prefix, prefix)
73 full_resolved = resolved_prefix + full[len(prefix) :]
74 if full_resolved in constructs_to_detect:
75 construct = full_resolved
76 elif (
77 node.func.attr in ("extractall", "extract")
78 and "tarfile" in core_modules
79 ): # note only in combination with tarfile module or alias,see step1
80 construct = "tarfile.TarFile"
81 elif (
82 node.func.attr in ("eval") and "builtins" in core_modules
83 ): # catch obfuscating eval construct with builtins module
84 construct = "eval"
85 elif (
86 node.func.attr in ("exec") and "builtins" in core_modules
87 ): # catch obfuscating exec construct with builtins module
88 construct = "exec"
89 elif (
90 node.func.attr in ("input") and "builtins" in core_modules
91 ): # catch obfuscating construct with builtins module
92 construct = "input"
93 elif (
94 node.func.attr in ("compile") and "builtins" in core_modules
95 ): # catch obfuscating construct with builtins module
96 construct = "compile"
97 elif isinstance(func, ast.Name):
98 resolved = alias_map.get(func.id, func.id)
99 if resolved in constructs_to_detect:
100 construct = resolved
102 # Attribute usage: path.exists
103 elif isinstance(node, ast.Attribute):
104 full = get_full_attr_name(node)
105 prefix = full.split(".")[0]
106 resolved_prefix = alias_map.get(prefix, prefix)
107 full_resolved = resolved_prefix + full[len(prefix) :]
108 if full_resolved in constructs_to_detect:
109 construct = full_resolved
111 # Name usage: e.g. eval, os
112 elif isinstance(node, ast.Name):
113 resolved = alias_map.get(node.id, node.id)
114 if resolved in constructs_to_detect:
115 construct = resolved
117 # ast.Assert node ,to check on assert - assert is the only valid ast.Assert node!
118 elif isinstance(node, ast.Assert):
119 if "assert" in constructs_to_detect:
120 construct = "assert"
122 # ast.ExceptHandler — detect use of bare `pass` inside body
123 elif isinstance(node, ast.ExceptHandler):
124 if "pass" in constructs_to_detect:
125 for stmt in node.body:
126 if isinstance(stmt, ast.Pass):
127 construct = "pass"
128 # ast.ExceptHandler — detect use of bare `continue` inside body
129 if "continue" in constructs_to_detect:
130 for stmt in node.body:
131 if isinstance(stmt, ast.Continue):
132 construct = "continue"
134 # If valid construct and not yet seen at this line
135 if construct and lineno and (construct, lineno) not in seen:
136 results[construct].append(lineno)
137 seen.add((construct, lineno))
139 # sort the results by line number
140 data = dict(results)
141 sorted_results = {k: sorted(v) for k, v in data.items()}
142 return dict(sorted_results)