or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

command-line-tools.mdcontext-analysis.mdcore-management.mdindex.mdissue-reporting.mdoutput-formatters.mdplugin-development.md

plugin-development.mddocs/

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

```