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

1""" 

2License GPLv3 or higher. 

3 

4(C) 2025 Created by Maikel Mardjan - https://nocomplexity.com/ 

5 

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. 

7 

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. 

9 

10You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. 

11 

12Issue validation functions for codeaudit 

13""" 

14 

15import ast 

16import warnings 

17from collections import defaultdict 

18 

19from codeaudit.checkmodules import get_imported_modules 

20 

21 

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)) 

31 

32 

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 

43 

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 

49 

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 

61 

62 # Step 2: Walk the AST 

63 for node in ast.walk(tree): 

64 lineno = getattr(node, "lineno", None) 

65 construct = None 

66 

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 

101 

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 

110 

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 

116 

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" 

121 

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" 

133 

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)) 

138 

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)