cyberaudit-skill 3.0.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.
Files changed (60) hide show
  1. package/README.md +77 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +241 -0
  4. package/dist/mcp-server.d.ts +2 -0
  5. package/dist/mcp-server.js +126 -0
  6. package/package.json +58 -0
  7. package/skills/cyberaudit/AGENT-BOOT.md +122 -0
  8. package/skills/cyberaudit/COMMANDS.md +1125 -0
  9. package/skills/cyberaudit/INSTALL.md +311 -0
  10. package/skills/cyberaudit/MASTER.md +194 -0
  11. package/skills/cyberaudit/README.md +154 -0
  12. package/skills/cyberaudit/USAGE-GUIDE.md +107 -0
  13. package/skills/cyberaudit/mobile/MOBILE-CHECKLIST.md +245 -0
  14. package/skills/cyberaudit/mobile/MOBILE-PHILOSOPHY.md +352 -0
  15. package/skills/cyberaudit/mobile/MOBILE-REMEDIATION-LIBRARY.md +468 -0
  16. package/skills/cyberaudit/mobile/MOBILE-THREAT-MODELS.md +279 -0
  17. package/skills/cyberaudit/mobile/frameworks/EXPO.md +247 -0
  18. package/skills/cyberaudit/mobile/frameworks/FLUTTER.md +338 -0
  19. package/skills/cyberaudit/mobile/frameworks/IONIC.md +248 -0
  20. package/skills/cyberaudit/mobile/frameworks/REACT-NATIVE.md +479 -0
  21. package/skills/cyberaudit/mobile/vulnerabilities/AUTH-MOBILE.md +160 -0
  22. package/skills/cyberaudit/mobile/vulnerabilities/BINARY-ANALYSIS.md +193 -0
  23. package/skills/cyberaudit/mobile/vulnerabilities/CRYPTO-MOBILE.md +192 -0
  24. package/skills/cyberaudit/mobile/vulnerabilities/IPC-DEEPLINKS.md +231 -0
  25. package/skills/cyberaudit/mobile/vulnerabilities/NETWORK-MOBILE.md +165 -0
  26. package/skills/cyberaudit/mobile/vulnerabilities/PERMISSIONS.md +180 -0
  27. package/skills/cyberaudit/mobile/vulnerabilities/RUNTIME-MOBILE.md +214 -0
  28. package/skills/cyberaudit/mobile/vulnerabilities/STORAGE.md +197 -0
  29. package/skills/cyberaudit/reports/EXECUTIVE-SUMMARY-TEMPLATE.md +188 -0
  30. package/skills/cyberaudit/reports/REPORT-TEMPLATE-MOBILE.md +410 -0
  31. package/skills/cyberaudit/reports/REPORT-TEMPLATE-WEB.md +222 -0
  32. package/skills/cyberaudit/shared/COMPLIANCE.md +307 -0
  33. package/skills/cyberaudit/shared/CVSS-GUIDE.md +175 -0
  34. package/skills/cyberaudit/shared/OWASP-MAPPER.md +130 -0
  35. package/skills/cyberaudit/shared/SEVERITY-SCORING.md +102 -0
  36. package/skills/cyberaudit/shared/THREAT-MODELING.md +112 -0
  37. package/skills/cyberaudit/web/WEB-CHECKLIST.md +222 -0
  38. package/skills/cyberaudit/web/WEB-PHILOSOPHY.md +308 -0
  39. package/skills/cyberaudit/web/WEB-REMEDIATION-LIBRARY.md +665 -0
  40. package/skills/cyberaudit/web/WEB-THREAT-MODELS.md +460 -0
  41. package/skills/cyberaudit/web/frameworks/ANGULAR.md +169 -0
  42. package/skills/cyberaudit/web/frameworks/EXPRESS.md +185 -0
  43. package/skills/cyberaudit/web/frameworks/LARAVEL.md +371 -0
  44. package/skills/cyberaudit/web/frameworks/NESTJS.md +337 -0
  45. package/skills/cyberaudit/web/frameworks/NEXTJS.md +352 -0
  46. package/skills/cyberaudit/web/frameworks/REACT.md +346 -0
  47. package/skills/cyberaudit/web/frameworks/VUE.md +205 -0
  48. package/skills/cyberaudit/web/vulnerabilities/AUTH-AUTHZ.md +117 -0
  49. package/skills/cyberaudit/web/vulnerabilities/BUSINESS-LOGIC.md +603 -0
  50. package/skills/cyberaudit/web/vulnerabilities/CORS.md +117 -0
  51. package/skills/cyberaudit/web/vulnerabilities/CSRF.md +131 -0
  52. package/skills/cyberaudit/web/vulnerabilities/DESERIALIZATION.md +96 -0
  53. package/skills/cyberaudit/web/vulnerabilities/HEADERS.md +105 -0
  54. package/skills/cyberaudit/web/vulnerabilities/IDOR-BOLA.md +153 -0
  55. package/skills/cyberaudit/web/vulnerabilities/INJECTION.md +165 -0
  56. package/skills/cyberaudit/web/vulnerabilities/SECRETS.md +119 -0
  57. package/skills/cyberaudit/web/vulnerabilities/SSRF.md +124 -0
  58. package/skills/cyberaudit/web/vulnerabilities/SUPPLY-CHAIN.md +142 -0
  59. package/skills/cyberaudit/web/vulnerabilities/XSS.md +133 -0
  60. package/skills/cyberaudit/web/vulnerabilities/XXE.md +94 -0
@@ -0,0 +1,665 @@
1
+ # 🔧 WEB REMEDIATION LIBRARY — CYBERAUDIT SKILL
2
+ # Comprehensive library of ready-to-use fixes
3
+
4
+ ═══════════════════════════════════════════════════════════════
5
+ USAGE: When a finding is identified, pick the corresponding
6
+ remediation code here.
7
+ Each remediation is tested and production-ready.
8
+ ═══════════════════════════════════════════════════════════════
9
+
10
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
11
+ REM-001 — UNIVERSAL INPUT VALIDATION
12
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
13
+
14
+ LARAVEL — Full FormRequest
15
+ class StoreUserRequest extends FormRequest
16
+ {
17
+ public function authorize(): bool
18
+ {
19
+ return auth()->check();
20
+ }
21
+
22
+ public function rules(): array
23
+ {
24
+ return [
25
+ 'name' => ['required', 'string', 'min:2', 'max:100',
26
+ 'regex:/^[\p{L}\s\-]+$/u'],
27
+ 'email' => ['required', 'email:rfc,dns', 'max:255',
28
+ 'unique:users,email'],
29
+ 'age' => ['required', 'integer', 'min:18', 'max:120'],
30
+ 'website' => ['nullable', 'url', 'max:255',
31
+ 'regex:/^https?:\/\//'],
32
+ 'bio' => ['nullable', 'string', 'max:1000'],
33
+ 'role' => ['prohibited'], // Never from the client
34
+ ];
35
+ }
36
+
37
+ public function messages(): array
38
+ {
39
+ return [
40
+ 'email.unique' => 'This email is already in use.',
41
+ // Generic messages to avoid enumeration
42
+ ];
43
+ }
44
+ }
45
+
46
+ NESTJS — DTO with class-validator
47
+ import { IsString, IsEmail, MinLength, MaxLength,
48
+ Matches, IsOptional, IsUrl } from 'class-validator'
49
+ import { Transform } from 'class-transformer'
50
+
51
+ export class CreateUserDto {
52
+ @IsString()
53
+ @MinLength(2)
54
+ @MaxLength(100)
55
+ @Matches(/^[\p{L}\s\-]+$/u, {
56
+ message: 'Name can only contain letters'
57
+ })
58
+ @Transform(({ value }) => value?.trim())
59
+ name: string
60
+
61
+ @IsEmail()
62
+ @MaxLength(255)
63
+ @Transform(({ value }) => value?.toLowerCase().trim())
64
+ email: string
65
+
66
+ @IsString()
67
+ @MinLength(12)
68
+ @MaxLength(128)
69
+ @Matches(
70
+ /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/,
71
+ { message: 'Password insufficiently complex' }
72
+ )
73
+ password: string
74
+
75
+ @IsOptional()
76
+ @IsUrl({ protocols: ['https'] })
77
+ website?: string
78
+ }
79
+
80
+ // In the main module:
81
+ // app.useGlobalPipes(new ValidationPipe({
82
+ // whitelist: true, // Strips undeclared fields
83
+ // forbidNonWhitelisted: true, // Error on unknown fields
84
+ // transform: true, // Transforms according to DTO types
85
+ // }))
86
+
87
+ NEXTJS — Validation with Zod
88
+ import { z } from 'zod'
89
+
90
+ const UserSchema = z.object({
91
+ name: z.string()
92
+ .min(2, 'Name too short')
93
+ .max(100, 'Name too long')
94
+ .regex(/^[\p{L}\s\-]+$/u, 'Invalid characters'),
95
+
96
+ email: z.string()
97
+ .email('Invalid email')
98
+ .max(255)
99
+ .toLowerCase()
100
+ .trim(),
101
+
102
+ password: z.string()
103
+ .min(12, 'Minimum 12 characters')
104
+ .regex(/[A-Z]/, 'At least one uppercase')
105
+ .regex(/[0-9]/, 'At least one digit')
106
+ .regex(/[^A-Za-z0-9]/, 'At least one special character'),
107
+
108
+ website: z.string()
109
+ .url()
110
+ .startsWith('https://')
111
+ .optional(),
112
+ })
113
+
114
+ // Usage in a Server Action or API Route:
115
+ export async function createUser(data: unknown) {
116
+ const validated = UserSchema.safeParse(data)
117
+ if (!validated.success) {
118
+ return { error: validated.error.flatten() }
119
+ }
120
+ // Now validated.data is typed and safe
121
+ }
122
+
123
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
124
+ REM-002 — SECURE PASSWORD HASHING
125
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
126
+
127
+ NODE.JS — Argon2 (recommended)
128
+ import argon2 from 'argon2'
129
+
130
+ // Hash
131
+ const hashedPassword = await argon2.hash(plainPassword, {
132
+ type: argon2.argon2id, // Resistant to timing AND side-channel
133
+ memoryCost: 65536, // 64 MB
134
+ timeCost: 3, // 3 iterations
135
+ parallelism: 4, // 4 threads
136
+ })
137
+
138
+ // Verify
139
+ const isValid = await argon2.verify(hashedPassword, plainPassword)
140
+
141
+ NODE.JS — bcrypt (acceptable)
142
+ import bcrypt from 'bcrypt'
143
+ const SALT_ROUNDS = 12 // Minimum 12 in production
144
+
145
+ const hashedPassword = await bcrypt.hash(plainPassword, SALT_ROUNDS)
146
+ const isValid = await bcrypt.compare(plainPassword, hashedPassword)
147
+
148
+ LARAVEL
149
+ // Hash (automatic with $casts)
150
+ protected $casts = ['password' => 'hashed'];
151
+
152
+ // Or manually
153
+ $hashedPassword = Hash::make($plainPassword);
154
+
155
+ // Verify
156
+ $isValid = Hash::check($plainPassword, $hashedPassword);
157
+
158
+ // Check if rehash needed (after config change)
159
+ if (Hash::needsRehash($user->password)) {
160
+ $user->update(['password' => Hash::make($newPassword)]);
161
+ }
162
+
163
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
164
+ REM-003 — SECURE JWT
165
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
166
+
167
+ NODE.JS — Full configuration
168
+ import jwt from 'jsonwebtoken'
169
+ import crypto from 'crypto'
170
+
171
+ // Generate a strong secret (do once)
172
+ // node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
173
+
174
+ const JWT_CONFIG = {
175
+ accessToken: {
176
+ secret: process.env.JWT_ACCESS_SECRET, // 64+ random bytes
177
+ expiresIn: '15m',
178
+ algorithm: 'HS256' as const,
179
+ },
180
+ refreshToken: {
181
+ secret: process.env.JWT_REFRESH_SECRET, // Different secret!
182
+ expiresIn: '7d',
183
+ algorithm: 'HS256' as const,
184
+ }
185
+ }
186
+
187
+ // Create an access token
188
+ function createAccessToken(userId: string, role: string): string {
189
+ return jwt.sign(
190
+ {
191
+ sub: userId,
192
+ role: role,
193
+ type: 'access',
194
+ jti: crypto.randomUUID(), // Unique token ID
195
+ },
196
+ JWT_CONFIG.accessToken.secret!,
197
+ {
198
+ expiresIn: JWT_CONFIG.accessToken.expiresIn,
199
+ algorithm: JWT_CONFIG.accessToken.algorithm,
200
+ issuer: 'yourapp.com',
201
+ audience: 'yourapp-api',
202
+ }
203
+ )
204
+ }
205
+
206
+ // Verify a token
207
+ function verifyAccessToken(token: string) {
208
+ try {
209
+ return jwt.verify(token, JWT_CONFIG.accessToken.secret!, {
210
+ algorithms: ['HS256'], // NEVER leave 'none'
211
+ issuer: 'yourapp.com',
212
+ audience: 'yourapp-api',
213
+ })
214
+ } catch (error) {
215
+ // TokenExpiredError, JsonWebTokenError, NotBeforeError
216
+ throw new UnauthorizedException('Invalid token')
217
+ }
218
+ }
219
+
220
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
221
+ REM-004 — COMPLETE SECURITY HEADERS
222
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
223
+
224
+ NEXT.JS — next.config.js
225
+ const securityHeaders = [
226
+ {
227
+ key: 'Content-Security-Policy',
228
+ value: [
229
+ "default-src 'self'",
230
+ "script-src 'self' 'nonce-{NONCE}'",
231
+ "style-src 'self' 'unsafe-inline'",
232
+ "img-src 'self' data: https:",
233
+ "font-src 'self'",
234
+ "connect-src 'self' https://api.yourapp.com",
235
+ "frame-ancestors 'none'",
236
+ "base-uri 'self'",
237
+ "form-action 'self'",
238
+ "upgrade-insecure-requests",
239
+ ].join('; ')
240
+ },
241
+ {
242
+ key: 'Strict-Transport-Security',
243
+ value: 'max-age=31536000; includeSubDomains; preload'
244
+ },
245
+ {
246
+ key: 'X-Frame-Options',
247
+ value: 'DENY'
248
+ },
249
+ {
250
+ key: 'X-Content-Type-Options',
251
+ value: 'nosniff'
252
+ },
253
+ {
254
+ key: 'Referrer-Policy',
255
+ value: 'strict-origin-when-cross-origin'
256
+ },
257
+ {
258
+ key: 'Permissions-Policy',
259
+ value: 'camera=(), microphone=(), geolocation=(), payment=()'
260
+ },
261
+ {
262
+ key: 'X-DNS-Prefetch-Control',
263
+ value: 'on'
264
+ }
265
+ ]
266
+
267
+ module.exports = {
268
+ async headers() {
269
+ return [
270
+ {
271
+ source: '/(.*)',
272
+ headers: securityHeaders,
273
+ },
274
+ ]
275
+ },
276
+ }
277
+
278
+ LARAVEL — Middleware
279
+ class SecurityHeadersMiddleware
280
+ {
281
+ public function handle(Request $request, Closure $next): Response
282
+ {
283
+ $response = $next($request);
284
+
285
+ $response->headers->set(
286
+ 'Content-Security-Policy',
287
+ "default-src 'self'; " .
288
+ "script-src 'self'; " .
289
+ "style-src 'self' 'unsafe-inline'; " .
290
+ "img-src 'self' data: https:; " .
291
+ "frame-ancestors 'none'; " .
292
+ "base-uri 'self'"
293
+ );
294
+ $response->headers->set(
295
+ 'Strict-Transport-Security',
296
+ 'max-age=31536000; includeSubDomains; preload'
297
+ );
298
+ $response->headers->set('X-Frame-Options', 'DENY');
299
+ $response->headers->set('X-Content-Type-Options', 'nosniff');
300
+ $response->headers->set(
301
+ 'Referrer-Policy',
302
+ 'strict-origin-when-cross-origin'
303
+ );
304
+ $response->headers->set(
305
+ 'Permissions-Policy',
306
+ 'camera=(), microphone=(), geolocation=()'
307
+ );
308
+ $response->headers->remove('X-Powered-By');
309
+ $response->headers->remove('Server');
310
+
311
+ return $response;
312
+ }
313
+ }
314
+
315
+ NESTJS — Helmet
316
+ import helmet from '@fastify/helmet'
317
+ // OR for Express:
318
+ import helmet from 'helmet'
319
+
320
+ app.use(helmet({
321
+ contentSecurityPolicy: {
322
+ directives: {
323
+ defaultSrc: ["'self'"],
324
+ scriptSrc: ["'self'"],
325
+ styleSrc: ["'self'", "'unsafe-inline'"],
326
+ imgSrc: ["'self'", 'data:', 'https:'],
327
+ frameAncestors: ["'none'"],
328
+ baseUri: ["'self'"],
329
+ formAction: ["'self'"],
330
+ },
331
+ },
332
+ hsts: {
333
+ maxAge: 31536000,
334
+ includeSubDomains: true,
335
+ preload: true,
336
+ },
337
+ frameguard: { action: 'deny' },
338
+ noSniff: true,
339
+ referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
340
+ }))
341
+
342
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
343
+ REM-005 — RATE LIMITING
344
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
345
+
346
+ NESTJS — ThrottlerModule
347
+ // app.module.ts
348
+ ThrottlerModule.forRoot([
349
+ { name: 'global', ttl: 60000, limit: 100 },
350
+ ])
351
+
352
+ // On sensitive endpoints:
353
+ @Throttle({ default: { ttl: 60000, limit: 5 } })
354
+ @Post('auth/login')
355
+ login(@Body() dto: LoginDto) { ... }
356
+
357
+ @Throttle({ default: { ttl: 3600000, limit: 3 } })
358
+ @Post('auth/forgot-password')
359
+ forgotPassword(@Body() dto: ForgotPasswordDto) { ... }
360
+
361
+ LARAVEL
362
+ // routes/api.php
363
+ Route::middleware(['throttle:login'])->group(function () {
364
+ Route::post('/login', [AuthController::class, 'login']);
365
+ });
366
+
367
+ // app/Providers/RouteServiceProvider.php
368
+ RateLimiter::for('login', function (Request $request) {
369
+ return [
370
+ Limit::perMinute(5)->by($request->ip()),
371
+ Limit::perMinute(10)->by($request->input('email')),
372
+ ];
373
+ });
374
+
375
+ RateLimiter::for('api', function (Request $request) {
376
+ return Limit::perMinute(60)->by(
377
+ $request->user()?->id ?: $request->ip()
378
+ );
379
+ });
380
+
381
+ NEXT.JS — with upstash/ratelimit
382
+ import { Ratelimit } from '@upstash/ratelimit'
383
+ import { Redis } from '@upstash/redis'
384
+
385
+ const ratelimit = new Ratelimit({
386
+ redis: Redis.fromEnv(),
387
+ limiter: Ratelimit.slidingWindow(5, '60 s'),
388
+ })
389
+
390
+ export async function POST(request: Request) {
391
+ const ip = request.headers.get('x-forwarded-for') ?? 'anonymous'
392
+ const { success, limit, remaining } = await ratelimit.limit(ip)
393
+
394
+ if (!success) {
395
+ return new Response('Too Many Requests', {
396
+ status: 429,
397
+ headers: {
398
+ 'X-RateLimit-Limit': limit.toString(),
399
+ 'X-RateLimit-Remaining': remaining.toString(),
400
+ 'Retry-After': '60',
401
+ }
402
+ })
403
+ }
404
+ // Continue processing
405
+ }
406
+
407
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
408
+ REM-006 — SECURE CORS
409
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
410
+
411
+ NESTJS
412
+ app.enableCors({
413
+ origin: (origin, callback) => {
414
+ const allowedOrigins = [
415
+ 'https://yourapp.com',
416
+ 'https://www.yourapp.com',
417
+ process.env.NODE_ENV === 'development'
418
+ ? 'http://localhost:3000'
419
+ : null,
420
+ ].filter(Boolean)
421
+
422
+ if (!origin || allowedOrigins.includes(origin)) {
423
+ callback(null, true)
424
+ } else {
425
+ callback(new Error(`CORS: origin ${origin} not allowed`))
426
+ }
427
+ },
428
+ methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
429
+ allowedHeaders: ['Content-Type', 'Authorization'],
430
+ credentials: true, // Allows cookies
431
+ maxAge: 86400, // Cache preflight 24h
432
+ })
433
+
434
+ LARAVEL — config/cors.php
435
+ return [
436
+ 'paths' => ['api/*'],
437
+ 'allowed_methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
438
+ 'allowed_origins' => [env('FRONTEND_URL', 'https://yourapp.com')],
439
+ 'allowed_origins_patterns' => [],
440
+ 'allowed_headers' => ['Content-Type', 'Authorization', 'X-Requested-With'],
441
+ 'exposed_headers' => [],
442
+ 'max_age' => 86400,
443
+ 'supports_credentials' => true,
444
+ ];
445
+
446
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
447
+ REM-007 — SECURE ERROR HANDLING
448
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
449
+
450
+ NESTJS — Global Exception Filter
451
+ @Catch()
452
+ export class GlobalExceptionFilter implements ExceptionFilter {
453
+ catch(exception: unknown, host: ArgumentsHost) {
454
+ const ctx = host.switchToHttp()
455
+ const response = ctx.getResponse()
456
+ const request = ctx.getRequest()
457
+
458
+ const status = exception instanceof HttpException
459
+ ? exception.getStatus()
460
+ : HttpStatus.INTERNAL_SERVER_ERROR
461
+
462
+ // Full log for internal monitoring
463
+ console.error({
464
+ timestamp: new Date().toISOString(),
465
+ path: request.url,
466
+ exception: exception,
467
+ })
468
+
469
+ // Minimal response for the client
470
+ // Never expose internal details
471
+ response.status(status).json({
472
+ statusCode: status,
473
+ message: status === 500
474
+ ? 'An internal error occurred'
475
+ : (exception as HttpException).message,
476
+ // No stack trace, no infrastructure details
477
+ })
478
+ }
479
+ }
480
+
481
+ LARAVEL — app/Exceptions/Handler.php
482
+ public function render($request, Throwable $exception): Response
483
+ {
484
+ if ($request->expectsJson()) {
485
+ if ($exception instanceof ValidationException) {
486
+ return response()->json([
487
+ 'message' => 'Invalid data',
488
+ 'errors' => $exception->errors(),
489
+ ], 422);
490
+ }
491
+
492
+ if ($exception instanceof ModelNotFoundException) {
493
+ return response()->json([
494
+ 'message' => 'Resource not found',
495
+ ], 404);
496
+ }
497
+
498
+ // Generic error — do not expose details
499
+ $statusCode = method_exists($exception, 'getStatusCode')
500
+ ? $exception->getStatusCode() : 500;
501
+
502
+ return response()->json([
503
+ 'message' => app()->isProduction()
504
+ ? 'An error occurred'
505
+ : $exception->getMessage(),
506
+ ], $statusCode);
507
+ }
508
+
509
+ return parent::render($request, $exception);
510
+ }
511
+
512
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
513
+ REM-008 — SECURE UPLOAD
514
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
515
+
516
+ NESTJS — Full validation pipe
517
+ import { FileInterceptor } from '@nestjs/platform-express'
518
+ import { memoryStorage } from 'multer'
519
+ import { fromBuffer } from 'file-type'
520
+ import sharp from 'sharp'
521
+ import { randomBytes } from 'crypto'
522
+
523
+ @Post('upload')
524
+ @UseInterceptors(FileInterceptor('file', {
525
+ storage: memoryStorage(),
526
+ limits: {
527
+ fileSize: 5 * 1024 * 1024, // 5MB max
528
+ files: 1,
529
+ },
530
+ }))
531
+ async uploadFile(@UploadedFile() file: Express.Multer.File) {
532
+
533
+ // 1. Verify real MIME type (magic bytes)
534
+ const fileType = await fromBuffer(file.buffer)
535
+ const allowedMimes = ['image/jpeg', 'image/png', 'image/webp']
536
+
537
+ if (!fileType || !allowedMimes.includes(fileType.mime)) {
538
+ throw new BadRequestException('File type not allowed')
539
+ }
540
+
541
+ // 2. Reprocess image (strips metadata and payloads)
542
+ const safeBuffer = await sharp(file.buffer)
543
+ .resize(1920, 1080, { fit: 'inside', withoutEnlargement: true })
544
+ .jpeg({ quality: 85 })
545
+ .toBuffer()
546
+
547
+ // 3. Random filename
548
+ const filename = `${randomBytes(32).toString('hex')}.jpg`
549
+
550
+ // 4. Store in a private bucket (not web-accessible)
551
+ // await s3.upload({ Bucket: 'private-uploads', Key: filename, ... })
552
+
553
+ // 5. Return a signed URL or controlled route
554
+ return { url: `/api/files/${filename}` }
555
+ }
556
+
557
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
558
+ REM-009 — SECURE LOGGING
559
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
560
+
561
+ UNIVERSAL PATTERN — What should be logged
562
+ // ALWAYS log:
563
+ {
564
+ timestamp: "2024-01-15T10:30:00Z",
565
+ event_type: "auth.login.success" | "auth.login.failure" |
566
+ "resource.access" | "resource.modify" | "resource.delete" |
567
+ "permission.denied" | "rate_limit.exceeded",
568
+ user_id: "uuid-of-the-user", // Never name/email
569
+ ip_address: "x.x.x.x", // Hashed if strict GDPR
570
+ user_agent: "Mozilla/5.0...",
571
+ resource: "/api/orders/uuid-xxx",
572
+ action: "GET",
573
+ result: "success" | "failure",
574
+ // NEVER: password, token, credit_card, ssn, etc.
575
+ }
576
+
577
+ NODE.JS — Winston configured
578
+ import winston from 'winston'
579
+
580
+ const logger = winston.createLogger({
581
+ level: process.env.NODE_ENV === 'production' ? 'warn' : 'debug',
582
+ format: winston.format.combine(
583
+ winston.format.timestamp(),
584
+ winston.format.errors({ stack: false }), // No stack in prod
585
+ winston.format.json(),
586
+ ),
587
+ transports: [
588
+ new winston.transports.File({
589
+ filename: 'logs/security.log',
590
+ level: 'warn',
591
+ }),
592
+ new winston.transports.Console({
593
+ silent: process.env.NODE_ENV === 'production',
594
+ }),
595
+ ],
596
+ })
597
+
598
+ // Audit middleware
599
+ function auditLog(event: string, userId: string,
600
+ resource: string, result: string) {
601
+ logger.info({
602
+ event_type: event,
603
+ user_id: userId,
604
+ resource: resource,
605
+ result: result,
606
+ timestamp: new Date().toISOString(),
607
+ // No sensitive data here
608
+ })
609
+ }
610
+
611
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
612
+ REM-010 — ENVIRONMENT VARIABLES
613
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
614
+
615
+ STARTUP VALIDATION — Node.js with Zod
616
+ import { z } from 'zod'
617
+
618
+ const EnvSchema = z.object({
619
+ NODE_ENV: z.enum(['development', 'staging', 'production']),
620
+ DATABASE_URL: z.string().url().startsWith('postgresql://'),
621
+ JWT_ACCESS_SECRET: z.string().min(64),
622
+ JWT_REFRESH_SECRET:z.string().min(64),
623
+ REDIS_URL: z.string().url(),
624
+ APP_URL: z.string().url().startsWith('https://'),
625
+
626
+ // Keys that MUST NOT be there
627
+ // (if someone adds them by mistake, error at startup)
628
+ PASSWORD: z.undefined(),
629
+ SECRET: z.undefined(),
630
+ })
631
+
632
+ const env = EnvSchema.safeParse(process.env)
633
+
634
+ if (!env.success) {
635
+ console.error('❌ Invalid environment variables:')
636
+ console.error(env.error.flatten().fieldErrors)
637
+ process.exit(1) // Refuse to start
638
+ }
639
+
640
+ export const config = env.data
641
+
642
+ STARTUP VALIDATION — Laravel
643
+ // app/Providers/AppServiceProvider.php
644
+ public function boot(): void
645
+ {
646
+ $requiredVars = [
647
+ 'APP_KEY', 'DB_CONNECTION', 'DB_HOST',
648
+ 'DB_DATABASE', 'DB_USERNAME', 'DB_PASSWORD',
649
+ 'JWT_SECRET',
650
+ ];
651
+
652
+ foreach ($requiredVars as $var) {
653
+ if (empty(env($var))) {
654
+ throw new \RuntimeException(
655
+ "Missing environment variable: {$var}"
656
+ );
657
+ }
658
+ }
659
+
660
+ if (app()->isProduction() && config('app.debug')) {
661
+ throw new \RuntimeException(
662
+ 'APP_DEBUG must be false in production!'
663
+ );
664
+ }
665
+ }