Initial commit
This commit is contained in:
293
skills/python-uv-scripts/reference/bash-to-python.md
Normal file
293
skills/python-uv-scripts/reference/bash-to-python.md
Normal file
@@ -0,0 +1,293 @@
|
||||
# Converting Bash Scripts to Python uv Scripts
|
||||
|
||||
This document guides the conversion of bash scripts to Python uv scripts.
|
||||
|
||||
## When Conversion Makes Sense
|
||||
|
||||
### ✅ Good Candidates for Conversion
|
||||
|
||||
**Convert when**:
|
||||
|
||||
- Script needs better error handling
|
||||
- Cross-platform compatibility required
|
||||
- Complex data processing needed
|
||||
- API interactions involved
|
||||
- Script will grow in complexity
|
||||
|
||||
**Examples**:
|
||||
|
||||
```bash
|
||||
# Good candidate - API interaction
|
||||
curl -X POST https://api.example.com/data \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{"status": "active"}'
|
||||
|
||||
# Good candidate - Data processing
|
||||
cat data.json | jq '.users[] | select(.active == true)'
|
||||
|
||||
# Good candidate - Complex logic
|
||||
for file in $(find /data -name "*.log"); do
|
||||
count=$(grep -c "ERROR" "$file")
|
||||
if [ $count -gt 100 ]; then
|
||||
# Complex processing
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
### ❌ Keep as Bash When
|
||||
|
||||
**Don't convert when**:
|
||||
|
||||
- Simple file operations (cp, mv, mkdir)
|
||||
- Shell-specific features heavily used (job control, pipes)
|
||||
- System administration tasks
|
||||
- Script is <10 lines and works fine
|
||||
- Team primarily knows bash
|
||||
|
||||
**Examples**:
|
||||
|
||||
```bash
|
||||
# Keep as bash - Simple operations
|
||||
#!/bin/bash
|
||||
mkdir -p /var/log/app
|
||||
cp config.yaml /etc/app/
|
||||
|
||||
# Keep as bash - Shell-specific
|
||||
#!/bin/bash
|
||||
find /data -name "*.tmp" -mtime +7 -delete
|
||||
|
||||
# Keep as bash - System admin
|
||||
#!/bin/bash
|
||||
systemctl restart nginx
|
||||
journalctl -u nginx -f
|
||||
```
|
||||
|
||||
## Common Conversions
|
||||
|
||||
### File Operations
|
||||
|
||||
**Bash**:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
if [ -f "/etc/config.yaml" ]; then
|
||||
cp /etc/config.yaml /backup/
|
||||
fi
|
||||
```
|
||||
|
||||
**Python**:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
config = Path("/etc/config.yaml")
|
||||
if config.exists():
|
||||
shutil.copy(config, "/backup/")
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
**Bash**:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
API_URL=${API_URL:-"https://api.example.com"}
|
||||
if [ -z "$API_TOKEN" ]; then
|
||||
echo "Error: API_TOKEN not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
**Python**:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
API_URL = os.getenv("API_URL", "https://api.example.com")
|
||||
API_TOKEN = os.getenv("API_TOKEN")
|
||||
|
||||
if not API_TOKEN:
|
||||
print("Error: API_TOKEN not set", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
```
|
||||
|
||||
### Running Commands
|
||||
|
||||
**Bash**:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
output=$(systemctl status nginx)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: nginx not running"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
**Python**:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["systemctl", "status", "nginx"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
print(result.stdout)
|
||||
except subprocess.CalledProcessError:
|
||||
print("Error: nginx not running", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
```
|
||||
|
||||
### HTTP Requests
|
||||
|
||||
**Bash**:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
response=$(curl -s -X GET https://api.github.com/users/octocat)
|
||||
name=$(echo "$response" | jq -r '.name')
|
||||
echo "Name: $name"
|
||||
```
|
||||
|
||||
**Python**:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "httpx>=0.27.0",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import httpx
|
||||
|
||||
response = httpx.get("https://api.github.com/users/octocat")
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
print(f"Name: {data['name']}")
|
||||
```
|
||||
|
||||
### JSON Processing
|
||||
|
||||
**Bash**:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
jq '.users[] | select(.active == true) | .name' data.json
|
||||
```
|
||||
|
||||
**Python**:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
data = json.loads(Path("data.json").read_text())
|
||||
active_names = [
|
||||
user["name"]
|
||||
for user in data["users"]
|
||||
if user.get("active")
|
||||
]
|
||||
print("\n".join(active_names))
|
||||
```
|
||||
|
||||
## Decision Framework
|
||||
|
||||
```text
|
||||
Should I convert this bash script?
|
||||
|
||||
├─ Is it >50 lines? → Consider conversion
|
||||
├─ Does it process JSON/YAML? → Strong candidate
|
||||
├─ Does it make API calls? → Strong candidate
|
||||
├─ Does it have complex logic? → Consider conversion
|
||||
├─ Does it need better error handling? → Consider conversion
|
||||
├─ Is it mostly shell commands? → Keep as bash
|
||||
└─ Is it <10 lines and works? → Keep as bash
|
||||
```
|
||||
|
||||
## Hybrid Approach
|
||||
|
||||
Sometimes the best solution is calling bash from Python:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
"""
|
||||
Hybrid: Use Python for logic, bash for system commands.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
|
||||
def backup_with_rsync(source: str, dest: str):
|
||||
"""Use rsync (better than reimplementing in Python)."""
|
||||
subprocess.run(
|
||||
["rsync", "-av", "--delete", source, dest],
|
||||
check=True
|
||||
)
|
||||
|
||||
# Python logic here
|
||||
# ...
|
||||
|
||||
# Leverage bash tools where appropriate
|
||||
backup_with_rsync("/data/", "/backup/")
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
**Convert to Python when**:
|
||||
|
||||
- Complex logic or data processing
|
||||
- API interactions
|
||||
- Cross-platform needs
|
||||
- Better error handling required
|
||||
- Will grow in complexity
|
||||
|
||||
**Keep as Bash when**:
|
||||
|
||||
- Simple file operations
|
||||
- System administration
|
||||
- Heavily uses shell features
|
||||
- Works well and won't change
|
||||
- Team expertise is bash
|
||||
|
||||
**Consider Hybrid**:
|
||||
|
||||
- Complex Python logic + system commands
|
||||
- Leverage both Python libraries and shell tools
|
||||
63
skills/python-uv-scripts/reference/dependency-management.md
Normal file
63
skills/python-uv-scripts/reference/dependency-management.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Dependency Management Reference
|
||||
|
||||
> **Status**: 🚧 Placeholder - Content in development
|
||||
|
||||
## Overview
|
||||
|
||||
Comprehensive guide to managing dependencies in UV single-file scripts using PEP 723 inline metadata.
|
||||
|
||||
## Topics to Cover
|
||||
|
||||
- [ ] Version pinning strategies
|
||||
- [ ] Semantic versioning best practices
|
||||
- [ ] Dependency conflict resolution
|
||||
- [ ] Optional dependencies
|
||||
- [ ] Development vs production dependencies
|
||||
- [ ] Security updates and vulnerability scanning
|
||||
- [ ] Lock file equivalents for scripts
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Version Pinning Strategies
|
||||
|
||||
**Exact pinning** (most restrictive):
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# dependencies = ["requests==2.31.0"]
|
||||
# ///
|
||||
```
|
||||
|
||||
**Compatible release** (recommended):
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# dependencies = ["requests~=2.31.0"] # >=2.31.0, <2.32.0
|
||||
# ///
|
||||
```
|
||||
|
||||
**Minimum version**:
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# dependencies = ["requests>=2.31.0"]
|
||||
# ///
|
||||
```
|
||||
|
||||
### Following Repository Standards
|
||||
|
||||
From this repository's `pyproject.toml`, we use:
|
||||
|
||||
- `>=` for minimum versions with flexibility
|
||||
- Specific version ranges for critical dependencies
|
||||
- Regular dependency audits with Renovate
|
||||
|
||||
## TODO
|
||||
|
||||
This file will be expanded to include:
|
||||
|
||||
- Complete version specifier syntax
|
||||
- Dependency resolution strategies
|
||||
- Security scanning integration
|
||||
- Update strategies and automation
|
||||
- Conflict resolution techniques
|
||||
391
skills/python-uv-scripts/reference/pep-723-spec.md
Normal file
391
skills/python-uv-scripts/reference/pep-723-spec.md
Normal file
@@ -0,0 +1,391 @@
|
||||
# PEP 723: Inline Script Metadata Specification
|
||||
|
||||
Complete reference for PEP 723 inline script metadata format used by uv.
|
||||
|
||||
## Overview
|
||||
|
||||
PEP 723 defines a standardized way to embed dependency and configuration metadata directly within Python script files. This eliminates the need for separate `requirements.txt` files and enables self-contained, reproducible scripts.
|
||||
|
||||
## Basic Format
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "package-name>=1.0.0",
|
||||
# ]
|
||||
# ///
|
||||
```
|
||||
|
||||
**Key Requirements:**
|
||||
|
||||
- Must appear as comments (`#`)
|
||||
- Must use `# /// script` as opening marker
|
||||
- Must use `# ///` as closing marker
|
||||
- Must be valid TOML syntax
|
||||
- Recommended placement: After shebang, before module docstring
|
||||
|
||||
## Complete Specification
|
||||
|
||||
### Required Fields
|
||||
|
||||
#### requires-python
|
||||
|
||||
Specifies minimum Python version:
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# ///
|
||||
```
|
||||
|
||||
**Formats:**
|
||||
|
||||
```python
|
||||
requires-python = ">=3.11" # Minimum version
|
||||
requires-python = ">=3.11,<3.13" # Version range
|
||||
requires-python = "==3.12" # Exact version
|
||||
```
|
||||
|
||||
#### dependencies
|
||||
|
||||
Lists required packages:
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "httpx>=0.27.0",
|
||||
# "rich>=13.0.0",
|
||||
# "typer~=0.9.0",
|
||||
# ]
|
||||
# ///
|
||||
```
|
||||
|
||||
**Version Specifiers:**
|
||||
|
||||
```python
|
||||
"package" # Any version
|
||||
"package>=1.0" # Minimum version
|
||||
"package>=1.0,<2.0" # Version range
|
||||
"package~=1.2.3" # Compatible release (1.2.x)
|
||||
"package==1.2.3" # Exact version
|
||||
```
|
||||
|
||||
### Optional Fields
|
||||
|
||||
#### [tool.uv] Section
|
||||
|
||||
uv-specific configuration:
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
#
|
||||
# [tool.uv]
|
||||
# exclude-newer = "2024-10-01T00:00:00Z"
|
||||
# index-url = "https://pypi.org/simple"
|
||||
# ///
|
||||
```
|
||||
|
||||
**Available options:**
|
||||
|
||||
- `exclude-newer`: Only use packages published before this date
|
||||
- `index-url`: Alternative PyPI index
|
||||
- `extra-index-url`: Additional package indexes
|
||||
- `find-links`: Additional package sources
|
||||
- `no-index`: Ignore PyPI entirely
|
||||
|
||||
#### [tool.uv.sources]
|
||||
|
||||
Custom package sources:
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# dependencies = ["my-package"]
|
||||
#
|
||||
# [tool.uv.sources]
|
||||
# my-package = { git = "https://github.com/user/repo", tag = "v1.0" }
|
||||
# ///
|
||||
```
|
||||
|
||||
#### Valid [tool.uv] Fields
|
||||
|
||||
Additional uv-specific configuration (optional):
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
#
|
||||
# [tool.uv]
|
||||
# exclude-newer = "2025-01-01T00:00:00Z" # Reproducibility constraint
|
||||
# ///
|
||||
```
|
||||
|
||||
**Note**: For custom metadata like purpose, team, author, use Python docstrings instead:
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
"""
|
||||
Purpose: cluster-monitoring
|
||||
Team: infrastructure
|
||||
Author: devops@example.com
|
||||
Created: 2024-10-20
|
||||
"""
|
||||
```
|
||||
|
||||
## Complete Examples
|
||||
|
||||
### Minimal Script
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
"""Simple script with no dependencies"""
|
||||
|
||||
print("Hello, world!")
|
||||
```
|
||||
|
||||
### Production Script
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script --quiet
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "httpx>=0.27.0",
|
||||
# "rich>=13.0.0",
|
||||
# "typer>=0.9.0",
|
||||
# ]
|
||||
#
|
||||
# [tool.uv]
|
||||
# exclude-newer = "2025-01-01T00:00:00Z"
|
||||
# ///
|
||||
"""
|
||||
API client for Proxmox cluster monitoring
|
||||
|
||||
Purpose: api-client
|
||||
Team: infrastructure
|
||||
Author: devops@spaceships.work
|
||||
|
||||
Usage:
|
||||
check_cluster.py [--node NODE] [--json]
|
||||
"""
|
||||
|
||||
import typer
|
||||
import httpx
|
||||
from rich import print
|
||||
```
|
||||
|
||||
### With Git Dependencies
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "my-internal-lib",
|
||||
# ]
|
||||
#
|
||||
# [tool.uv.sources]
|
||||
# my-internal-lib = { git = "https://github.com/org/lib.git", tag = "v1.2.3" }
|
||||
# ///
|
||||
```
|
||||
|
||||
### With Local Dependencies
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "my-local-package",
|
||||
# ]
|
||||
#
|
||||
# [tool.uv.sources]
|
||||
# my-local-package = { path = "../my-package", editable = true }
|
||||
# ///
|
||||
```
|
||||
|
||||
## Placement Guidelines
|
||||
|
||||
### Correct Placement
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
"""Module docstring comes after metadata"""
|
||||
|
||||
import sys
|
||||
|
||||
def main():
|
||||
pass
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
### Multiple Metadata Blocks (Invalid)
|
||||
|
||||
```python
|
||||
# ❌ INVALID - Only one metadata block allowed
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# ///
|
||||
|
||||
# /// script
|
||||
# dependencies = ["httpx"]
|
||||
# ///
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
### Valid Metadata
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "httpx>=0.27.0", # Comments allowed in arrays
|
||||
# "rich",
|
||||
# ]
|
||||
# ///
|
||||
```
|
||||
|
||||
### Invalid Metadata
|
||||
|
||||
```python
|
||||
# ❌ Missing closing marker
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
|
||||
# ❌ Invalid TOML syntax
|
||||
# /// script
|
||||
# dependencies = httpx # Missing quotes
|
||||
# ///
|
||||
|
||||
# ❌ Not in comments
|
||||
/// script
|
||||
requires-python = ">=3.11"
|
||||
///
|
||||
```
|
||||
|
||||
## Creating Metadata
|
||||
|
||||
### Manual Creation
|
||||
|
||||
Add metadata block manually:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "httpx>=0.27.0",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import httpx
|
||||
```
|
||||
|
||||
### Using uv init
|
||||
|
||||
Generate script with metadata:
|
||||
|
||||
```bash
|
||||
uv init --script my_script.py --python 3.11
|
||||
```
|
||||
|
||||
### Using uv add
|
||||
|
||||
Add dependencies to existing script:
|
||||
|
||||
```bash
|
||||
uv add --script my_script.py httpx rich typer
|
||||
```
|
||||
|
||||
## Validation Tools
|
||||
|
||||
### Check Metadata Validity
|
||||
|
||||
```bash
|
||||
# Parse metadata
|
||||
uv run --script my_script.py --dry-run
|
||||
|
||||
# Validate with custom tool
|
||||
python tools/validate_script.py my_script.py
|
||||
```
|
||||
|
||||
### Extract Metadata
|
||||
|
||||
```python
|
||||
import re
|
||||
import tomllib
|
||||
|
||||
def extract_metadata(script_path: str) -> dict:
|
||||
"""Extract PEP 723 metadata from script"""
|
||||
with open(script_path) as f:
|
||||
content = f.read()
|
||||
|
||||
# Find metadata block
|
||||
pattern = r'# /// script\n((?:# .*\n)+)# ///'
|
||||
match = re.search(pattern, content)
|
||||
|
||||
if not match:
|
||||
return {}
|
||||
|
||||
# Parse TOML (remove leading # from each line)
|
||||
toml_lines = match.group(1).split('\n')
|
||||
toml_content = '\n'.join(line[2:] for line in toml_lines if line.startswith('# '))
|
||||
|
||||
return tomllib.loads(toml_content)
|
||||
```
|
||||
|
||||
## Compatibility
|
||||
|
||||
### PEP 723 Support
|
||||
|
||||
- ✅ uv (native support)
|
||||
- ✅ pip (via `pip-run`)
|
||||
- ✅ pipx (v1.4.0+)
|
||||
- ⚠️ Other tools (check documentation)
|
||||
|
||||
### Fallback for Non-Supporting Tools
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = ["httpx"]
|
||||
# ///
|
||||
"""
|
||||
Fallback installation for non-PEP-723 tools:
|
||||
pip install httpx
|
||||
python script.py
|
||||
"""
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always include requires-python** - Prevents compatibility issues
|
||||
2. **Pin major versions** - `>=X.Y.Z` for stability
|
||||
3. **Add metadata section** - Document purpose and ownership
|
||||
4. **Keep dependencies minimal** - Only what's necessary
|
||||
5. **Document fallbacks** - Help non-uv users
|
||||
6. **Validate syntax** - Use validation tools
|
||||
7. **Version consistently** - Match project conventions
|
||||
|
||||
## References
|
||||
|
||||
- [PEP 723 Specification](https://peps.python.org/pep-0723/)
|
||||
- [uv Documentation](https://docs.astral.sh/uv/)
|
||||
- [TOML Specification](https://toml.io/)
|
||||
- [Python Version Specifiers](https://peps.python.org/pep-0440/)
|
||||
523
skills/python-uv-scripts/reference/security-patterns.md
Normal file
523
skills/python-uv-scripts/reference/security-patterns.md
Normal file
@@ -0,0 +1,523 @@
|
||||
# Security Patterns for uv Scripts
|
||||
|
||||
Security best practices for Python single-file scripts following Virgo-Core patterns.
|
||||
|
||||
## Never Hardcode Secrets
|
||||
|
||||
### ❌ Anti-Pattern: Hardcoded Secrets
|
||||
|
||||
```python
|
||||
# NEVER DO THIS
|
||||
API_KEY = "sk_live_1234567890abcdef"
|
||||
DATABASE_URL = "postgresql://user:password@localhost/db"
|
||||
PROXMOX_PASSWORD = "admin123"
|
||||
```
|
||||
|
||||
**Risks:**
|
||||
|
||||
- Secrets exposed in version control
|
||||
- No audit trail
|
||||
- Difficult to rotate
|
||||
- Same credentials across environments
|
||||
|
||||
### ✅ Pattern 1: Environment Variables
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
def get_secret(name: str, required: bool = True) -> str:
|
||||
"""Get secret from environment"""
|
||||
value = os.getenv(name)
|
||||
|
||||
if required and not value:
|
||||
print(f"Error: {name} environment variable not set", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return value
|
||||
|
||||
# Usage
|
||||
PROXMOX_PASSWORD = get_secret("PROXMOX_PASSWORD")
|
||||
API_URL = get_secret("PROXMOX_API_URL")
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```bash
|
||||
export PROXMOX_PASSWORD="$(cat ~/.secrets/proxmox_pass)"
|
||||
./script.py
|
||||
```
|
||||
|
||||
### ✅ Pattern 2: Keyring Integration
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "keyring>=24.0.0",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import keyring
|
||||
import sys
|
||||
|
||||
def get_password(service: str, username: str) -> str:
|
||||
"""Get password from system keyring"""
|
||||
password = keyring.get_password(service, username)
|
||||
|
||||
if not password:
|
||||
print(f"Error: No password found for {username}@{service}", file=sys.stderr)
|
||||
print(f"Set with: keyring set {service} {username}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return password
|
||||
|
||||
# Usage
|
||||
proxmox_password = get_password("proxmox", "terraform@pam")
|
||||
```
|
||||
|
||||
**Setup:**
|
||||
|
||||
```bash
|
||||
# Store password in system keyring
|
||||
keyring set proxmox terraform@pam
|
||||
# Prompts for password, stores securely
|
||||
|
||||
# Run script (no password in environment)
|
||||
./script.py
|
||||
```
|
||||
|
||||
### ✅ Pattern 3: Infisical Integration (Repository Standard)
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "infisical-python>=2.3.3",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
from infisical import InfisicalClient
|
||||
import os
|
||||
import sys
|
||||
|
||||
def get_infisical_secret(
|
||||
secret_name: str,
|
||||
project_id: str,
|
||||
environment: str = "prod",
|
||||
path: str = "/"
|
||||
) -> str:
|
||||
"""Get secret from Infisical vault"""
|
||||
try:
|
||||
client = InfisicalClient()
|
||||
secret = client.get_secret(
|
||||
secret_name=secret_name,
|
||||
project_id=project_id,
|
||||
environment=environment,
|
||||
path=path
|
||||
)
|
||||
return secret.secret_value
|
||||
except Exception as e:
|
||||
print(f"Error retrieving secret {secret_name}: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Usage (following Virgo-Core pattern)
|
||||
PROXMOX_PASSWORD = get_infisical_secret(
|
||||
secret_name="PROXMOX_PASSWORD",
|
||||
project_id="7b832220-24c0-45bc-a5f1-ce9794a31259",
|
||||
environment="prod",
|
||||
path="/matrix"
|
||||
)
|
||||
```
|
||||
|
||||
**Setup:**
|
||||
|
||||
```bash
|
||||
# Authenticate with Infisical
|
||||
infisical login
|
||||
|
||||
# Run script (secrets fetched automatically)
|
||||
./script.py
|
||||
```
|
||||
|
||||
## Input Validation
|
||||
|
||||
### ❌ Anti-Pattern: No Validation
|
||||
|
||||
```python
|
||||
import subprocess
|
||||
|
||||
# DANGEROUS - Command injection risk
|
||||
def ping_host(hostname: str):
|
||||
subprocess.run(f"ping -c 3 {hostname}", shell=True)
|
||||
|
||||
# User provides: "localhost; rm -rf /"
|
||||
ping_host(sys.argv[1]) # 💥 System destroyed
|
||||
```
|
||||
|
||||
### ✅ Pattern: Validate All Inputs
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
|
||||
import re
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
def validate_hostname(hostname: str) -> bool:
|
||||
"""Validate hostname format"""
|
||||
# Only allow alphanumeric, dots, hyphens
|
||||
pattern = r'^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]$'
|
||||
if not re.match(pattern, hostname):
|
||||
return False
|
||||
|
||||
# Max length check
|
||||
if len(hostname) > 253:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def ping_host(hostname: str):
|
||||
"""Safely ping a host"""
|
||||
if not validate_hostname(hostname):
|
||||
print(f"Error: Invalid hostname: {hostname}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Use list form (no shell injection)
|
||||
result = subprocess.run(
|
||||
["ping", "-c", "3", hostname],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
return result.returncode == 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: ping_host.py <hostname>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
ping_host(sys.argv[1])
|
||||
```
|
||||
|
||||
### Input Validation Patterns
|
||||
|
||||
```python
|
||||
import re
|
||||
from ipaddress import IPv4Address, AddressValueError
|
||||
|
||||
def validate_ip(ip: str) -> bool:
|
||||
"""Validate IPv4 address"""
|
||||
try:
|
||||
IPv4Address(ip)
|
||||
return True
|
||||
except AddressValueError:
|
||||
return False
|
||||
|
||||
def validate_port(port: int) -> bool:
|
||||
"""Validate TCP/UDP port number"""
|
||||
return 1 <= port <= 65535
|
||||
|
||||
def validate_vmid(vmid: int) -> bool:
|
||||
"""Validate Proxmox VMID (100-999999999)"""
|
||||
return 100 <= vmid <= 999999999
|
||||
|
||||
def validate_path(path: str) -> bool:
|
||||
"""Validate file path (no directory traversal)"""
|
||||
# Reject paths with ../
|
||||
if ".." in path:
|
||||
return False
|
||||
|
||||
# Reject absolute paths
|
||||
if path.startswith("/"):
|
||||
return False
|
||||
|
||||
return True
|
||||
```
|
||||
|
||||
## Dependency Security
|
||||
|
||||
### Pin Dependencies
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "httpx>=0.27.0", # ✅ Minimum version pinned
|
||||
# "rich>=13.0.0", # ✅ Known good version
|
||||
# ]
|
||||
# ///
|
||||
```
|
||||
|
||||
**Why pin?**
|
||||
|
||||
- Prevents automatic upgrades with vulnerabilities
|
||||
- Ensures reproducible execution
|
||||
- Allows controlled dependency updates
|
||||
|
||||
### Exclude Recent Packages
|
||||
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = ["httpx>=0.27.0"]
|
||||
#
|
||||
# [tool.uv]
|
||||
# exclude-newer = "2024-10-01T00:00:00Z" # Only packages before this date
|
||||
# ///
|
||||
```
|
||||
|
||||
**Use cases:**
|
||||
|
||||
- Prevent supply chain attacks from compromised packages
|
||||
- Freeze dependencies at known-good state
|
||||
- Reproducible builds in CI/CD
|
||||
|
||||
### Check for Vulnerabilities
|
||||
|
||||
```bash
|
||||
# Use safety or pip-audit
|
||||
uv pip install safety
|
||||
safety check --json
|
||||
|
||||
# Or use built-in tools
|
||||
uv pip list --format json | jq '.[] | select(.name == "httpx")'
|
||||
```
|
||||
|
||||
## File Operations Security
|
||||
|
||||
### ❌ Anti-Pattern: Unsafe File Access
|
||||
|
||||
```python
|
||||
# DANGEROUS - Path traversal vulnerability
|
||||
def read_log(filename: str):
|
||||
with open(f"/var/log/{filename}") as f:
|
||||
return f.read()
|
||||
|
||||
# User provides: "../../../etc/passwd"
|
||||
read_log(sys.argv[1]) # 💥 Reads /etc/passwd
|
||||
```
|
||||
|
||||
### ✅ Pattern: Safe File Operations
|
||||
|
||||
```python
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
def safe_read_log(filename: str, log_dir: str = "/var/log") -> str:
|
||||
"""Safely read log file"""
|
||||
# Resolve to absolute paths
|
||||
log_dir_path = Path(log_dir).resolve()
|
||||
file_path = (log_dir_path / filename).resolve()
|
||||
|
||||
# Ensure file is within log directory
|
||||
try:
|
||||
file_path.relative_to(log_dir_path)
|
||||
except ValueError:
|
||||
print(f"Error: Path traversal detected: {filename}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Check file exists and is readable
|
||||
if not file_path.exists():
|
||||
print(f"Error: File not found: {filename}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if not file_path.is_file():
|
||||
print(f"Error: Not a file: {filename}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Read with size limit
|
||||
MAX_SIZE = 10 * 1024 * 1024 # 10MB
|
||||
if file_path.stat().st_size > MAX_SIZE:
|
||||
print(f"Error: File too large: {filename}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
with open(file_path) as f:
|
||||
return f.read()
|
||||
```
|
||||
|
||||
## Command Execution Security
|
||||
|
||||
### ❌ Anti-Pattern: Shell Injection
|
||||
|
||||
```python
|
||||
# DANGEROUS
|
||||
import subprocess
|
||||
|
||||
def list_directory(path: str):
|
||||
subprocess.run(f"ls -la {path}", shell=True)
|
||||
|
||||
# User provides: "; rm -rf /"
|
||||
list_directory(sys.argv[1]) # 💥 Disaster
|
||||
```
|
||||
|
||||
### ✅ Pattern: Safe Command Execution
|
||||
|
||||
```python
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
def list_directory(path: str):
|
||||
"""Safely list directory contents"""
|
||||
# Validate path first
|
||||
if not validate_path(path):
|
||||
print(f"Error: Invalid path: {path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Use list form (no shell)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["ls", "-la", path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
timeout=5 # Prevent hanging
|
||||
)
|
||||
print(result.stdout)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error: {e.stderr}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except subprocess.TimeoutExpired:
|
||||
print("Error: Command timed out", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
```
|
||||
|
||||
## Logging and Audit
|
||||
|
||||
### Secure Logging
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "structlog>=24.0.0",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import structlog
|
||||
import sys
|
||||
|
||||
# Configure structured logging
|
||||
structlog.configure(
|
||||
processors=[
|
||||
structlog.processors.add_log_level,
|
||||
structlog.processors.TimeStamper(fmt="iso"),
|
||||
structlog.processors.JSONRenderer(),
|
||||
],
|
||||
logger_factory=structlog.PrintLoggerFactory(file=sys.stderr),
|
||||
)
|
||||
|
||||
log = structlog.get_logger()
|
||||
|
||||
def process_data(user_id: int, action: str):
|
||||
"""Process user action with audit logging"""
|
||||
log.info(
|
||||
"user_action",
|
||||
user_id=user_id,
|
||||
action=action,
|
||||
# Don't log sensitive data!
|
||||
)
|
||||
|
||||
# ... processing logic ...
|
||||
|
||||
log.info(
|
||||
"action_completed",
|
||||
user_id=user_id,
|
||||
action=action,
|
||||
status="success"
|
||||
)
|
||||
```
|
||||
|
||||
### Never Log Secrets
|
||||
|
||||
```python
|
||||
# ❌ BAD - Logs password
|
||||
log.info(f"Connecting with password: {password}")
|
||||
|
||||
# ✅ GOOD - No sensitive data
|
||||
log.info("Connecting to API", endpoint=api_url)
|
||||
|
||||
# ✅ GOOD - Masked credentials
|
||||
log.info("Authentication successful", user=username)
|
||||
```
|
||||
|
||||
## Network Security
|
||||
|
||||
### HTTPS Only
|
||||
|
||||
```python
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# dependencies = ["httpx>=0.27.0"]
|
||||
# ///
|
||||
|
||||
import httpx
|
||||
import sys
|
||||
|
||||
def fetch_api(url: str) -> dict:
|
||||
"""Fetch data from API (HTTPS only)"""
|
||||
# Validate HTTPS
|
||||
if not url.startswith("https://"):
|
||||
print("Error: Only HTTPS URLs allowed", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
with httpx.Client(verify=True) as client: # Verify SSL certs
|
||||
response = client.get(url, timeout=10.0)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
```
|
||||
|
||||
### Certificate Verification
|
||||
|
||||
```python
|
||||
import httpx
|
||||
import certifi
|
||||
|
||||
# Use trusted CA bundle
|
||||
client = httpx.Client(verify=certifi.where())
|
||||
|
||||
# For internal CAs, specify cert path
|
||||
client = httpx.Client(verify="/path/to/internal-ca.pem")
|
||||
|
||||
# ONLY disable for development (never in production)
|
||||
if os.getenv("DEVELOPMENT") == "true":
|
||||
client = httpx.Client(verify=False)
|
||||
```
|
||||
|
||||
## Security Checklist
|
||||
|
||||
Before deploying a script:
|
||||
|
||||
- [ ] No hardcoded secrets
|
||||
- [ ] Secrets from environment/keyring/Infisical
|
||||
- [ ] All inputs validated
|
||||
- [ ] No shell=True in subprocess
|
||||
- [ ] File paths checked for traversal
|
||||
- [ ] Dependencies pinned
|
||||
- [ ] No sensitive data in logs
|
||||
- [ ] HTTPS for network requests
|
||||
- [ ] Certificate verification enabled
|
||||
- [ ] Timeouts on external calls
|
||||
- [ ] Error messages don't leak information
|
||||
- [ ] Principle of least privilege applied
|
||||
|
||||
## References
|
||||
|
||||
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
|
||||
- [Infisical Python SDK](https://infisical.com/docs/sdks/languages/python)
|
||||
- [Python Security Best Practices](https://python.readthedocs.io/en/latest/library/security_warnings.html)
|
||||
- Virgo-Core Infisical integration: [../../ansible/tasks/infisical-secret-lookup.yml](../../ansible/tasks/infisical-secret-lookup.yml)
|
||||
Reference in New Issue
Block a user