Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions bandit-python/.github/workflows/security.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Security

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

jobs:
bandit-and-ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.14"
- name: Install scanners
run: python -m pip install "bandit[toml]==1.9.4" ruff==0.17.3
- name: Ruff
run: ruff check .
- name: Bandit
if: success() || failure()
run: bandit -r . -c pyproject.toml
- name: Bandit on tests
if: success() || failure()
run: bandit -r tests -s B101
- name: Reject blanket suppressions
if: success() || failure()
run: |
! git grep -nE '#[[:space:]]*(nosec|noqa)[[:space:]]*(#|$)' -- '*.py'
14 changes: 14 additions & 0 deletions bandit-python/.pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.17.3
hooks:
- id: ruff-check
args: [--fix]
- id: ruff-format
- repo: https://github.com/PyCQA/bandit
rev: 1.9.4
hooks:
- id: bandit
args: ["-c", "pyproject.toml"]
additional_dependencies: ["bandit[toml]"]
language_version: python3.14
32 changes: 32 additions & 0 deletions bandit-python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Hardening AI-Generated Python Code With Bandit and Ruff

Sample code for the Real Python guide on hardening AI-generated Python with Bandit and Ruff.

- `insecure/helpers.py`: realistic "AI-generated" helpers with typical security issues
- `fixed/helpers.py`: the same helpers after fixes
- `pyproject.toml`: Bandit and Ruff settings (`bandit -c pyproject.toml`)
- `.pre-commit-config.yaml`: pre-commit hooks for Ruff and Bandit
- `.github/workflows/security.yml`: GitHub Actions workflow for your own repository

## Setup

Requires Python 3.11 or later.

```sh
$ python -m venv venv
$ source venv/bin/activate
(venv) $ python -m pip install -r requirements.txt
```

## Run

```sh
(venv) $ bandit -r insecure
(venv) $ ruff check --select S insecure
(venv) $ bandit -r fixed -c pyproject.toml
(venv) $ ruff check fixed
```

The `pyproject.toml` here has one extra line that the guide's file doesn't: `extend-exclude = ["insecure"]`. It keeps the `realpython/materials` lint from failing on the deliberately insecure module. Remove that line to see the `ruff-check` pre-commit hook fail on `insecure/helpers.py`, as in Step 4.

The workflow in `.github/workflows/` runs only after you copy it into your own repository.
98 changes: 98 additions & 0 deletions bandit-python/fixed/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import ast
import hashlib
import json
import logging
import os
import secrets
import sqlite3
import subprocess
import tempfile

import requests
import yaml

logger = logging.getLogger(__name__)

API_KEY = os.environ["API_KEY"]
DB_PASSWORD = os.environ["DB_PASSWORD"]


def run_report(filename):
subprocess.run(["sort", f"reports/{filename}"], check=True) # nosec B603 B607 # noqa: S603, S607


def ping_host(host):
return subprocess.check_output(["ping", "-c", "1", host]) # nosec B603 B607 # noqa: S603, S607


def make_token():
return secrets.token_hex(16)


def hash_password(password, salt):
return hashlib.scrypt(
password.encode(), salt=salt, n=2**14, r=8, p=1
).hex()


def checksum(data):
return hashlib.md5(data, usedforsecurity=False).hexdigest()


def load_config(text):
return yaml.safe_load(text)


def parse_setting(text):
return ast.literal_eval(text)


def load_session(blob):
return json.loads(blob)


def fetch_status(url):
return requests.get(url, timeout=10).status_code


def fetch_profile(url):
return requests.get(url, timeout=10).json()


def write_cache(data):
with tempfile.NamedTemporaryFile("w", delete=False) as f:
f.write(data)
return f.name


def find_user(conn: sqlite3.Connection, name):
query = "SELECT * FROM users WHERE name = ?"
return conn.execute(query, (name,)).fetchall()


def safe_int(value):
try:
return int(value)
except ValueError:
logger.warning("Not an integer: %r", value)
return None


def parse_all(values):
results = []
for value in values:
try:
results.append(int(value))
except ValueError:
logger.warning("Skipping %r", value)
return results


def check_admin(user):
if not user.is_admin:
raise PermissionError("Admin only")
return True


def start_server(app):
app.run(host="127.0.0.1", port=8000)
85 changes: 85 additions & 0 deletions bandit-python/insecure/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import hashlib
import pickle
import random
import subprocess
import tempfile
import yaml
import requests

API_KEY = "sk-live-4f9a8b7c6d5e4f3a2b1c"
DB_PASSWORD = "admin123"


def run_report(filename):
subprocess.run(f"cat reports/{filename} | sort", shell=True)


def ping_host(host):
return subprocess.check_output("ping -c 1 " + host, shell=True)


def make_token():
return "".join(random.choice("abcdef0123456789") for _ in range(32))


def hash_password(password):
return hashlib.md5(password.encode()).hexdigest()


def load_config(text):
return yaml.load(text, Loader=yaml.Loader)


def parse_setting(text):
return eval(text)


def load_session(blob):
return pickle.loads(blob)


def fetch_status(url):
response = requests.get(url, verify=False)
return response.status_code


def fetch_profile(url):
return requests.get(url).json()


def write_cache(data):
path = tempfile.mktemp()
with open(path, "w") as f:
f.write(data)
return path


def find_user(conn, name):
query = "SELECT * FROM users WHERE name = '%s'" % name
return conn.execute(query).fetchall()


def safe_int(value):
try:
return int(value)
except Exception:
pass


def parse_all(values):
results = []
for value in values:
try:
results.append(int(value))
except:
continue
return results


def check_admin(user):
assert user.is_admin, "Admin only"
return True


def start_server(app):
app.run(host="0.0.0.0", port=8000)
15 changes: 15 additions & 0 deletions bandit-python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[tool.bandit]
exclude_dirs = ["venv", ".venv", "tests"]
skips = ["B404"]

[tool.ruff]
line-length = 79
# Keeps the repo-wide CI lint from failing on the deliberately insecure
# module. Remove this line to see Ruff reject it in the pre-commit hook.
extend-exclude = ["insecure"]

[tool.ruff.lint]
select = ["E", "F", "S", "BLE"]

[tool.ruff.lint.per-file-ignores]
"tests/**" = ["S101"]
5 changes: 5 additions & 0 deletions bandit-python/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
bandit==1.9.4
pre-commit==4.7.0
PyYAML==6.0.3
requests==2.34.2
ruff==0.17.3
Loading