cc-codeconductor 0.2.1
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/LICENSE +18 -0
- package/README.md +319 -0
- package/dist/index.js +12741 -0
- package/package.json +51 -0
- package/policy.yml +124 -0
- package/presets/claude/CLAUDE.md +598 -0
- package/presets/claude/commands/cc/feature.md +115 -0
- package/presets/claude/commands/cc/fix.md +121 -0
- package/presets/claude/commands/cc/refactor.md +148 -0
- package/presets/claude/commands/cc/review.md +126 -0
- package/presets/claude/commands/cc/tdd-cycle.md +226 -0
- package/presets/claude/commands/cc/test-plan.md +138 -0
- package/presets/claude/settings.json +37 -0
- package/presets/claude/skills/api-versioning/SKILL.md +389 -0
- package/presets/claude/skills/django-orm/SKILL.md +455 -0
- package/presets/claude/skills/django-testing/SKILL.md +409 -0
- package/presets/claude/skills/jpa-postgres/SKILL.md +618 -0
- package/presets/claude/skills/python/SKILL.md +606 -0
- package/presets/claude/skills/python-django-stack/SKILL.md +492 -0
- package/presets/claude/skills/python-fastapi-stack/SKILL.md +457 -0
- package/presets/claude/skills/spring-boot-feature/SKILL.md +558 -0
- package/presets/claude/skills/spring-boot-kotlin/SKILL.md +400 -0
- package/presets/claude/skills/sqlalchemy/SKILL.md +466 -0
- package/presets/claude/skills/testing-strategy/SKILL.md +479 -0
- package/presets/codex/AGENTS.md +883 -0
- package/presets/codex/README.md +102 -0
- package/presets/codex/skills/api-versioning/SKILL.md +389 -0
- package/presets/codex/skills/django-orm/SKILL.md +455 -0
- package/presets/codex/skills/django-testing/SKILL.md +409 -0
- package/presets/codex/skills/jpa-postgres/SKILL.md +618 -0
- package/presets/codex/skills/python/SKILL.md +606 -0
- package/presets/codex/skills/python-django-stack/SKILL.md +492 -0
- package/presets/codex/skills/python-fastapi-stack/SKILL.md +457 -0
- package/presets/codex/skills/spring-boot-feature/SKILL.md +558 -0
- package/presets/codex/skills/spring-boot-kotlin/SKILL.md +400 -0
- package/presets/codex/skills/sqlalchemy/SKILL.md +466 -0
- package/presets/codex/skills/testing-strategy/SKILL.md +479 -0
- package/presets/opencode/README.md +191 -0
- package/presets/opencode/agents/architect.md +133 -0
- package/presets/opencode/agents/docs.md +113 -0
- package/presets/opencode/agents/implementer.md +116 -0
- package/presets/opencode/agents/orchestrator.md +248 -0
- package/presets/opencode/agents/repo-explorer.md +119 -0
- package/presets/opencode/agents/reviewer.md +130 -0
- package/presets/opencode/agents/task-coach.md +103 -0
- package/presets/opencode/agents/tester.md +269 -0
- package/presets/opencode/commands/cc-feature.md +114 -0
- package/presets/opencode/commands/cc-fix.md +123 -0
- package/presets/opencode/commands/cc-refactor.md +148 -0
- package/presets/opencode/commands/cc-review.md +141 -0
- package/presets/opencode/commands/cc-tdd-cycle.md +225 -0
- package/presets/opencode/commands/cc-test-plan.md +144 -0
- package/presets/opencode/opencode.jsonc +89 -0
- package/presets/opencode/prompts/v0.1.0/architect.md +213 -0
- package/presets/opencode/prompts/v0.1.0/docs.md +181 -0
- package/presets/opencode/prompts/v0.1.0/implementer.md +154 -0
- package/presets/opencode/prompts/v0.1.0/orchestrator.md +169 -0
- package/presets/opencode/prompts/v0.1.0/repo-explorer.md +102 -0
- package/presets/opencode/prompts/v0.1.0/reviewer.md +183 -0
- package/presets/opencode/prompts/v0.1.0/task-coach.md +142 -0
- package/presets/opencode/prompts/v0.1.0/tester.md +160 -0
- package/presets/opencode/prompts/v0.2.0/architect.md +219 -0
- package/presets/opencode/prompts/v0.2.0/docs.md +187 -0
- package/presets/opencode/prompts/v0.2.0/implementer.md +160 -0
- package/presets/opencode/prompts/v0.2.0/orchestrator.md +238 -0
- package/presets/opencode/prompts/v0.2.0/repo-explorer.md +108 -0
- package/presets/opencode/prompts/v0.2.0/reviewer.md +190 -0
- package/presets/opencode/prompts/v0.2.0/task-coach.md +153 -0
- package/presets/opencode/prompts/v0.2.0/tester.md +249 -0
- package/presets/opencode/skills/api-versioning/SKILL.md +388 -0
- package/presets/opencode/skills/astro/SKILL.md +313 -0
- package/presets/opencode/skills/code-review/SKILL.md +202 -0
- package/presets/opencode/skills/django-orm/SKILL.md +455 -0
- package/presets/opencode/skills/django-testing/SKILL.md +408 -0
- package/presets/opencode/skills/django-uv/SKILL.md +400 -0
- package/presets/opencode/skills/jpa-postgres/SKILL.md +617 -0
- package/presets/opencode/skills/nextjs-typescript/SKILL.md +385 -0
- package/presets/opencode/skills/python/SKILL.md +605 -0
- package/presets/opencode/skills/python-django-stack/SKILL.md +491 -0
- package/presets/opencode/skills/python-fastapi-stack/SKILL.md +456 -0
- package/presets/opencode/skills/security/SKILL.md +376 -0
- package/presets/opencode/skills/spring-boot-feature/SKILL.md +557 -0
- package/presets/opencode/skills/spring-boot-kotlin/SKILL.md +399 -0
- package/presets/opencode/skills/spring-boot-testing-strategy/SKILL.md +470 -0
- package/presets/opencode/skills/sqlalchemy/SKILL.md +465 -0
- package/presets/opencode/skills/testing-tdd/SKILL.md +586 -0
- package/src/presets/council/council.yml +59 -0
- package/src/presets/manifests/claude.yml +23 -0
- package/src/presets/manifests/codex.yml +12 -0
- package/src/presets/manifests/opencode.yml +18 -0
- package/src/presets/models/claude.yml +37 -0
- package/src/presets/models/codex.yml +38 -0
- package/src/presets/models/opencode.yml +37 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: security
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
name: Security
|
|
5
|
+
description: >
|
|
6
|
+
Provides expert knowledge for implementing and reviewing application security following OWASP Top 10 (2021), with stack-specific patterns for Spring Boot, Django, Next.js, and Astro.
|
|
7
|
+
|
|
8
|
+
compatibility:
|
|
9
|
+
tools: [claude, codex, opencode]
|
|
10
|
+
stacks:
|
|
11
|
+
languages: []
|
|
12
|
+
frameworks: []
|
|
13
|
+
|
|
14
|
+
risk:
|
|
15
|
+
level: high
|
|
16
|
+
can_execute_shell: false
|
|
17
|
+
can_modify_files: true
|
|
18
|
+
requires_network: false
|
|
19
|
+
|
|
20
|
+
inputs: []
|
|
21
|
+
|
|
22
|
+
outputs: []
|
|
23
|
+
|
|
24
|
+
quality:
|
|
25
|
+
reviewed_by: codeconductor-core
|
|
26
|
+
version: 0.1.0
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Security
|
|
32
|
+
|
|
33
|
+
## OWASP Top 10 (2021) — Quick Reference
|
|
34
|
+
|
|
35
|
+
| # | Category | What to check |
|
|
36
|
+
|---|----------|--------------|
|
|
37
|
+
| A01 | Broken Access Control | Is authorization enforced on every endpoint? |
|
|
38
|
+
| A02 | Cryptographic Failures | Are secrets encrypted at rest and in transit? |
|
|
39
|
+
| A03 | Injection | Is all input sanitized or parameterized? |
|
|
40
|
+
| A04 | Insecure Design | Is the threat model documented? |
|
|
41
|
+
| A05 | Security Misconfiguration | Are defaults changed? Debug off in prod? |
|
|
42
|
+
| A06 | Vulnerable Components | Are dependencies up to date? |
|
|
43
|
+
| A07 | Auth & Session Failures | Are sessions invalidated on logout? Token expiry set? |
|
|
44
|
+
| A08 | Software Integrity Failures | Are supply-chain dependencies verified? |
|
|
45
|
+
| A09 | Logging & Monitoring Failures | Are security events logged without leaking PII? |
|
|
46
|
+
| A10 | SSRF | Are outbound URLs allowlisted? |
|
|
47
|
+
|
|
48
|
+
## Input Validation
|
|
49
|
+
|
|
50
|
+
Validate all input at system boundaries. Never trust data from: HTTP request
|
|
51
|
+
body, query parameters, headers, cookies, or environment variables passed by
|
|
52
|
+
external systems.
|
|
53
|
+
|
|
54
|
+
**Rules:**
|
|
55
|
+
|
|
56
|
+
- Validate type, format, length, and range
|
|
57
|
+
- Reject unknown fields — do not silently ignore them
|
|
58
|
+
- Use allowlists (accept known-good), not blocklists (reject known-bad)
|
|
59
|
+
- Validate at the API layer before it reaches the service layer
|
|
60
|
+
|
|
61
|
+
```kotlin
|
|
62
|
+
// Spring Boot — Bean Validation
|
|
63
|
+
data class CreateUserRequest(
|
|
64
|
+
@field:NotBlank @field:Email val email: String,
|
|
65
|
+
@field:Size(min = 8, max = 72) val password: String,
|
|
66
|
+
@field:Size(max = 100) val name: String
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@PostMapping
|
|
70
|
+
fun createUser(@Valid @RequestBody request: CreateUserRequest): ResponseEntity<UserResponse> {
|
|
71
|
+
// request is guaranteed valid here
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
# Django REST Framework — serializer validation
|
|
77
|
+
class CreateUserSerializer(serializers.Serializer):
|
|
78
|
+
email = serializers.EmailField(max_length=254)
|
|
79
|
+
password = serializers.CharField(min_length=8, max_length=72, write_only=True)
|
|
80
|
+
name = serializers.CharField(max_length=100)
|
|
81
|
+
|
|
82
|
+
def validate_email(self, value):
|
|
83
|
+
if User.objects.filter(email=value).exists():
|
|
84
|
+
raise serializers.ValidationError("Email already registered.")
|
|
85
|
+
return value.lower()
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// Next.js / Astro — Zod
|
|
90
|
+
import { z } from 'zod';
|
|
91
|
+
|
|
92
|
+
const CreateUserSchema = z.object({
|
|
93
|
+
email: z.string().email().max(254),
|
|
94
|
+
password: z.string().min(8).max(72),
|
|
95
|
+
name: z.string().max(100),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const result = CreateUserSchema.safeParse(body);
|
|
99
|
+
if (!result.success) {
|
|
100
|
+
return Response.json({ error: result.error.flatten() }, { status: 400 });
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Authentication
|
|
105
|
+
|
|
106
|
+
### JWT (Stateless)
|
|
107
|
+
|
|
108
|
+
Use JWTs for stateless API authentication. Keep access tokens short-lived.
|
|
109
|
+
|
|
110
|
+
**Token lifetimes:**
|
|
111
|
+
|
|
112
|
+
| Token | Lifetime | Storage |
|
|
113
|
+
|-------|----------|---------|
|
|
114
|
+
| Access token | 15 minutes | Memory (never localStorage) |
|
|
115
|
+
| Refresh token | 7–30 days | HttpOnly cookie |
|
|
116
|
+
|
|
117
|
+
Never store access tokens in `localStorage` — XSS can read them. Store in
|
|
118
|
+
memory (React state, module variable) and refresh via the HttpOnly cookie.
|
|
119
|
+
|
|
120
|
+
```kotlin
|
|
121
|
+
// Spring Security 6 + JWT
|
|
122
|
+
@Configuration
|
|
123
|
+
@EnableWebSecurity
|
|
124
|
+
class SecurityConfig(private val jwtFilter: JwtAuthenticationFilter) {
|
|
125
|
+
|
|
126
|
+
@Bean
|
|
127
|
+
fun filterChain(http: HttpSecurity): SecurityFilterChain = http
|
|
128
|
+
.csrf { it.disable() } // stateless API — CSRF not needed
|
|
129
|
+
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
|
|
130
|
+
.authorizeHttpRequests { auth ->
|
|
131
|
+
auth
|
|
132
|
+
.requestMatchers("/api/auth/**").permitAll()
|
|
133
|
+
.requestMatchers("/api/admin/**").hasRole("ADMIN")
|
|
134
|
+
.anyRequest().authenticated()
|
|
135
|
+
}
|
|
136
|
+
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter::class.java)
|
|
137
|
+
.build()
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
# Django — djangorestframework-simplejwt
|
|
143
|
+
# settings.py
|
|
144
|
+
SIMPLE_JWT = {
|
|
145
|
+
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=15),
|
|
146
|
+
"REFRESH_TOKEN_LIFETIME": timedelta(days=7),
|
|
147
|
+
"ROTATE_REFRESH_TOKENS": True,
|
|
148
|
+
"BLACKLIST_AFTER_ROTATION": True,
|
|
149
|
+
"ALGORITHM": "HS256",
|
|
150
|
+
"AUTH_HEADER_TYPES": ("Bearer",),
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// Next.js — Auth.js (NextAuth v5)
|
|
156
|
+
// auth.ts
|
|
157
|
+
import NextAuth from 'next-auth';
|
|
158
|
+
import Credentials from 'next-auth/providers/credentials';
|
|
159
|
+
|
|
160
|
+
export const { auth, signIn, signOut } = NextAuth({
|
|
161
|
+
providers: [
|
|
162
|
+
Credentials({
|
|
163
|
+
authorize: async (credentials) => {
|
|
164
|
+
// validate and return user or null
|
|
165
|
+
},
|
|
166
|
+
}),
|
|
167
|
+
],
|
|
168
|
+
session: { strategy: 'jwt', maxAge: 7 * 24 * 60 * 60 },
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Password Hashing
|
|
173
|
+
|
|
174
|
+
Never store plaintext passwords. Never use MD5 or SHA-1 for password hashing.
|
|
175
|
+
|
|
176
|
+
| Algorithm | Use | Work factor |
|
|
177
|
+
|-----------|-----|-------------|
|
|
178
|
+
| bcrypt | default | cost=12 |
|
|
179
|
+
| Argon2id | preferred when available | m=65536, t=3 |
|
|
180
|
+
| PBKDF2 | FIPS environments | iterations=600000 |
|
|
181
|
+
|
|
182
|
+
```kotlin
|
|
183
|
+
// Spring Security uses bcrypt by default
|
|
184
|
+
@Bean
|
|
185
|
+
fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder(12)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
# Django uses PBKDF2 by default; switch to Argon2 for stronger hashing
|
|
190
|
+
# pip install django[argon2]
|
|
191
|
+
PASSWORD_HASHERS = [
|
|
192
|
+
"django.contrib.auth.hashers.Argon2PasswordHasher",
|
|
193
|
+
"django.contrib.auth.hashers.PBKDF2PasswordHasher", # fallback for existing hashes
|
|
194
|
+
]
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Authorization
|
|
198
|
+
|
|
199
|
+
**Principle of least privilege:** every endpoint must explicitly declare who
|
|
200
|
+
can access it. Deny by default.
|
|
201
|
+
|
|
202
|
+
```kotlin
|
|
203
|
+
// Spring — method-level security
|
|
204
|
+
@PreAuthorize("hasRole('ADMIN') or #userId == authentication.name")
|
|
205
|
+
fun getUser(userId: String): UserResponse { ... }
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
# Django REST Framework
|
|
210
|
+
class UserDetailView(RetrieveUpdateAPIView):
|
|
211
|
+
permission_classes = [IsAuthenticated, IsOwnerOrAdmin]
|
|
212
|
+
queryset = User.objects.all()
|
|
213
|
+
|
|
214
|
+
class IsOwnerOrAdmin(BasePermission):
|
|
215
|
+
def has_object_permission(self, request, view, obj):
|
|
216
|
+
return request.user.is_staff or obj == request.user
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Always check authorization at the **object level**, not just the endpoint
|
|
220
|
+
level. An authenticated user should not be able to access another user's data
|
|
221
|
+
by changing an ID in the URL.
|
|
222
|
+
|
|
223
|
+
## CORS
|
|
224
|
+
|
|
225
|
+
```kotlin
|
|
226
|
+
// Spring Boot — explicit allowlist; never use "*" in production
|
|
227
|
+
@Bean
|
|
228
|
+
fun corsConfigurationSource(): CorsConfigurationSource {
|
|
229
|
+
val config = CorsConfiguration().apply {
|
|
230
|
+
allowedOrigins = listOf("https://app.example.com")
|
|
231
|
+
allowedMethods = listOf("GET", "POST", "PUT", "DELETE")
|
|
232
|
+
allowedHeaders = listOf("Authorization", "Content-Type")
|
|
233
|
+
allowCredentials = true
|
|
234
|
+
maxAge = 3600L
|
|
235
|
+
}
|
|
236
|
+
return UrlBasedCorsConfigurationSource().apply {
|
|
237
|
+
registerCorsConfiguration("/api/**", config)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
# Django — django-cors-headers
|
|
244
|
+
CORS_ALLOWED_ORIGINS = [
|
|
245
|
+
"https://app.example.com",
|
|
246
|
+
]
|
|
247
|
+
CORS_ALLOW_CREDENTIALS = True
|
|
248
|
+
CORS_ALLOW_ALL_ORIGINS = False # never True in production
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Security Headers
|
|
252
|
+
|
|
253
|
+
Set these headers on every response. They defend against XSS, clickjacking,
|
|
254
|
+
and MIME sniffing.
|
|
255
|
+
|
|
256
|
+
```text
|
|
257
|
+
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
|
|
258
|
+
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'
|
|
259
|
+
X-Frame-Options: DENY
|
|
260
|
+
X-Content-Type-Options: nosniff
|
|
261
|
+
Referrer-Policy: strict-origin-when-cross-origin
|
|
262
|
+
Permissions-Policy: camera=(), microphone=(), geolocation=()
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
// Next.js — next.config.ts
|
|
267
|
+
const securityHeaders = [
|
|
268
|
+
{ key: 'X-Frame-Options', value: 'DENY' },
|
|
269
|
+
{ key: 'X-Content-Type-Options', value: 'nosniff' },
|
|
270
|
+
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
|
|
271
|
+
{
|
|
272
|
+
key: 'Strict-Transport-Security',
|
|
273
|
+
value: 'max-age=63072000; includeSubDomains; preload',
|
|
274
|
+
},
|
|
275
|
+
];
|
|
276
|
+
|
|
277
|
+
export default {
|
|
278
|
+
headers: async () => [
|
|
279
|
+
{ source: '/(.*)', headers: securityHeaders },
|
|
280
|
+
],
|
|
281
|
+
};
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Secrets Management
|
|
285
|
+
|
|
286
|
+
**Never commit secrets to source control.** No exceptions.
|
|
287
|
+
|
|
288
|
+
```text
|
|
289
|
+
✗ DATABASE_URL = "postgres://user:password@prod.example.com/db" # in source
|
|
290
|
+
✓ DATABASE_URL = env("DATABASE_URL") # from environment
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Rules:
|
|
294
|
+
|
|
295
|
+
- Load secrets from environment variables or a secrets manager (AWS Secrets
|
|
296
|
+
Manager, Vault, GCP Secret Manager)
|
|
297
|
+
- Rotate credentials on any suspected exposure
|
|
298
|
+
- Use separate credentials per environment (dev, staging, prod)
|
|
299
|
+
- Add `.env` to `.gitignore` and never commit it
|
|
300
|
+
|
|
301
|
+
```python
|
|
302
|
+
# Django — django-environ
|
|
303
|
+
import environ
|
|
304
|
+
env = environ.Env()
|
|
305
|
+
environ.Env.read_env() # reads .env in development only
|
|
306
|
+
|
|
307
|
+
SECRET_KEY = env("SECRET_KEY")
|
|
308
|
+
DATABASE_URL = env.db("DATABASE_URL")
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## Rate Limiting
|
|
312
|
+
|
|
313
|
+
Protect authentication endpoints. Brute-force without rate limiting can
|
|
314
|
+
compromise any account with a weak password.
|
|
315
|
+
|
|
316
|
+
```kotlin
|
|
317
|
+
// Spring Boot — Bucket4j
|
|
318
|
+
@Component
|
|
319
|
+
class RateLimitFilter(private val rateLimiter: RateLimiter) : OncePerRequestFilter() {
|
|
320
|
+
|
|
321
|
+
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
|
|
322
|
+
if (request.requestURI.startsWith("/api/auth/")) {
|
|
323
|
+
val probe = rateLimiter.tryConsumeAndReturnRemaining(1)
|
|
324
|
+
if (!probe.isConsumed) {
|
|
325
|
+
response.status = HttpStatus.TOO_MANY_REQUESTS.value()
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
chain.doFilter(request, response)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Minimum rate limits for auth endpoints: 5 requests per minute per IP. Stricter
|
|
335
|
+
limits (e.g., 3 per 15 minutes) are better for login endpoints.
|
|
336
|
+
|
|
337
|
+
## SQL Injection Prevention
|
|
338
|
+
|
|
339
|
+
Never concatenate user input into SQL strings. Always use parameterized queries
|
|
340
|
+
or an ORM.
|
|
341
|
+
|
|
342
|
+
```kotlin
|
|
343
|
+
// bad — SQL injection risk
|
|
344
|
+
val user = jdbcTemplate.queryForObject("SELECT * FROM users WHERE email = '$email'")
|
|
345
|
+
|
|
346
|
+
// good — parameterized
|
|
347
|
+
val user = jdbcTemplate.queryForObject(
|
|
348
|
+
"SELECT * FROM users WHERE email = ?",
|
|
349
|
+
UserRowMapper(),
|
|
350
|
+
email
|
|
351
|
+
)
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
```python
|
|
355
|
+
# bad
|
|
356
|
+
User.objects.raw(f"SELECT * FROM users WHERE email = '{email}'")
|
|
357
|
+
|
|
358
|
+
# good — Django ORM handles parameterization automatically
|
|
359
|
+
User.objects.filter(email=email)
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## Logging Security Events
|
|
363
|
+
|
|
364
|
+
Log authentication events without including sensitive data.
|
|
365
|
+
|
|
366
|
+
```text
|
|
367
|
+
Log: login success/failure, permission denied, password reset, token refresh
|
|
368
|
+
Never: passwords, tokens, PII (full email in prod logs), credit card numbers
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
```kotlin
|
|
372
|
+
// Log security events at INFO level; failures at WARN
|
|
373
|
+
log.info("Authentication successful for user={}", userId)
|
|
374
|
+
log.warn("Authentication failed: invalid credentials for user={}", maskedEmail)
|
|
375
|
+
log.warn("Authorization denied: user={} attempted access to resource={}", userId, resourceId)
|
|
376
|
+
```
|