Automating Security in Python: A Hands-On Guide to SAST with Bandit and GitHub Actions
Introduction
In today's fast-paced development cycles, security can no longer be an afterthought. Static Application Security Testing (SAST) tools help developers catch vulnerabilities early by analyzing source code before it is compiled or deployed to production. In this article, we will explore how to apply a SAST tool to a Python application. While tools like SonarQube or Snyk are popular, we will focus on Bandit, a powerful and lightweight tool designed specifically to find common security issues in Python code.
What is Bandit?
Bandit is an open-source SAST tool maintained by the Python Packaging Authority (PyPA). It parses Python code, builds an Abstract Syntax Tree (AST), and runs appropriate plugins against the AST nodes to detect security flaws like hardcoded passwords, injection vulnerabilities, and unsafe library usage.
The Demo Application
Let's create a simple Python application that intentionally contains some security vulnerabilities for Bandit to find. We'll simulate a basic script that has hardcoded credentials and uses dangerous functions. Here is our app.py:
import subprocess
import yaml
# VULNERABILITY 1: Hardcoded credentials
DB_PASSWORD = "super_secret_password_123"
def run_command(user_input):
# VULNERABILITY 2: Command injection risk (shell=True)
print("Executing command...")
subprocess.call(user_input, shell=True)
def parse_config(yaml_data):
# VULNERABILITY 3: Unsafe YAML loading
config = yaml.load(yaml_data)
return config
if __name__ == "__main__":
print("App started.")
run_command("ls -la")
Running Bandit Locally
To test our code locally, we first install Bandit via pip:
pip install bandit
Then, we can run it against our app.py file:
bandit -r app.py
Bandit will immediately flag the issues we planted:
- B105: hardcoded_password_string - Flags the hardcoded
DB_PASSWORD. - B602: subprocess_popen_with_shell_equals_true - Warns about the potential command injection in
subprocess.call. - B506: yaml_load - Warns about using the unsafe
yaml.loadfunction instead ofyaml.safe_load.
Automating SAST with GitHub Actions
Running tools locally is great, but enforcing them automatically on every push is the best practice. Let's set up a GitHub Action to automatically run Bandit whenever code is pushed to our repository.
Create a file in your repository at .github/workflows/bandit.yml:
name: Bandit SAST Scan
on:
push:
branches: [ "main" ]
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install dependencies
run: pip install bandit
- name: Run Bandit
run: bandit -r . -f txt -o bandit-report.txt || true
- name: Upload Bandit Report
uses: actions/upload-artifact@v4
with:
name: bandit-security-report
path: bandit-report.txt
How this automation works:
- Triggers: The action runs on every push to the main branch.
- Execution: It installs Bandit and runs it across the entire repository (
-r .). - Artifacts: It generates a report and uploads it as an artifact, so developers can download and review the findings directly from the GitHub Actions tab. (Note: We use
|| trueto prevent the workflow from failing immediately so the report can be uploaded).
Conclusion
Integrating a SAST tool like Bandit into your Python projects is a straightforward process that significantly enhances your code's security posture. By automating this process with GitHub Actions, you ensure that vulnerabilities are caught in the CI/CD pipeline before they ever reach production.
🔗 Check out the full demo code and automated GitHub Action in this repository: https://github.com/Cristhian465/Trabajo-en-equipo-de-investigaci-n-N-01-Herramientas-Sast-para-aplicaciones
Comments
No comments yet. Start the discussion.