start-vibing-stacks 2.6.0 → 2.7.0
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.
- package/dist/index.js +16 -2
- package/dist/migrate.d.ts +27 -0
- package/dist/migrate.js +217 -0
- package/dist/setup.js +10 -0
- package/package.json +1 -1
- package/stacks/_shared/agents/claude-md-compactor.md +1 -0
- package/stacks/_shared/agents/commit-manager.md +1 -0
- package/stacks/_shared/agents/documenter.md +1 -0
- package/stacks/_shared/agents/domain-updater.md +1 -0
- package/stacks/_shared/agents/research-web.md +1 -0
- package/stacks/_shared/agents/security-auditor.md +168 -0
- package/stacks/_shared/agents/tester.md +1 -0
- package/stacks/_shared/hooks/final-check.ts +205 -0
- package/stacks/_shared/hooks/stop-validator.ts +77 -1
- package/stacks/_shared/skills/accessibility-wcag22/SKILL.md +284 -0
- package/stacks/_shared/skills/ci-pipelines/SKILL.md +166 -0
- package/stacks/_shared/skills/codebase-knowledge/SKILL.md +5 -0
- package/stacks/_shared/skills/database-migrations/SKILL.md +256 -0
- package/stacks/_shared/skills/debugging-patterns/SKILL.md +5 -0
- package/stacks/_shared/skills/docker-patterns/SKILL.md +5 -0
- package/stacks/_shared/skills/docs-tracker/SKILL.md +5 -0
- package/stacks/_shared/skills/error-handling/SKILL.md +335 -0
- package/stacks/_shared/skills/final-check/SKILL.md +74 -37
- package/stacks/_shared/skills/git-workflow/SKILL.md +5 -0
- package/stacks/_shared/skills/hook-development/SKILL.md +5 -0
- package/stacks/_shared/skills/observability/SKILL.md +351 -0
- package/stacks/_shared/skills/performance-patterns/SKILL.md +5 -0
- package/stacks/_shared/skills/playwright-automation/SKILL.md +5 -0
- package/stacks/_shared/skills/quality-gate/SKILL.md +5 -0
- package/stacks/_shared/skills/research-cache/SKILL.md +5 -0
- package/stacks/_shared/skills/secrets-management/SKILL.md +245 -0
- package/stacks/_shared/skills/security-baseline/SKILL.md +202 -0
- package/stacks/_shared/skills/test-coverage/SKILL.md +5 -0
- package/stacks/_shared/skills/ui-ux-audit/SKILL.md +5 -0
- package/stacks/frontend/react/skills/preline-ui/SKILL.md +5 -0
- package/stacks/frontend/react/skills/react-patterns/SKILL.md +5 -0
- package/stacks/frontend/react/skills/react-standards/SKILL.md +5 -0
- package/stacks/frontend/react/skills/react-ui-patterns/SKILL.md +5 -0
- package/stacks/frontend/react/skills/shadcn-ui/SKILL.md +5 -0
- package/stacks/frontend/react/skills/tailwind-patterns/SKILL.md +5 -0
- package/stacks/frontend/react/skills/zod-validation/SKILL.md +5 -0
- package/stacks/frontend/react-inertia/skills/inertia-react/SKILL.md +5 -0
- package/stacks/frontend/react-inertia/skills/react-standards/SKILL.md +5 -0
- package/stacks/nodejs/skills/api-security-node/SKILL.md +275 -0
- package/stacks/nodejs/skills/bun-runtime/SKILL.md +5 -0
- package/stacks/nodejs/skills/mongoose-patterns/SKILL.md +5 -0
- package/stacks/nodejs/skills/nextjs-app-router/SKILL.md +5 -0
- package/stacks/nodejs/skills/trpc-api/SKILL.md +5 -0
- package/stacks/nodejs/skills/typescript-strict/SKILL.md +5 -0
- package/stacks/nodejs/stack.json +2 -1
- package/stacks/nodejs/workflows/ci.yml +90 -0
- package/stacks/nodejs/workflows/security.yml +45 -0
- package/stacks/php/skills/api-design/SKILL.md +5 -0
- package/stacks/php/skills/api-security/SKILL.md +5 -0
- package/stacks/php/skills/composer-workflow/SKILL.md +5 -0
- package/stacks/php/skills/external-api-patterns/SKILL.md +5 -0
- package/stacks/php/skills/inertia-react/SKILL.md +5 -0
- package/stacks/php/skills/laravel-inertia-i18n/SKILL.md +5 -0
- package/stacks/php/skills/laravel-octane/SKILL.md +5 -0
- package/stacks/php/skills/laravel-patterns/SKILL.md +5 -0
- package/stacks/php/skills/mariadb-octane/SKILL.md +5 -0
- package/stacks/php/skills/php-patterns/SKILL.md +5 -0
- package/stacks/php/skills/phpstan-analysis/SKILL.md +5 -0
- package/stacks/php/skills/phpunit-testing/SKILL.md +5 -0
- package/stacks/php/skills/security-scan-php/SKILL.md +5 -0
- package/stacks/php/workflows/ci.yml +106 -0
- package/stacks/php/workflows/security.yml +36 -0
- package/stacks/python/skills/api-security-python/SKILL.md +312 -0
- package/stacks/python/skills/async-patterns/SKILL.md +5 -0
- package/stacks/python/skills/django-patterns/SKILL.md +5 -0
- package/stacks/python/skills/fastapi-patterns/SKILL.md +5 -0
- package/stacks/python/skills/pydantic-validation/SKILL.md +5 -0
- package/stacks/python/skills/pytest-testing/SKILL.md +5 -0
- package/stacks/python/skills/python-patterns/SKILL.md +5 -0
- package/stacks/python/skills/python-performance/SKILL.md +5 -0
- package/stacks/python/skills/scripting-automation/SKILL.md +5 -0
- package/stacks/python/stack.json +2 -1
- package/stacks/python/workflows/ci.yml +76 -0
- package/stacks/python/workflows/security.yml +56 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: api-security-python
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
description: Production-grade API hardening for Python (FastAPI, Django, Flask). Rate limit, CORS, JWT, secure cookies, CSRF, OAuth2.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# API Security — Python
|
|
8
|
+
|
|
9
|
+
**ALWAYS invoke when building API endpoints, auth flows, or admin actions.**
|
|
10
|
+
|
|
11
|
+
> Pair this with `security-baseline` for OWASP Top 10. This skill is stack-specific hardening.
|
|
12
|
+
|
|
13
|
+
## Layered Defense
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
Edge (CDN/WAF) → Rate Limit → CORS → Headers → Auth → Authz → Validate → Logic → Encode → Audit
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 1. Security Headers
|
|
22
|
+
|
|
23
|
+
### FastAPI — middleware
|
|
24
|
+
```python
|
|
25
|
+
from fastapi import FastAPI
|
|
26
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
27
|
+
|
|
28
|
+
class SecurityHeaders(BaseHTTPMiddleware):
|
|
29
|
+
async def dispatch(self, request, call_next):
|
|
30
|
+
response = await call_next(request)
|
|
31
|
+
response.headers["Strict-Transport-Security"] = "max-age=63072000; includeSubDomains; preload"
|
|
32
|
+
response.headers["X-Content-Type-Options"] = "nosniff"
|
|
33
|
+
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
|
|
34
|
+
response.headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()"
|
|
35
|
+
response.headers["X-Frame-Options"] = "DENY"
|
|
36
|
+
response.headers["Content-Security-Policy"] = (
|
|
37
|
+
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; "
|
|
38
|
+
"img-src 'self' data: https:; frame-ancestors 'none'"
|
|
39
|
+
)
|
|
40
|
+
return response
|
|
41
|
+
|
|
42
|
+
app = FastAPI()
|
|
43
|
+
app.add_middleware(SecurityHeaders)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Django — `settings.py`
|
|
47
|
+
```python
|
|
48
|
+
SECURE_HSTS_SECONDS = 63072000
|
|
49
|
+
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
|
50
|
+
SECURE_HSTS_PRELOAD = True
|
|
51
|
+
SECURE_CONTENT_TYPE_NOSNIFF = True
|
|
52
|
+
SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"
|
|
53
|
+
SECURE_SSL_REDIRECT = True
|
|
54
|
+
SESSION_COOKIE_SECURE = True
|
|
55
|
+
SESSION_COOKIE_HTTPONLY = True
|
|
56
|
+
SESSION_COOKIE_SAMESITE = "Lax"
|
|
57
|
+
CSRF_COOKIE_SECURE = True
|
|
58
|
+
CSRF_COOKIE_HTTPONLY = True
|
|
59
|
+
X_FRAME_OPTIONS = "DENY"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 2. CORS — Strict Allowlist
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
68
|
+
import os
|
|
69
|
+
|
|
70
|
+
ALLOW = [o.strip() for o in os.getenv("CORS_ORIGINS", "").split(",") if o]
|
|
71
|
+
|
|
72
|
+
app.add_middleware(
|
|
73
|
+
CORSMiddleware,
|
|
74
|
+
allow_origins=ALLOW, # explicit list, NEVER ["*"] with credentials
|
|
75
|
+
allow_credentials=True,
|
|
76
|
+
allow_methods=["GET", "POST", "PATCH", "DELETE"],
|
|
77
|
+
allow_headers=["Authorization", "Content-Type", "X-CSRF-Token"],
|
|
78
|
+
max_age=600,
|
|
79
|
+
)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Never** `allow_origins=["*"]` with `allow_credentials=True` — browsers reject and auth silently breaks.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 3. Rate Limiting
|
|
87
|
+
|
|
88
|
+
### FastAPI — `slowapi`
|
|
89
|
+
```python
|
|
90
|
+
from slowapi import Limiter, _rate_limit_exceeded_handler
|
|
91
|
+
from slowapi.util import get_remote_address
|
|
92
|
+
from slowapi.errors import RateLimitExceeded
|
|
93
|
+
|
|
94
|
+
limiter = Limiter(key_func=get_remote_address, storage_uri="redis://localhost:6379")
|
|
95
|
+
app.state.limiter = limiter
|
|
96
|
+
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
|
97
|
+
|
|
98
|
+
@app.post("/auth/login")
|
|
99
|
+
@limiter.limit("5/15minutes")
|
|
100
|
+
async def login(request: Request, body: LoginIn):
|
|
101
|
+
...
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Django — `django-ratelimit`
|
|
105
|
+
```python
|
|
106
|
+
from django_ratelimit.decorators import ratelimit
|
|
107
|
+
|
|
108
|
+
@ratelimit(key='ip', rate='5/15m', block=True)
|
|
109
|
+
def login_view(request):
|
|
110
|
+
...
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Limits to set:** auth (5/15min), password reset (3/hour), signup (3/hour/IP), generic write (60/min/user).
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 4. Cookies
|
|
118
|
+
|
|
119
|
+
### FastAPI
|
|
120
|
+
```python
|
|
121
|
+
from fastapi import Response
|
|
122
|
+
|
|
123
|
+
response.set_cookie(
|
|
124
|
+
key="session",
|
|
125
|
+
value=token,
|
|
126
|
+
httponly=True, # JS cannot read
|
|
127
|
+
secure=True, # HTTPS only
|
|
128
|
+
samesite="lax", # 'strict' if no cross-site flows
|
|
129
|
+
max_age=60 * 60 * 24 * 7, # 7d
|
|
130
|
+
path="/",
|
|
131
|
+
domain=os.getenv("COOKIE_DOMAIN"),
|
|
132
|
+
)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## 5. JWT / OAuth2 — FastAPI
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from datetime import datetime, timedelta, timezone
|
|
141
|
+
from jose import jwt, JWTError
|
|
142
|
+
import os, uuid
|
|
143
|
+
|
|
144
|
+
SECRET = os.environ["JWT_SECRET"]
|
|
145
|
+
ALG = "HS256"
|
|
146
|
+
|
|
147
|
+
def issue_access_token(user_id: str, role: str) -> str:
|
|
148
|
+
now = datetime.now(timezone.utc)
|
|
149
|
+
return jwt.encode(
|
|
150
|
+
{
|
|
151
|
+
"sub": user_id,
|
|
152
|
+
"role": role,
|
|
153
|
+
"iat": now,
|
|
154
|
+
"exp": now + timedelta(minutes=15),
|
|
155
|
+
"jti": str(uuid.uuid4()),
|
|
156
|
+
},
|
|
157
|
+
SECRET,
|
|
158
|
+
algorithm=ALG,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
async def current_user(token: str = Depends(oauth2_scheme)) -> User:
|
|
162
|
+
try:
|
|
163
|
+
payload = jwt.decode(token, SECRET, algorithms=[ALG]) # pin algorithm
|
|
164
|
+
except JWTError:
|
|
165
|
+
raise HTTPException(401, "Invalid token")
|
|
166
|
+
user = await User.get_or_none(id=payload["sub"])
|
|
167
|
+
if not user:
|
|
168
|
+
raise HTTPException(401, "User not found")
|
|
169
|
+
return user
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Rules:
|
|
173
|
+
- Access tokens: ≤ 15 min. Refresh tokens: rotate on use, store hash in DB, revocable.
|
|
174
|
+
- Pin `algorithms=[ALG]`. Never accept `alg: none`.
|
|
175
|
+
- Include `jti` for revocation lists.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## 6. CSRF
|
|
180
|
+
|
|
181
|
+
### Django
|
|
182
|
+
Built-in: `django.middleware.csrf.CsrfViewMiddleware`. Always enabled — never disable globally.
|
|
183
|
+
For DRF + SessionAuth, use `@ensure_csrf_cookie` on the view that bootstraps the SPA.
|
|
184
|
+
|
|
185
|
+
### FastAPI — double-submit cookie
|
|
186
|
+
```python
|
|
187
|
+
import secrets
|
|
188
|
+
from fastapi import Request, HTTPException
|
|
189
|
+
|
|
190
|
+
CSRF_COOKIE = "csrf-token"
|
|
191
|
+
CSRF_HEADER = "x-csrf-token"
|
|
192
|
+
UNSAFE = {"POST", "PUT", "PATCH", "DELETE"}
|
|
193
|
+
|
|
194
|
+
@app.middleware("http")
|
|
195
|
+
async def csrf(request: Request, call_next):
|
|
196
|
+
if request.method in UNSAFE:
|
|
197
|
+
cookie = request.cookies.get(CSRF_COOKIE)
|
|
198
|
+
header = request.headers.get(CSRF_HEADER)
|
|
199
|
+
if not cookie or cookie != header:
|
|
200
|
+
raise HTTPException(403, "CSRF")
|
|
201
|
+
response = await call_next(request)
|
|
202
|
+
if not request.cookies.get(CSRF_COOKIE):
|
|
203
|
+
response.set_cookie(CSRF_COOKIE, secrets.token_urlsafe(32),
|
|
204
|
+
secure=True, samesite="lax", path="/")
|
|
205
|
+
return response
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## 7. Input Validation Boundary (Pydantic)
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
from pydantic import BaseModel, EmailStr, Field, ConfigDict
|
|
214
|
+
|
|
215
|
+
class CreateUser(BaseModel):
|
|
216
|
+
model_config = ConfigDict(extra="forbid") # rejects unknown keys → blocks mass assignment
|
|
217
|
+
email: EmailStr = Field(max_length=254)
|
|
218
|
+
age: int = Field(ge=13, le=120)
|
|
219
|
+
|
|
220
|
+
@app.post("/users")
|
|
221
|
+
async def create(body: CreateUser): # FastAPI validates automatically; 422 on failure
|
|
222
|
+
...
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## 8. File Upload
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
import magic # python-magic — reads magic bytes
|
|
231
|
+
ALLOWED = {"image/jpeg", "image/png", "image/webp"}
|
|
232
|
+
|
|
233
|
+
async def upload(file: UploadFile = File(...)):
|
|
234
|
+
if file.size and file.size > 10 * 1024 * 1024:
|
|
235
|
+
raise HTTPException(413, "Too large")
|
|
236
|
+
head = await file.read(2048)
|
|
237
|
+
mime = magic.from_buffer(head, mime=True)
|
|
238
|
+
if mime not in ALLOWED:
|
|
239
|
+
raise HTTPException(415, "Unsupported type")
|
|
240
|
+
await file.seek(0)
|
|
241
|
+
# Save with UUID name, outside webroot
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## 9. Password Hashing
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
from argon2 import PasswordHasher
|
|
250
|
+
ph = PasswordHasher(memory_cost=19_456, time_cost=2, parallelism=1)
|
|
251
|
+
|
|
252
|
+
hashed = ph.hash(password)
|
|
253
|
+
try:
|
|
254
|
+
ph.verify(hashed, attempt)
|
|
255
|
+
except argon2.exceptions.VerifyMismatchError:
|
|
256
|
+
raise HTTPException(401, "Invalid credentials")
|
|
257
|
+
|
|
258
|
+
if ph.check_needs_rehash(hashed): # transparent upgrade
|
|
259
|
+
user.password = ph.hash(attempt)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Argon2id is the modern default. Avoid bare `hashlib`. For Django, the framework handles this — don't reinvent it.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## 10. SQL Injection — Use ORM Bindings
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
# WRONG — string interpolation
|
|
270
|
+
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
|
|
271
|
+
|
|
272
|
+
# CORRECT — parameterized
|
|
273
|
+
cursor.execute("SELECT * FROM users WHERE id = %s", [user_id])
|
|
274
|
+
|
|
275
|
+
# CORRECT — ORM
|
|
276
|
+
user = await User.get(id=user_id) # Tortoise / SQLAlchemy / Django ORM
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
`SQLAlchemy.text()` with `:param` bindings is also safe. Raw f-strings are not.
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## Endpoint Checklist
|
|
284
|
+
|
|
285
|
+
- [ ] `Depends(current_user)` for protected routes
|
|
286
|
+
- [ ] User ID from `current_user`, **never** from body
|
|
287
|
+
- [ ] Pydantic model with `extra="forbid"`
|
|
288
|
+
- [ ] Authz check on the resource (object-level)
|
|
289
|
+
- [ ] Rate limit applied to auth + writes
|
|
290
|
+
- [ ] No PII in logs
|
|
291
|
+
- [ ] Errors: `HTTPException` with generic detail; full info to logs only
|
|
292
|
+
|
|
293
|
+
## FORBIDDEN Patterns
|
|
294
|
+
|
|
295
|
+
| Anti-pattern | Reason |
|
|
296
|
+
|---|---|
|
|
297
|
+
| `allow_origins=["*"]` with credentials | Browsers reject; auth breaks |
|
|
298
|
+
| Calling `jwt.decode` without `algorithms=` | `alg: none` and confusion attacks |
|
|
299
|
+
| Storing JWT in `localStorage` | XSS exfiltration |
|
|
300
|
+
| Disabling Django `CsrfViewMiddleware` globally | CSRF wide open |
|
|
301
|
+
| f-string interpolation in SQL | SQL injection |
|
|
302
|
+
| Deserializing untrusted bytes (pickle, marshal, shelve) | RCE via gadget chains — use JSON |
|
|
303
|
+
| `eval` / `exec` on user input | RCE |
|
|
304
|
+
| Logging request body or headers raw | Leaks passwords, cookies, tokens |
|
|
305
|
+
| Shell-mode subprocess with user input | Command injection — use list args, no shell |
|
|
306
|
+
|
|
307
|
+
## See Also
|
|
308
|
+
|
|
309
|
+
- `security-baseline` — OWASP Top 10
|
|
310
|
+
- `secrets-management` — env vars, rotation
|
|
311
|
+
- `pydantic-validation` — schema patterns
|
|
312
|
+
- `observability` — structured logs without PII
|
package/stacks/python/stack.json
CHANGED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches: [main]
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
10
|
+
cancel-in-progress: true
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
ci:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
timeout-minutes: 15
|
|
19
|
+
strategy:
|
|
20
|
+
matrix:
|
|
21
|
+
python: ['3.12']
|
|
22
|
+
fail-fast: false
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
|
|
26
|
+
- uses: actions/setup-python@v5
|
|
27
|
+
with:
|
|
28
|
+
python-version: ${{ matrix.python }}
|
|
29
|
+
cache: pip
|
|
30
|
+
|
|
31
|
+
- name: Install uv
|
|
32
|
+
run: curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
33
|
+
|
|
34
|
+
- name: Install dependencies
|
|
35
|
+
run: uv sync --frozen
|
|
36
|
+
|
|
37
|
+
- name: Typecheck (mypy)
|
|
38
|
+
run: uv run mypy .
|
|
39
|
+
|
|
40
|
+
- name: Lint (ruff)
|
|
41
|
+
run: uv run ruff check .
|
|
42
|
+
|
|
43
|
+
- name: Format check (ruff)
|
|
44
|
+
run: uv run ruff format --check .
|
|
45
|
+
|
|
46
|
+
- name: Tests (pytest)
|
|
47
|
+
run: uv run pytest --tb=short --cov --cov-report=xml
|
|
48
|
+
|
|
49
|
+
- uses: codecov/codecov-action@v4
|
|
50
|
+
if: always()
|
|
51
|
+
with:
|
|
52
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
53
|
+
fail_ci_if_error: false
|
|
54
|
+
|
|
55
|
+
security:
|
|
56
|
+
runs-on: ubuntu-latest
|
|
57
|
+
timeout-minutes: 10
|
|
58
|
+
steps:
|
|
59
|
+
- uses: actions/checkout@v4
|
|
60
|
+
with:
|
|
61
|
+
fetch-depth: 0
|
|
62
|
+
|
|
63
|
+
- uses: actions/setup-python@v5
|
|
64
|
+
with:
|
|
65
|
+
python-version: '3.12'
|
|
66
|
+
|
|
67
|
+
- name: Install pip-audit
|
|
68
|
+
run: pip install pip-audit
|
|
69
|
+
|
|
70
|
+
- name: Audit dependencies
|
|
71
|
+
run: pip-audit --strict --vulnerability-service osv
|
|
72
|
+
|
|
73
|
+
- name: Gitleaks (secret scan)
|
|
74
|
+
uses: gitleaks/gitleaks-action@v2
|
|
75
|
+
env:
|
|
76
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
name: Security
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
- cron: '0 4 * * 1'
|
|
6
|
+
push:
|
|
7
|
+
branches: [main]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
security-events: write
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
audit:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
timeout-minutes: 10
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
- uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: '3.12'
|
|
23
|
+
- run: pip install pip-audit
|
|
24
|
+
- run: pip-audit --strict
|
|
25
|
+
|
|
26
|
+
bandit:
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
timeout-minutes: 10
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/checkout@v4
|
|
31
|
+
- uses: actions/setup-python@v5
|
|
32
|
+
with:
|
|
33
|
+
python-version: '3.12'
|
|
34
|
+
- run: pip install bandit[toml]
|
|
35
|
+
- run: bandit -r . -ll -ii --exclude tests,test
|
|
36
|
+
|
|
37
|
+
gitleaks:
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
timeout-minutes: 5
|
|
40
|
+
steps:
|
|
41
|
+
- uses: actions/checkout@v4
|
|
42
|
+
with:
|
|
43
|
+
fetch-depth: 0
|
|
44
|
+
- uses: gitleaks/gitleaks-action@v2
|
|
45
|
+
env:
|
|
46
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
47
|
+
|
|
48
|
+
codeql:
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
timeout-minutes: 20
|
|
51
|
+
steps:
|
|
52
|
+
- uses: actions/checkout@v4
|
|
53
|
+
- uses: github/codeql-action/init@v3
|
|
54
|
+
with:
|
|
55
|
+
languages: python
|
|
56
|
+
- uses: github/codeql-action/analyze@v3
|