Coverage for tests / integration / tools / test_bandit_integration.py: 100%

48 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-04-03 18:53 +0000

1"""Integration tests for Bandit tool definition. 

2 

3These tests require bandit to be installed and available in PATH. 

4They verify the BanditPlugin definition, check command, and set_options method. 

5""" 

6 

7from __future__ import annotations 

8 

9import shutil 

10from collections.abc import Callable 

11from pathlib import Path 

12from typing import TYPE_CHECKING 

13 

14import pytest 

15from assertpy import assert_that 

16 

17if TYPE_CHECKING: 

18 from lintro.plugins.base import BaseToolPlugin 

19 

20# Skip all tests if bandit is not installed 

21pytestmark = pytest.mark.skipif( 

22 shutil.which("bandit") is None, 

23 reason="bandit not installed", 

24) 

25 

26 

27@pytest.fixture 

28def temp_python_file_with_security_issues(tmp_path: Path) -> str: 

29 """Create a temporary Python file with security issues. 

30 

31 Creates a file containing code with deliberate security vulnerabilities 

32 that bandit should detect, including: 

33 - B602: subprocess call with shell=True 

34 - B105: Hardcoded password 

35 - B311: Use of standard pseudo-random generators 

36 

37 Args: 

38 tmp_path: Pytest fixture providing a temporary directory. 

39 

40 Returns: 

41 Path to the created file as a string. 

42 """ 

43 file_path = tmp_path / "insecure.py" 

44 file_path.write_text( 

45 """\ 

46import subprocess 

47import os 

48 

49def run_command(cmd): 

50 # B602: subprocess call with shell=True 

51 subprocess.call(cmd, shell=True) 

52 

53def read_file(filename): 

54 # Potential path traversal 

55 with open(filename) as f: 

56 return f.read() 

57 

58# B105: Hardcoded password 

59password = "secret123" 

60 

61# B311: Standard pseudo-random generators not suitable for security 

62import random 

63token = random.randint(0, 1000000) 

64""", 

65 ) 

66 return str(file_path) 

67 

68 

69@pytest.fixture 

70def temp_python_file_secure(tmp_path: Path) -> str: 

71 """Create a temporary Python file with no security issues. 

72 

73 Creates a file containing secure Python code that should pass 

74 bandit security analysis without issues. 

75 

76 Args: 

77 tmp_path: Pytest fixture providing a temporary directory. 

78 

79 Returns: 

80 Path to the created file as a string. 

81 """ 

82 file_path = tmp_path / "secure.py" 

83 file_path.write_text( 

84 """\ 

85\"\"\"A secure module.\"\"\" 

86 

87import secrets 

88 

89 

90def generate_token() -> str: 

91 \"\"\"Generate a secure token.\"\"\" 

92 return secrets.token_hex(32) 

93 

94 

95def add_numbers(a: int, b: int) -> int: 

96 \"\"\"Add two numbers securely.\"\"\" 

97 return a + b 

98""", 

99 ) 

100 return str(file_path) 

101 

102 

103# --- Tests for BanditPlugin definition --- 

104 

105 

106@pytest.mark.parametrize( 

107 ("attr", "expected"), 

108 [ 

109 ("name", "bandit"), 

110 ("can_fix", False), 

111 ], 

112 ids=["name", "can_fix"], 

113) 

114def test_definition_attributes( 

115 get_plugin: Callable[[str], BaseToolPlugin], 

116 attr: str, 

117 expected: object, 

118) -> None: 

119 """Verify BanditPlugin definition has correct attribute values. 

120 

121 Tests that the plugin definition exposes the expected values for 

122 name and can_fix attributes. 

123 

124 Args: 

125 get_plugin: Fixture factory to get plugin instances. 

126 attr: The attribute name to check on the definition. 

127 expected: The expected value of the attribute. 

128 """ 

129 bandit_plugin = get_plugin("bandit") 

130 assert_that(getattr(bandit_plugin.definition, attr)).is_equal_to(expected) 

131 

132 

133def test_definition_file_patterns(get_plugin: Callable[[str], BaseToolPlugin]) -> None: 

134 """Verify BanditPlugin definition includes Python file patterns. 

135 

136 Tests that the plugin is configured to handle Python files (*.py). 

137 

138 Args: 

139 get_plugin: Fixture factory to get plugin instances. 

140 """ 

141 bandit_plugin = get_plugin("bandit") 

142 assert_that(bandit_plugin.definition.file_patterns).contains("*.py") 

143 

144 

145# --- Integration tests for bandit check command --- 

146 

147 

148def test_check_file_with_security_issues( 

149 get_plugin: Callable[[str], BaseToolPlugin], 

150 temp_python_file_with_security_issues: str, 

151) -> None: 

152 """Verify bandit check detects security issues in problematic files. 

153 

154 Runs bandit on a file containing deliberate security vulnerabilities 

155 and verifies that issues are found. 

156 

157 Args: 

158 get_plugin: Fixture factory to get plugin instances. 

159 temp_python_file_with_security_issues: Path to file with security issues. 

160 """ 

161 bandit_plugin = get_plugin("bandit") 

162 result = bandit_plugin.check([temp_python_file_with_security_issues], {}) 

163 

164 assert_that(result).is_not_none() 

165 assert_that(result.name).is_equal_to("bandit") 

166 assert_that(result.issues_count).is_greater_than(0) 

167 

168 

169def test_check_secure_file( 

170 get_plugin: Callable[[str], BaseToolPlugin], 

171 temp_python_file_secure: str, 

172) -> None: 

173 """Verify bandit check passes on secure files. 

174 

175 Runs bandit on a properly secured file and verifies no issues are found. 

176 

177 Args: 

178 get_plugin: Fixture factory to get plugin instances. 

179 temp_python_file_secure: Path to file with no security issues. 

180 """ 

181 bandit_plugin = get_plugin("bandit") 

182 result = bandit_plugin.check([temp_python_file_secure], {}) 

183 

184 assert_that(result).is_not_none() 

185 assert_that(result.name).is_equal_to("bandit") 

186 assert_that(result.issues_count).is_equal_to(0) 

187 

188 

189def test_check_empty_directory( 

190 get_plugin: Callable[[str], BaseToolPlugin], 

191 tmp_path: Path, 

192) -> None: 

193 """Verify bandit check handles empty directories gracefully. 

194 

195 Runs bandit on an empty directory and verifies a result is returned 

196 without errors. 

197 

198 Args: 

199 get_plugin: Fixture factory to get plugin instances. 

200 tmp_path: Pytest fixture providing a temporary directory. 

201 """ 

202 bandit_plugin = get_plugin("bandit") 

203 result = bandit_plugin.check([str(tmp_path)], {}) 

204 

205 assert_that(result).is_not_none() 

206 

207 

208# --- Tests for BanditPlugin.set_options method --- 

209 

210 

211@pytest.mark.parametrize( 

212 ("option_name", "option_value"), 

213 [ 

214 ("severity", "high"), 

215 ("confidence", "high"), 

216 ("skip", ["B101", "B102"]), 

217 ], 

218 ids=["severity", "confidence", "skip_tests"], 

219) 

220def test_set_options( 

221 get_plugin: Callable[[str], BaseToolPlugin], 

222 option_name: str, 

223 option_value: object, 

224) -> None: 

225 """Verify BanditPlugin.set_options correctly sets various options. 

226 

227 Tests that plugin options can be set and retrieved correctly. 

228 

229 Args: 

230 get_plugin: Fixture factory to get plugin instances. 

231 option_name: Name of the option to set. 

232 option_value: Value to set for the option. 

233 """ 

234 bandit_plugin = get_plugin("bandit") 

235 bandit_plugin.set_options(**{option_name: option_value}) 

236 assert_that(bandit_plugin.options.get(option_name)).is_not_none() 

237 if option_name == "skip": 

238 assert_that(bandit_plugin.options.get(option_name)).is_equal_to(option_value)