Prerequisites
- Basic understanding of programming concepts ๐
- Python installation (3.8+) ๐
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write clean, Pythonic code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on security testing with Bandit and Safety! ๐ In this guide, weโll explore how to protect your Python applications from vulnerabilities and security risks.
Youโll discover how automated security testing can transform your development process. Whether youโre building web applications ๐, APIs ๐, or command-line tools ๐ฅ๏ธ, understanding security testing is essential for writing safe, trustworthy code.
By the end of this tutorial, youโll feel confident using Bandit and Safety to scan your code for security issues! Letโs dive in! ๐โโ๏ธ
๐ Understanding Security Testing
๐ค What is Security Testing?
Security testing is like having a security guard ๐ฎ for your code. Think of it as a thorough inspection that checks every corner of your application for potential vulnerabilities that hackers might exploit.
In Python terms, security testing tools analyze your code to find:
- ๐ช Backdoors and security holes
- ๐ Weak authentication patterns
- ๐ SQL injection vulnerabilities
- ๐ก๏ธ Hardcoded secrets and passwords
๐ก Why Use Bandit and Safety?
Hereโs why developers love these tools:
- Automated Scanning ๐ค: Find vulnerabilities without manual review
- Early Detection ๐จ: Catch security issues before production
- Best Practices ๐: Learn secure coding patterns
- CI/CD Integration ๐: Automate security checks in your pipeline
Real-world example: Imagine building a banking app ๐ฆ. With Bandit and Safety, you can automatically detect if you accidentally commit API keys or use insecure random number generation!
๐ง Basic Syntax and Usage
๐ Installing the Tools
Letโs start by installing our security testing tools:
# ๐ Install Bandit for code scanning
pip install bandit
# ๐ก๏ธ Install Safety for dependency checking
pip install safety
# ๐ฏ Or install both at once!
pip install bandit safety
๐ก Explanation: Bandit scans your Python code for security issues, while Safety checks your installed packages for known vulnerabilities!
๐ฏ Running Your First Security Scan
Hereโs how to use these tools:
# ๐ Scan a single file with Bandit
bandit example.py
# ๐ Scan an entire project
bandit -r ./my_project/
# ๐ก๏ธ Check dependencies with Safety
safety check
# ๐ Generate detailed reports
bandit -r ./my_project/ -f json -o security_report.json
๐ก Practical Examples
๐จ Example 1: Detecting Common Vulnerabilities
Letโs create a file with security issues and scan it:
# vulnerable_code.py - DON'T use this in production! ๐ซ
import pickle
import subprocess
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
# โ Hardcoded password (B105)
PASSWORD = "super_secret_123" # ๐จ Never do this!
# โ Using pickle with untrusted data (B301)
def load_user_data(data):
return pickle.loads(data) # ๐ฅ Dangerous!
# โ Shell injection vulnerability (B602)
def run_command(user_input):
subprocess.call(user_input, shell=True) # ๐ฑ Security risk!
# โ Weak cryptography (B304)
def weak_random():
import random
return random.random() # ๐ฒ Not cryptographically secure!
# โ
Let's scan this file!
# Run: bandit vulnerable_code.py
๐ฏ Bandit Output:
>> Issue: [B105:hardcoded_password_string] Possible hardcoded password: 'super_secret_123'
Severity: Low Confidence: Medium
Location: vulnerable_code.py:7
>> Issue: [B301:blacklist] Pickle library used - potential security risk
Severity: Medium Confidence: High
Location: vulnerable_code.py:11
>> Issue: [B602:subprocess_popen_with_shell_equals_true] Risk of shell injection
Severity: High Confidence: High
Location: vulnerable_code.py:15
๐ก๏ธ Example 2: Secure Code Patterns
Now letโs write secure versions:
# secure_code.py - The RIGHT way! โ
import os
import json
import subprocess
import secrets
from cryptography.fernet import Fernet
# โ
Use environment variables for secrets
PASSWORD = os.environ.get('APP_PASSWORD') # ๐ Much better!
# โ
Use JSON instead of pickle
def load_user_data(data):
return json.loads(data) # ๐ฏ Safe parsing!
# โ
Avoid shell=True, validate input
def run_command(command_list):
# ๐ก๏ธ Only allow specific commands
allowed_commands = ['ls', 'pwd', 'echo']
if command_list[0] in allowed_commands:
subprocess.run(command_list, shell=False) # โจ Secure!
# โ
Use cryptographically secure random
def secure_random():
return secrets.token_hex(16) # ๐ Cryptographically strong!
# โ
Proper encryption
def encrypt_data(data):
key = Fernet.generate_key() # ๐๏ธ Generate secure key
f = Fernet(key)
encrypted = f.encrypt(data.encode()) # ๐ Encrypted!
return key, encrypted
๐ฆ Example 3: Real-World Security Scanning
Letโs create a complete security testing workflow:
# security_scanner.py - Automated security testing! ๐
import subprocess
import json
import sys
from datetime import datetime
class SecurityScanner:
def __init__(self, project_path):
self.project_path = project_path
self.results = {
'scan_date': datetime.now().isoformat(),
'bandit_issues': [],
'safety_issues': []
}
# ๐ Run Bandit scan
def scan_code(self):
print("๐ Running Bandit security scan...")
try:
# ๐ฏ Run Bandit and capture output
result = subprocess.run(
['bandit', '-r', self.project_path, '-f', 'json'],
capture_output=True,
text=True
)
if result.stdout:
bandit_results = json.loads(result.stdout)
self.results['bandit_issues'] = bandit_results.get('results', [])
print(f"โ
Found {len(self.results['bandit_issues'])} issues")
except Exception as e:
print(f"โ Bandit scan failed: {e}")
# ๐ก๏ธ Check dependencies
def check_dependencies(self):
print("๐ก๏ธ Checking dependencies with Safety...")
try:
# ๐ฆ Run Safety check
result = subprocess.run(
['safety', 'check', '--json'],
capture_output=True,
text=True
)
if result.stdout:
safety_results = json.loads(result.stdout)
self.results['safety_issues'] = safety_results
print(f"โ
Found {len(safety_results)} vulnerable packages")
except Exception as e:
print(f"โ Safety check failed: {e}")
# ๐ Generate report
def generate_report(self):
print("\n๐ Security Report Summary:")
print("=" * 50)
# ๐จ Bandit issues by severity
severity_count = {'High': 0, 'Medium': 0, 'Low': 0}
for issue in self.results['bandit_issues']:
severity_count[issue['issue_severity']] += 1
print("\n๐ Code Security Issues:")
for severity, count in severity_count.items():
emoji = "๐ด" if severity == "High" else "๐ก" if severity == "Medium" else "๐ข"
print(f" {emoji} {severity}: {count} issues")
# ๐ฆ Vulnerable dependencies
print(f"\n๐ก๏ธ Vulnerable Dependencies: {len(self.results['safety_issues'])}")
# ๐พ Save detailed report
with open('security_report.json', 'w') as f:
json.dump(self.results, f, indent=2)
print("\nโ
Detailed report saved to security_report.json")
# ๐ฏ Exit with error if high severity issues
if severity_count['High'] > 0:
print("\nโ High severity issues found! Fix before deploying!")
sys.exit(1)
# ๐ฎ Let's use it!
if __name__ == "__main__":
scanner = SecurityScanner("./my_project")
scanner.scan_code()
scanner.check_dependencies()
scanner.generate_report()
๐ Advanced Concepts
๐งโโ๏ธ Custom Bandit Configuration
When youโre ready to level up, create custom security profiles:
# .bandit configuration file
# ๐ฏ Customize your security scanning!
[bandit]
# ๐ซ Exclude test files
exclude: /test,/tests
# ๐ฏ Only check for high severity issues
severity: high
# ๐ Custom test selection
tests: B201,B301,B302,B303,B304,B305,B306
# ๐ก๏ธ Skip specific lines with comments
# Example: # nosec - tells Bandit to skip this line
๐๏ธ CI/CD Integration
Integrate security testing into your pipeline:
# .github/workflows/security.yml
# ๐ Automated security testing!
name: Security Tests ๐ก๏ธ
on: [push, pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python ๐
uses: actions/setup-python@v2
with:
python-version: '3.9'
- name: Install dependencies ๐ฆ
run: |
pip install bandit safety
pip install -r requirements.txt
- name: Run Bandit ๐
run: bandit -r . -f json -o bandit-report.json
- name: Check dependencies ๐ก๏ธ
run: safety check --json > safety-report.json
- name: Upload reports ๐
uses: actions/upload-artifact@v2
with:
name: security-reports
path: |
bandit-report.json
safety-report.json
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Ignoring Low Severity Issues
# โ Wrong approach - ignoring "low" severity
def get_user():
# B104: Low severity - but still important!
host = "0.0.0.0" # ๐จ Binds to all interfaces!
return host
# โ
Correct approach - fix all issues
def get_user():
# ๐ Bind to specific interface
host = "127.0.0.1" # โ
Much more secure!
return host
๐คฏ Pitfall 2: False Positives
# โ Bandit might flag this incorrectly
password_field_name = "password" # ๐จ False positive!
# โ
Use inline comments to suppress false positives
password_field_name = "password" # nosec - just a field name
# ๐ฏ Or use more specific variable names
form_field_for_password = "password" # โ
Clearer intent
๐ ๏ธ Best Practices
- ๐ฏ Regular Scanning: Run security tests on every commit
- ๐ Track Metrics: Monitor security issues over time
- ๐ก๏ธ Update Dependencies: Keep packages current
- ๐จ Security First: Design with security in mind
- โจ Education: Learn from the issues found
๐งช Hands-On Exercise
๐ฏ Challenge: Secure a Web Application
Create a security testing suite for a Flask application:
๐ Requirements:
- โ Scan all Python files for security issues
- ๐ก๏ธ Check all dependencies for vulnerabilities
- ๐ Generate a comprehensive security report
- ๐จ Fail the build if critical issues are found
- ๐จ Create a security dashboard!
๐ Bonus Points:
- Integrate with pre-commit hooks
- Add custom security rules
- Create automated fixes for common issues
๐ก Solution
๐ Click to see solution
# security_suite.py - Complete security testing suite! ๐
import os
import json
import subprocess
from datetime import datetime
from pathlib import Path
import matplotlib.pyplot as plt
class SecurityTestSuite:
def __init__(self, project_root):
self.project_root = Path(project_root)
self.report = {
'timestamp': datetime.now().isoformat(),
'project': str(self.project_root),
'summary': {},
'details': {}
}
# ๐ Comprehensive code scan
def scan_code_security(self):
print("๐ Starting comprehensive security scan...")
# ๐ฏ Run Bandit with detailed output
bandit_cmd = [
'bandit', '-r', str(self.project_root),
'-f', 'json', '--severity-level', 'low'
]
result = subprocess.run(bandit_cmd, capture_output=True, text=True)
if result.stdout:
bandit_data = json.loads(result.stdout)
# ๐ Process results
issues_by_severity = {'High': [], 'Medium': [], 'Low': []}
for issue in bandit_data.get('results', []):
severity = issue['issue_severity']
issues_by_severity[severity].append({
'file': issue['filename'],
'line': issue['line_number'],
'issue': issue['issue_text'],
'confidence': issue['issue_confidence']
})
self.report['details']['code_scan'] = issues_by_severity
self.report['summary']['total_code_issues'] = len(bandit_data.get('results', []))
# ๐จ Print colorful summary
print(f"\n๐ Code Security Summary:")
print(f" ๐ด High: {len(issues_by_severity['High'])}")
print(f" ๐ก Medium: {len(issues_by_severity['Medium'])}")
print(f" ๐ข Low: {len(issues_by_severity['Low'])}")
# ๐ก๏ธ Check dependencies
def check_dependencies(self):
print("\n๐ก๏ธ Checking dependency vulnerabilities...")
safety_cmd = ['safety', 'check', '--json']
result = subprocess.run(safety_cmd, capture_output=True, text=True)
vulnerabilities = []
if result.stdout:
try:
safety_data = json.loads(result.stdout)
vulnerabilities = safety_data
except:
# ๐ Parse non-JSON output
vulnerabilities = []
self.report['details']['dependencies'] = vulnerabilities
self.report['summary']['vulnerable_packages'] = len(vulnerabilities)
print(f" ๐ฆ Found {len(vulnerabilities)} vulnerable packages")
# ๐ Check for secrets
def scan_for_secrets(self):
print("\n๐ Scanning for hardcoded secrets...")
# ๐ฏ Common patterns to check
secret_patterns = [
r'api[_-]?key\s*=\s*["\'][^"\']+["\']',
r'password\s*=\s*["\'][^"\']+["\']',
r'secret\s*=\s*["\'][^"\']+["\']',
r'token\s*=\s*["\'][^"\']+["\']'
]
secrets_found = []
# ๐ Search through Python files
for py_file in self.project_root.rglob('*.py'):
if '.venv' in str(py_file) or '__pycache__' in str(py_file):
continue
try:
content = py_file.read_text()
for line_no, line in enumerate(content.splitlines(), 1):
for pattern in secret_patterns:
if any(word in line.lower() for word in ['password', 'secret', 'key', 'token']):
if '=' in line and not 'environ' in line:
secrets_found.append({
'file': str(py_file),
'line': line_no,
'content': line.strip()[:50] + '...'
})
except:
pass
self.report['details']['secrets'] = secrets_found
self.report['summary']['hardcoded_secrets'] = len(secrets_found)
print(f" ๐ Found {len(secrets_found)} potential secrets")
# ๐ Generate visual report
def generate_dashboard(self):
print("\n๐ Generating security dashboard...")
# ๐จ Create pie chart of issues
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
# ๐ Code issues by severity
code_issues = self.report['details']['code_scan']
severities = ['High', 'Medium', 'Low']
counts = [len(code_issues[s]) for s in severities]
colors = ['#ff4444', '#ffaa00', '#44ff44']
ax1.pie(counts, labels=severities, colors=colors, autopct='%1.1f%%')
ax1.set_title('๐ Code Issues by Severity')
# ๐ Overall security score
total_issues = sum(counts)
score = max(0, 100 - (total_issues * 5)) # 5 points per issue
ax2.bar(['Security Score'], [score], color='#4CAF50' if score >= 80 else '#FF9800')
ax2.set_ylim(0, 100)
ax2.set_ylabel('Score')
ax2.set_title(f'๐ก๏ธ Security Score: {score}/100')
plt.tight_layout()
plt.savefig('security_dashboard.png')
print(" โ
Dashboard saved to security_dashboard.png")
# ๐ Run complete suite
def run_full_scan(self):
print("๐ Running complete security test suite...\n")
self.scan_code_security()
self.check_dependencies()
self.scan_for_secrets()
self.generate_dashboard()
# ๐พ Save full report
with open('security_report.json', 'w') as f:
json.dump(self.report, f, indent=2)
print("\nโ
Security scan complete!")
print(f"๐ Full report: security_report.json")
print(f"๐ Dashboard: security_dashboard.png")
# ๐จ Exit with error if critical issues
if self.report['summary'].get('total_code_issues', 0) > 0:
high_issues = len(self.report['details']['code_scan']['High'])
if high_issues > 0:
print(f"\nโ {high_issues} HIGH severity issues found!")
return False
print("\nโ
All security checks passed! ๐")
return True
# ๐ฎ Test it out!
if __name__ == "__main__":
suite = SecurityTestSuite(".")
success = suite.run_full_scan()
if not success:
exit(1) # ๐จ Fail the build
๐ Key Takeaways
Youโve learned so much about security testing! Hereโs what you can now do:
- โ Use Bandit to scan code for vulnerabilities ๐
- โ Use Safety to check dependencies ๐ก๏ธ
- โ Integrate security into your CI/CD pipeline ๐
- โ Write secure code following best practices ๐
- โ Build security into your development process! ๐
Remember: Security isnโt a feature, itโs a requirement! Make it part of your daily workflow. ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered security testing with Bandit and Safety!
Hereโs what to do next:
- ๐ป Run security scans on your existing projects
- ๐๏ธ Set up automated security testing in CI/CD
- ๐ Learn about OWASP Top 10 vulnerabilities
- ๐ Share security knowledge with your team!
Remember: Every secure application starts with a developer who cares about security. Keep scanning, keep learning, and most importantly, keep your code safe! ๐
Happy secure coding! ๐๐โจ