0
# Plugin Development
1
2
Framework for creating custom security tests using Bandit's decorator-based plugin system. The plugin architecture enables extensible security analysis by registering custom tests that integrate seamlessly with Bandit's scanning workflow.
3
4
## Capabilities
5
6
### Test Decorators
7
8
Decorator functions that register security tests and configure their behavior within Bandit's plugin system.
9
10
```python { .api }
11
def checks(*args):
12
"""
13
Specify which AST node types the test should analyze.
14
15
Parameters:
16
- *args: str, AST node type names ('Call', 'Import', 'Str', etc.)
17
18
Usage:
19
@checks('Call', 'Attribute') - Run test on function calls and attribute access
20
@checks('Import', 'ImportFrom') - Run test on import statements
21
"""
22
23
def test_id(id_val):
24
"""
25
Assign unique identifier to security test.
26
27
Parameters:
28
- id_val: str, unique test identifier (e.g., 'B101', 'B999')
29
30
Usage:
31
@test_id('B999') - Assign test ID B999
32
"""
33
34
def takes_config(name=None):
35
"""
36
Indicate test accepts configuration data.
37
38
Parameters:
39
- name: str, configuration section name (optional)
40
41
Usage:
42
@takes_config('hardcoded_password') - Access config['hardcoded_password']
43
@takes_config() - Access general test configuration
44
"""
45
46
def accepts_baseline(*args):
47
"""
48
Mark formatter as supporting baseline results.
49
Used for output formatters that can handle baseline comparison.
50
51
Parameters:
52
- *args: Additional baseline configuration options
53
"""
54
```
55
56
### Built-in Test IDs
57
58
Reference list of built-in security test identifiers and their purposes.
59
60
```python { .api }
61
# Assert and Debug Tests
62
B101 = "assert_used" # Use of assert detected
63
B201 = "flask_debug_true" # Flask app run in debug mode
64
65
# Code Injection Tests
66
B102 = "exec_used" # Use of exec detected
67
B301 = "pickle_load" # Pickle library usage
68
B506 = "yaml_load" # Use of yaml.load
69
70
# Shell Injection Tests
71
B601 = "paramiko_calls" # Paramiko shell commands
72
B602 = "subprocess_popen_with_shell_equals_true"
73
B603 = "subprocess_without_shell_equals_true"
74
B604 = "any_other_function_with_shell_equals_true"
75
B605 = "start_process_with_a_shell"
76
B606 = "start_process_with_no_shell"
77
B607 = "start_process_with_partial_path"
78
79
# SQL Injection Tests
80
B608 = "hardcoded_sql_expressions" # SQL string formatting
81
82
# Cryptography Tests
83
B101 = "hashlib_insecure_functions" # Weak hash functions
84
B326 = "weak_cryptographic_key" # Weak crypto keys
85
86
# File System Tests
87
B103 = "set_bad_file_permissions" # Insecure file permissions
88
B108 = "hardcoded_tmp_directory" # Hardcoded temp paths
89
90
# Network Security Tests
91
B104 = "hardcoded_bind_all_interfaces" # Binding to all interfaces
92
B501 = "request_with_no_cert_validation" # No certificate validation
93
B502 = "ssl_with_bad_version" # Weak SSL/TLS versions
94
95
# Password/Secret Tests
96
B105 = "hardcoded_password_string" # Hardcoded password strings
97
B106 = "hardcoded_password_funcarg" # Password in function args
98
B107 = "hardcoded_password_default" # Password in defaults
99
```
100
101
## Usage Examples
102
103
### Basic Security Test
104
105
```python
106
from bandit.core import test_properties as test
107
import bandit
108
109
@test.checks('Call')
110
@test.test_id('B999')
111
def detect_dangerous_function(context):
112
"""
113
Detect calls to a specific dangerous function.
114
115
Parameters:
116
- context: Context object with call information
117
118
Returns:
119
Issue object if vulnerability found, None otherwise
120
"""
121
if context.call_function_name == 'dangerous_function':
122
return bandit.Issue(
123
severity=bandit.HIGH,
124
confidence=bandit.HIGH,
125
text="Call to dangerous_function() detected",
126
cwe=94 # Code Injection CWE
127
)
128
```
129
130
### Multi-Node Type Test
131
132
```python
133
@test.checks('Import', 'ImportFrom', 'Call')
134
@test.test_id('B998')
135
def detect_crypto_misuse(context):
136
"""Detect cryptographic misuse across imports and calls."""
137
138
# Check imports
139
weak_crypto_modules = ['md5', 'sha1', 'des']
140
for module in weak_crypto_modules:
141
if context.is_module_being_imported(module):
142
return bandit.Issue(
143
severity=bandit.MEDIUM,
144
confidence=bandit.HIGH,
145
text=f"Import of weak cryptographic module: {module}",
146
cwe=326
147
)
148
149
# Check function calls
150
if context.call_function_name_qual in ['hashlib.md5', 'hashlib.sha1']:
151
return bandit.Issue(
152
severity=bandit.MEDIUM,
153
confidence=bandit.HIGH,
154
text="Use of weak hash function",
155
cwe=326
156
)
157
```
158
159
### Configurable Security Test
160
161
```python
162
@test.checks('Str')
163
@test.test_id('B997')
164
@test.takes_config('sensitive_strings')
165
def detect_sensitive_strings(context, config):
166
"""
167
Detect sensitive string patterns from configuration.
168
169
Configuration format in bandit.yaml:
170
sensitive_strings:
171
patterns:
172
- "password"
173
- "secret"
174
- "api_key"
175
min_length: 8
176
"""
177
if not context.string_literal_value:
178
return
179
180
# Get configuration
181
patterns = config.get('patterns', [])
182
min_length = config.get('min_length', 6)
183
184
string_value = context.string_literal_value.lower()
185
186
if len(string_value) >= min_length:
187
for pattern in patterns:
188
if pattern.lower() in string_value:
189
return bandit.Issue(
190
severity=bandit.MEDIUM,
191
confidence=bandit.LOW,
192
text=f"Potentially sensitive string containing '{pattern}'",
193
cwe=200 # Information Exposure
194
)
195
```
196
197
### Advanced Context Analysis Test
198
199
```python
200
@test.checks('Call')
201
@test.test_id('B996')
202
def detect_unsafe_deserialization(context):
203
"""Detect unsafe deserialization patterns."""
204
205
unsafe_functions = [
206
'pickle.loads',
207
'pickle.load',
208
'cPickle.loads',
209
'cPickle.load',
210
'dill.loads',
211
'yaml.load'
212
]
213
214
if context.call_function_name_qual in unsafe_functions:
215
# Analyze arguments for user input
216
severity = bandit.MEDIUM
217
confidence = bandit.MEDIUM
218
219
if context.call_args:
220
first_arg = context.call_args[0]
221
222
# Check for direct user input patterns
223
if hasattr(first_arg, 'id') and first_arg.id in ['input', 'raw_input']:
224
severity = bandit.HIGH
225
confidence = bandit.HIGH
226
227
# Check for network input patterns
228
elif (hasattr(first_arg, 'attr') and
229
any(net_attr in first_arg.attr for net_attr in ['recv', 'read', 'readline'])):
230
severity = bandit.HIGH
231
confidence = bandit.MEDIUM
232
233
return bandit.Issue(
234
severity=severity,
235
confidence=confidence,
236
text=f"Unsafe deserialization with {context.call_function_name_qual}",
237
cwe=502 # Deserialization of Untrusted Data
238
)
239
```
240
241
### Test with Error Handling
242
243
```python
244
@test.checks('Call')
245
@test.test_id('B995')
246
def detect_command_injection(context):
247
"""Detect potential command injection vulnerabilities."""
248
249
try:
250
if context.call_function_name_qual not in ['os.system', 'os.popen', 'subprocess.call']:
251
return
252
253
if not context.call_args:
254
return
255
256
first_arg = context.call_args[0]
257
258
# String literal analysis
259
if hasattr(first_arg, 's'):
260
command = first_arg.s
261
dangerous_chars = ['&', '|', ';', '`', '$', '(', ')']
262
263
if any(char in command for char in dangerous_chars):
264
return bandit.Issue(
265
severity=bandit.HIGH,
266
confidence=bandit.MEDIUM,
267
text="Command with shell metacharacters detected",
268
cwe=78
269
)
270
271
# Variable/expression analysis
272
elif hasattr(first_arg, 'id'):
273
# Variable is being used - lower confidence
274
return bandit.Issue(
275
severity=bandit.MEDIUM,
276
confidence=bandit.LOW,
277
text="Command execution with variable input",
278
cwe=78
279
)
280
281
except AttributeError:
282
# Handle cases where AST nodes don't have expected attributes
283
return None
284
```
285
286
### Custom Formatter Plugin
287
288
```python
289
@test.accepts_baseline()
290
def custom_formatter(manager, fileobj, sev_level, conf_level, lines):
291
"""
292
Custom output formatter for security reports.
293
294
Parameters:
295
- manager: BanditManager instance with scan results
296
- fileobj: File-like object for output
297
- sev_level: str, minimum severity level to include
298
- conf_level: str, minimum confidence level to include
299
- lines: bool, include line numbers in output
300
"""
301
302
# Get filtered issues
303
issues = manager.get_issue_list(sev_level, conf_level)
304
305
# Custom output format
306
fileobj.write("=== Custom Security Report ===\n")
307
fileobj.write(f"Total Issues: {len(issues)}\n\n")
308
309
# Group by severity
310
by_severity = {}
311
for issue in issues:
312
severity = issue.severity
313
if severity not in by_severity:
314
by_severity[severity] = []
315
by_severity[severity].append(issue)
316
317
# Output by severity level
318
for severity in ['HIGH', 'MEDIUM', 'LOW']:
319
if severity in by_severity:
320
fileobj.write(f"\n{severity} SEVERITY ISSUES:\n")
321
fileobj.write("-" * 40 + "\n")
322
323
for issue in by_severity[severity]:
324
fileobj.write(f"File: {issue.fname}\n")
325
if lines:
326
fileobj.write(f"Line: {issue.lineno}\n")
327
fileobj.write(f"Test: {issue.test_id}\n")
328
fileobj.write(f"Issue: {issue.text}\n")
329
fileobj.write(f"CWE: {issue.cwe}\n\n")
330
```
331
332
## Plugin Registration
333
334
Plugins are automatically discovered and registered through entry points in `setup.cfg`:
335
336
```ini
337
[entry_points]
338
bandit.plugins =
339
my_custom_test = mypackage.security_tests:my_custom_test
340
341
bandit.formatters =
342
custom = mypackage.formatters:custom_formatter
343
```