Static Analysis and Security
Static Analysis and Security
The boring shipping bar is: ruff check is clean, dependencies are audited, and secrets are not in source. A linter will not make the desk secure. It will catch the mistakes that keep recurring.
Mental model
Static analysis reads the code without running it. ruff check finds unused imports, undefined names, and a pile of foot-guns. Run it in CI the same way you run tests.
Audit means you look up known vulnerabilities in the lockfile. This book’s toolchain command is uv audit. The older standalone tool is pip-audit. Both need a lockfile or an environment worth scanning. They need the network. They are not a substitute for ruff.
Secrets are values that grant access: API tokens, passwords, private keys. They live in the environment or a secret store. They do not live in .py files, not even “just for now.”
Worked examples
Case 1: A tiny program worth checking
Save as label.py. This is the clean baseline.
# label.py
def label(ticket_id: int, table: int) -> str:
return f"ticket {ticket_id} → table {table}"
def main() -> None:
print(label(7, 12))
if __name__ == "__main__":
main()Run:
uv run python label.pyOutput:
ticket 7 → table 12
Then:
uv run ruff check label.pyOn a clean file ruff prints nothing and exits zero. That silence is the success signal.
Case 2: ruff catches an unused import
Save as unused_import.py. The program still runs. ruff still fails the check.
# unused_import.py
import os
def label(ticket_id: int, table: int) -> str:
return f"ticket {ticket_id} → table {table}"
def main() -> None:
print(label(7, 12))
if __name__ == "__main__":
main()Run the program:
uv run python unused_import.pyOutput:
ticket 7 → table 12
Run ruff:
uv run ruff check unused_import.pyOutput:
F401 [*] `os` imported but unused
--> unused_import.py:2:8
|
1 | # unused_import.py
2 | import os
| ^^
help: Remove unused import: `os`
|
1 | # unused_import.py
- import os
2 |
|
Found 1 error.
[*] 1 fixable with the `--fix` option.
ruff check --fix unused_import.py removes the import. Prefer understanding the diagnostic once, then let --fix take the obvious ones.
Case 3: Secrets from the environment
Save as env_key.py. Missing key is a hard exit, not a default token in source.
# env_key.py
import os
import sys
def main() -> None:
key = os.environ.get("DESK_API_KEY")
if key is None:
print("DESK_API_KEY is missing")
sys.exit(1)
print("key length:", len(key))
if __name__ == "__main__":
main()Run (unset):
uv run python env_key.pyOutput:
DESK_API_KEY is missing
Run with a value:
DESK_API_KEY=abc uv run python env_key.pyOutput:
key length: 3
Never print the key itself.
Case 4: Audit commands (project directory)
In a project that has pyproject.toml and a lockfile:
uv lock
uv audituv audit talks to a vulnerability service (OSV by default). It is allowed to need the network. If you maintain an older pipeline, pip-audit against the same lockfile is the same idea.
Do not paste audit JSON into the book as a fixture: the database changes. Wire the command into CI and fail the build on known issues you have not ignored on purpose (uv audit --ignore ... is explicit).
The trap
A literal in the file “works” and leaks the first time the repo is copied.
Save as leaked_key.py:
# leaked_key.py
API_KEY = "not-a-real-key"
def main() -> None:
print("would send", API_KEY)
if __name__ == "__main__":
main()Run:
uv run python leaked_key.pyOutput:
would send not-a-real-key
Git history keeps it after you delete the line. Case 3 is the fix: read DESK_API_KEY from the environment. Rotate anything that ever sat in source.
The boring rule
uv run ruff check(andruff format) on every change.uv lockthenuv audit(orpip-audit) in CI.- Secrets in the environment or a vault. Never in source, tests, or example configs you commit.
- Do not disable a ruff rule globally to silence one line; use a targeted
noqawith a reason, rarely. - Treat “the program ran” as necessary, not sufficient, to ship.
Try this
- Add an unused
import jsontolabel.pyand runuv run ruff check label.py. - Run
ruff check --fix unused_import.pyand confirm the import is gone. - In
env_key.py, reject an empty string (keyis"") the same way you rejectNone.