@sun-asterisk/sunlint 1.3.48 → 1.3.49

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 (152) hide show
  1. package/core/file-targeting-service.js +148 -15
  2. package/core/init-command.js +118 -70
  3. package/core/project-detector.js +517 -0
  4. package/core/tui-select.js +245 -0
  5. package/engines/arch-detect/rules/layered/l001-presentation-layer.js +7 -15
  6. package/engines/arch-detect/rules/layered/l002-business-layer.js +7 -15
  7. package/engines/arch-detect/rules/layered/l003-data-layer.js +7 -15
  8. package/engines/arch-detect/rules/layered/l004-model-layer.js +7 -15
  9. package/engines/arch-detect/rules/layered/l005-layer-separation.js +22 -2
  10. package/engines/arch-detect/rules/layered/l006-dependency-direction.js +8 -5
  11. package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +67 -29
  12. package/engines/arch-detect/rules/presentation/pr001-view-layer.js +16 -9
  13. package/engines/arch-detect/rules/presentation/pr006-router-layer.js +33 -8
  14. package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +35 -6
  15. package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +56 -10
  16. package/package.json +1 -1
  17. package/skill-assets/sunlint-code-quality/rules/dart/C006-verb-noun-functions.md +45 -0
  18. package/skill-assets/sunlint-code-quality/rules/dart/C013-no-dead-code.md +53 -0
  19. package/skill-assets/sunlint-code-quality/rules/dart/C014-dependency-injection.md +92 -0
  20. package/skill-assets/sunlint-code-quality/rules/dart/C017-no-constructor-logic.md +62 -0
  21. package/skill-assets/sunlint-code-quality/rules/dart/C018-generic-errors.md +57 -0
  22. package/skill-assets/sunlint-code-quality/rules/dart/C019-error-log-level.md +50 -0
  23. package/skill-assets/sunlint-code-quality/rules/dart/C020-no-unused-imports.md +46 -0
  24. package/skill-assets/sunlint-code-quality/rules/dart/C022-no-unused-variables.md +50 -0
  25. package/skill-assets/sunlint-code-quality/rules/dart/C023-no-duplicate-names.md +56 -0
  26. package/skill-assets/sunlint-code-quality/rules/dart/C024-centralize-constants.md +75 -0
  27. package/skill-assets/sunlint-code-quality/rules/dart/C029-catch-log-root-cause.md +53 -0
  28. package/skill-assets/sunlint-code-quality/rules/dart/C030-custom-error-classes.md +86 -0
  29. package/skill-assets/sunlint-code-quality/rules/dart/C033-separate-data-access.md +90 -0
  30. package/skill-assets/sunlint-code-quality/rules/dart/C035-error-context-logging.md +62 -0
  31. package/skill-assets/sunlint-code-quality/rules/dart/C041-no-hardcoded-secrets.md +75 -0
  32. package/skill-assets/sunlint-code-quality/rules/dart/C042-boolean-naming.md +73 -0
  33. package/skill-assets/sunlint-code-quality/rules/dart/C052-widget-parsing.md +84 -0
  34. package/skill-assets/sunlint-code-quality/rules/dart/C060-superclass-logic.md +91 -0
  35. package/skill-assets/sunlint-code-quality/rules/dart/C067-no-hardcoded-config.md +108 -0
  36. package/skill-assets/sunlint-code-quality/rules/go-gin/AGENTS.md +149 -0
  37. package/skill-assets/sunlint-code-quality/rules/go-gin/GN001-abort-after-response.md +75 -0
  38. package/skill-assets/sunlint-code-quality/rules/go-gin/GN002-request-context.md +64 -0
  39. package/skill-assets/sunlint-code-quality/rules/go-gin/GN003-bind-error-handling.md +70 -0
  40. package/skill-assets/sunlint-code-quality/rules/go-gin/GN004-dependency-injection.md +78 -0
  41. package/skill-assets/sunlint-code-quality/rules/go-gin/GN005-route-groups-middleware.md +71 -0
  42. package/skill-assets/sunlint-code-quality/rules/go-gin/GN006-http-status-codes.md +91 -0
  43. package/skill-assets/sunlint-code-quality/rules/go-gin/GN007-release-mode.md +64 -0
  44. package/skill-assets/sunlint-code-quality/rules/go-gin/GN008-struct-validation-tags.md +90 -0
  45. package/skill-assets/sunlint-code-quality/rules/go-gin/GN009-recovery-middleware.md +68 -0
  46. package/skill-assets/sunlint-code-quality/rules/go-gin/GN010-context-scope.md +68 -0
  47. package/skill-assets/sunlint-code-quality/rules/go-gin/GN011-middleware-concerns.md +92 -0
  48. package/skill-assets/sunlint-code-quality/rules/go-gin/GN012-no-log-sensitive.md +84 -0
  49. package/skill-assets/sunlint-code-quality/rules/java/J001-try-with-resources.md +86 -0
  50. package/skill-assets/sunlint-code-quality/rules/java/J002-equals-and-hashcode.md +88 -0
  51. package/skill-assets/sunlint-code-quality/rules/java/J003-string-comparison.md +72 -0
  52. package/skill-assets/sunlint-code-quality/rules/java/J004-use-java-time.md +91 -0
  53. package/skill-assets/sunlint-code-quality/rules/java/J005-no-print-stack-trace.md +80 -0
  54. package/skill-assets/sunlint-code-quality/rules/java/J006-no-system-println.md +89 -0
  55. package/skill-assets/sunlint-code-quality/rules/java/J007-proper-logger.md +91 -0
  56. package/skill-assets/sunlint-code-quality/rules/java/J008-thread-safe-singleton.md +119 -0
  57. package/skill-assets/sunlint-code-quality/rules/java/J009-utility-class-constructor.md +82 -0
  58. package/skill-assets/sunlint-code-quality/rules/java/J010-preserve-stack-trace.md +119 -0
  59. package/skill-assets/sunlint-code-quality/rules/java/J011-null-safe-compare.md +88 -0
  60. package/skill-assets/sunlint-code-quality/rules/java/J012-use-enum-collections.md +104 -0
  61. package/skill-assets/sunlint-code-quality/rules/java/J013-return-empty-not-null.md +102 -0
  62. package/skill-assets/sunlint-code-quality/rules/java/J014-hardcoded-crypto-key.md +108 -0
  63. package/skill-assets/sunlint-code-quality/rules/java/J015-optional-instead-of-null.md +109 -0
  64. package/skill-assets/sunlint-code-quality/rules/php-laravel/AGENTS.md +124 -0
  65. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV001-form-request-validation.md +64 -0
  66. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV002-eager-load-no-n-plus-1.md +58 -0
  67. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV003-config-not-env.md +54 -0
  68. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV004-fillable-mass-assignment.md +51 -0
  69. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV005-policies-gates-authorization.md +71 -0
  70. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV006-queue-heavy-tasks.md +68 -0
  71. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV007-hash-passwords.md +51 -0
  72. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV008-route-model-binding.md +67 -0
  73. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV009-api-resources.md +72 -0
  74. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV010-chunk-large-datasets.md +58 -0
  75. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV011-db-transactions.md +73 -0
  76. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV012-service-layer.md +78 -0
  77. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV013-testing-factories.md +75 -0
  78. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV014-service-container.md +61 -0
  79. package/skill-assets/sunlint-code-quality/rules/python/P001-mutable-default-argument.md +55 -0
  80. package/skill-assets/sunlint-code-quality/rules/python/P002-specify-file-encoding.md +45 -0
  81. package/skill-assets/sunlint-code-quality/rules/python/P003-context-manager-for-resources.md +54 -0
  82. package/skill-assets/sunlint-code-quality/rules/python/P004-no-bare-except.md +65 -0
  83. package/skill-assets/sunlint-code-quality/rules/python/P005-use-isinstance.md +60 -0
  84. package/skill-assets/sunlint-code-quality/rules/python/P006-timezone-aware-datetime.md +58 -0
  85. package/skill-assets/sunlint-code-quality/rules/python/P007-use-pathlib.md +62 -0
  86. package/skill-assets/sunlint-code-quality/rules/python/P008-no-wildcard-import.md +52 -0
  87. package/skill-assets/sunlint-code-quality/rules/python/P009-logging-lazy-format.md +50 -0
  88. package/skill-assets/sunlint-code-quality/rules/python/P010-exception-chaining.md +57 -0
  89. package/skill-assets/sunlint-code-quality/rules/python/P011-subprocess-check.md +59 -0
  90. package/skill-assets/sunlint-code-quality/rules/python/P012-requests-timeout.md +70 -0
  91. package/skill-assets/sunlint-code-quality/rules/python/P013-no-global-statement.md +73 -0
  92. package/skill-assets/sunlint-code-quality/rules/python/P014-no-modify-collection-while-iterating.md +66 -0
  93. package/skill-assets/sunlint-code-quality/rules/python/P015-prefer-fstrings.md +61 -0
  94. package/skill-assets/sunlint-code-quality/rules/ruby-rails/AGENTS.md +121 -0
  95. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR001-strong-parameters.md +55 -0
  96. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR002-eager-load-includes.md +51 -0
  97. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR003-service-objects.md +99 -0
  98. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR004-active-job-background.md +67 -0
  99. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR005-pagination.md +53 -0
  100. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR006-find-each-batches.md +53 -0
  101. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR007-http-status-codes.md +76 -0
  102. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR008-before-action-auth.md +77 -0
  103. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR009-rails-credentials.md +61 -0
  104. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR010-scopes.md +57 -0
  105. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR011-counter-cache.md +59 -0
  106. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR012-render-json-status.md +42 -0
  107. package/skill-assets/sunlint-code-quality/rules/swift/C006-verb-noun-functions.md +37 -0
  108. package/skill-assets/sunlint-code-quality/rules/swift/C013-no-dead-code.md +55 -0
  109. package/skill-assets/sunlint-code-quality/rules/swift/C014-dependency-injection.md +69 -0
  110. package/skill-assets/sunlint-code-quality/rules/swift/C017-no-constructor-logic.md +66 -0
  111. package/skill-assets/sunlint-code-quality/rules/swift/C018-generic-errors.md +64 -0
  112. package/skill-assets/sunlint-code-quality/rules/swift/C019-error-log-level.md +64 -0
  113. package/skill-assets/sunlint-code-quality/rules/swift/C020-no-unused-imports.md +47 -0
  114. package/skill-assets/sunlint-code-quality/rules/swift/C022-no-unused-variables.md +46 -0
  115. package/skill-assets/sunlint-code-quality/rules/swift/C023-no-duplicate-names.md +55 -0
  116. package/skill-assets/sunlint-code-quality/rules/swift/C024-centralize-constants.md +68 -0
  117. package/skill-assets/sunlint-code-quality/rules/swift/C029-catch-log-root-cause.md +69 -0
  118. package/skill-assets/sunlint-code-quality/rules/swift/C030-custom-error-classes.md +77 -0
  119. package/skill-assets/sunlint-code-quality/rules/swift/C033-separate-data-access.md +89 -0
  120. package/skill-assets/sunlint-code-quality/rules/swift/C035-error-context-logging.md +66 -0
  121. package/skill-assets/sunlint-code-quality/rules/swift/C041-no-hardcoded-secrets.md +65 -0
  122. package/skill-assets/sunlint-code-quality/rules/swift/C042-boolean-naming.md +60 -0
  123. package/skill-assets/sunlint-code-quality/rules/swift/C052-controller-parsing.md +67 -0
  124. package/skill-assets/sunlint-code-quality/rules/swift/C060-superclass-logic.md +95 -0
  125. package/skill-assets/sunlint-code-quality/rules/swift/C067-no-hardcoded-config.md +80 -0
  126. package/skill-assets/sunlint-code-quality/rules/swift/S003-sql-injection.md +65 -0
  127. package/skill-assets/sunlint-code-quality/rules/swift/S004-no-log-credentials.md +67 -0
  128. package/skill-assets/sunlint-code-quality/rules/swift/S005-server-authorization.md +73 -0
  129. package/skill-assets/sunlint-code-quality/rules/swift/S006-default-credentials.md +76 -0
  130. package/skill-assets/sunlint-code-quality/rules/swift/S007-output-encoding.md +96 -0
  131. package/skill-assets/sunlint-code-quality/rules/swift/S009-approved-crypto.md +86 -0
  132. package/skill-assets/sunlint-code-quality/rules/swift/S010-csprng.md +71 -0
  133. package/skill-assets/sunlint-code-quality/rules/swift/S011-insecure-deserialization.md +74 -0
  134. package/skill-assets/sunlint-code-quality/rules/swift/S012-secrets-management.md +81 -0
  135. package/skill-assets/sunlint-code-quality/rules/swift/S013-tls-connections.md +67 -0
  136. package/skill-assets/sunlint-code-quality/rules/swift/S017-parameterized-queries.md +86 -0
  137. package/skill-assets/sunlint-code-quality/rules/swift/S019-session-management.md +131 -0
  138. package/skill-assets/sunlint-code-quality/rules/swift/S020-kvc-injection.md +91 -0
  139. package/skill-assets/sunlint-code-quality/rules/swift/S025-input-validation.md +125 -0
  140. package/skill-assets/sunlint-code-quality/rules/swift/S029-brute-force-protection.md +120 -0
  141. package/skill-assets/sunlint-code-quality/rules/swift/S036-path-traversal.md +102 -0
  142. package/skill-assets/sunlint-code-quality/rules/swift/S039-tls-certificate-validation.md +109 -0
  143. package/skill-assets/sunlint-code-quality/rules/swift/S041-logout-invalidation.md +103 -0
  144. package/skill-assets/sunlint-code-quality/rules/swift/S043-password-hashing.md +116 -0
  145. package/skill-assets/sunlint-code-quality/rules/swift/S044-critical-changes-reauth.md +145 -0
  146. package/skill-assets/sunlint-code-quality/rules/swift/S045-debug-info-exposure.md +116 -0
  147. package/skill-assets/sunlint-code-quality/rules/swift/S046-unvalidated-redirect.md +140 -0
  148. package/skill-assets/sunlint-code-quality/rules/swift/S051-token-expiry.md +134 -0
  149. package/skill-assets/sunlint-code-quality/rules/swift/S053-jwt-validation.md +139 -0
  150. package/skill-assets/sunlint-code-quality/rules/swift/S059-background-snapshot-protection.md +113 -0
  151. package/skill-assets/sunlint-code-quality/rules/swift/S060-data-protection-api.md +106 -0
  152. package/skill-assets/sunlint-code-quality/rules/swift/S061-jailbreak-detection.md +132 -0
@@ -0,0 +1,71 @@
1
+ ---
2
+ title: "GN005 – Group Routes and Apply Middleware at Group Level"
3
+ impact: medium
4
+ impactDescription: "Registering middleware per-route duplicates code and makes it easy to miss a route; group-level middleware is exhaustive by design."
5
+ tags: [go, gin, routing, middleware]
6
+ ---
7
+
8
+ # GN005 – Group Routes and Apply Middleware at Group Level
9
+
10
+ ## Rule
11
+
12
+ Use `router.Group()` to organise routes by prefix and protection level. Apply authentication, logging, and rate-limiting middleware to the group — not individual routes.
13
+
14
+ ## Why
15
+
16
+ Per-route middleware registration (`r.GET("/users", authMiddleware, handler)`) requires the developer to remember to add middleware to every new route. A missed route is a security hole. Group-level middleware is applied to all current and future routes in the group automatically.
17
+
18
+ ## Wrong
19
+
20
+ ```go
21
+ r := gin.Default()
22
+
23
+ // ❌ Auth repeated on every route — easy to miss
24
+ r.GET("/users", authMiddleware, userHandler.List)
25
+ r.POST("/users", authMiddleware, userHandler.Create)
26
+ r.PUT("/users/:id", authMiddleware, userHandler.Update)
27
+ r.DELETE("/users/:id", authMiddleware, userHandler.Delete) // ❌ forgot auth on DELETE
28
+
29
+ // ❌ Mixed public and protected routes without groups — hard to audit
30
+ r.GET("/products", productHandler.List)
31
+ r.POST("/products", authMiddleware, adminMiddleware, productHandler.Create)
32
+ ```
33
+
34
+ ## Correct
35
+
36
+ ```go
37
+ r := gin.New()
38
+ r.Use(gin.Recovery(), requestLogger()) // global middleware
39
+
40
+ // Public routes — no auth
41
+ public := r.Group("/api/v1")
42
+ {
43
+ public.POST("/auth/login", authHandler.Login)
44
+ public.POST("/auth/register", authHandler.Register)
45
+ public.GET("/products", productHandler.List)
46
+ }
47
+
48
+ // Authenticated routes
49
+ authenticated := r.Group("/api/v1")
50
+ authenticated.Use(authMiddleware())
51
+ {
52
+ authenticated.GET("/users/me", userHandler.Profile)
53
+ authenticated.PUT("/users/me", userHandler.Update)
54
+ authenticated.GET("/orders", orderHandler.List)
55
+ authenticated.POST("/orders", orderHandler.Create)
56
+ }
57
+
58
+ // Admin-only routes
59
+ admin := r.Group("/api/v1/admin")
60
+ admin.Use(authMiddleware(), adminMiddleware())
61
+ {
62
+ admin.GET("/users", adminUserHandler.List)
63
+ admin.DELETE("/users/:id", adminUserHandler.Delete)
64
+ }
65
+ ```
66
+
67
+ ## Notes
68
+
69
+ - Use curly braces `{}` inside `Group()` to make the scope visually clear (they're just Go blocks, not syntactically required).
70
+ - Apply rate limiting at the group level for public endpoints to prevent brute force on `/auth/login`.
71
+ - Version your API with a group: `r.Group("/api/v1")` makes future versioning (`/api/v2`) straightforward.
@@ -0,0 +1,91 @@
1
+ ---
2
+ title: "GN006 – Return Correct HTTP Status Codes"
3
+ impact: medium
4
+ impactDescription: "Returning 200 for all responses breaks API clients, hides errors in monitoring, and makes retry logic impossible."
5
+ tags: [go, gin, api, http]
6
+ ---
7
+
8
+ # GN006 – Return Correct HTTP Status Codes
9
+
10
+ ## Rule
11
+
12
+ Always pass the correct HTTP status code to `c.JSON()`, `c.AbortWithStatusJSON()`, and `c.Status()`. Never return `200 OK` for errors, resource creation, or empty responses.
13
+
14
+ ## Why
15
+
16
+ HTTP semantics are the contract between your API and its clients. A `200` with `{"error": "not found"}` is invisible to load balancers, monitoring tools, and client SDKs. Correct status codes enable automatic retry logic, circuit breakers, and accurate error dashboards.
17
+
18
+ ## Wrong
19
+
20
+ ```go
21
+ func (h *UserHandler) Create(c *gin.Context) {
22
+ // ...
23
+ user, err := h.service.CreateUser(ctx, req)
24
+ if err != nil {
25
+ c.JSON(200, gin.H{"error": err.Error()}) // ❌ success code + error body
26
+ return
27
+ }
28
+ c.JSON(200, user) // ❌ should be 201 for newly created resource
29
+ }
30
+
31
+ func (h *UserHandler) Delete(c *gin.Context) {
32
+ h.service.DeleteUser(ctx, id)
33
+ c.JSON(200, gin.H{"message": "deleted"}) // ❌ should be 204 with no body
34
+ }
35
+ ```
36
+
37
+ ## Correct
38
+
39
+ ```go
40
+ import "net/http"
41
+
42
+ func (h *UserHandler) Create(c *gin.Context) {
43
+ var req CreateUserRequest
44
+ if err := c.ShouldBindJSON(&req); err != nil {
45
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()}) // 400
46
+ return
47
+ }
48
+ user, err := h.service.CreateUser(c.Request.Context(), req)
49
+ if err != nil {
50
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "creation failed"}) // 500
51
+ return
52
+ }
53
+ c.JSON(http.StatusCreated, user) // 201 ✅
54
+ }
55
+
56
+ func (h *UserHandler) GetUser(c *gin.Context) {
57
+ user, err := h.service.FindUserByID(c.Request.Context(), c.Param("id"))
58
+ if errors.Is(err, ErrNotFound) {
59
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "user not found"}) // 404
60
+ return
61
+ }
62
+ c.JSON(http.StatusOK, user) // 200
63
+ }
64
+
65
+ func (h *UserHandler) Delete(c *gin.Context) {
66
+ if err := h.service.DeleteUser(c.Request.Context(), c.Param("id")); err != nil {
67
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "not found"}) // 404
68
+ return
69
+ }
70
+ c.Status(http.StatusNoContent) // 204, no body ✅
71
+ }
72
+ ```
73
+
74
+ ## Status code reference
75
+
76
+ | Scenario | Code | Constant |
77
+ |---|---|---|
78
+ | Read success | 200 | `http.StatusOK` |
79
+ | Create success | 201 | `http.StatusCreated` |
80
+ | Delete / no body | 204 | `http.StatusNoContent` |
81
+ | Validation failed | 400 | `http.StatusBadRequest` |
82
+ | Not authenticated | 401 | `http.StatusUnauthorized` |
83
+ | Not authorized | 403 | `http.StatusForbidden` |
84
+ | Not found | 404 | `http.StatusNotFound` |
85
+ | Business rule violation | 422 | `http.StatusUnprocessableEntity` |
86
+ | Rate limited | 429 | `http.StatusTooManyRequests` |
87
+
88
+ ## Notes
89
+
90
+ - Use `net/http` constants — never magic numbers like `c.JSON(201, ...)`.
91
+ - Map service-layer sentinel errors (e.g. `ErrNotFound`, `ErrConflict`) to status codes in a central error-mapping function.
@@ -0,0 +1,64 @@
1
+ ---
2
+ title: "GN007 – Set gin.ReleaseMode in Production"
3
+ impact: medium
4
+ impactDescription: "The default gin.DebugMode prints every registered route and request detail to stdout, leaking your API surface and increasing log noise in production."
5
+ tags: [go, gin, security, configuration]
6
+ ---
7
+
8
+ # GN007 – Set `gin.ReleaseMode` in Production
9
+
10
+ ## Rule
11
+
12
+ Set `gin.SetMode(gin.ReleaseMode)` before calling `gin.New()` or `gin.Default()` in production builds. Read the mode from an environment variable so it can differ between environments.
13
+
14
+ ## Why
15
+
16
+ Debug mode outputs all routes on startup and logs detailed request info. In production this:
17
+ - Leaks your API surface to anyone with log access.
18
+ - Adds unnecessary CPU cost for string formatting.
19
+ - Makes it harder to find meaningful log entries.
20
+
21
+ ## Wrong
22
+
23
+ ```go
24
+ // main.go — mode not set, defaults to DebugMode
25
+ func main() {
26
+ r := gin.Default() // ❌ runs in debug mode, dumps all routes to stdout
27
+ r.GET("/internal/admin", adminHandler) // ❌ exposed in startup log
28
+ r.Run(":8080")
29
+ }
30
+
31
+ // Hardcoded mode
32
+ gin.SetMode(gin.DebugMode) // ❌ always debug, regardless of environment
33
+ ```
34
+
35
+ ## Correct
36
+
37
+ ```go
38
+ func main() {
39
+ // Read from environment — "release", "debug", or "test"
40
+ ginMode := os.Getenv("GIN_MODE")
41
+ if ginMode == "" {
42
+ ginMode = gin.ReleaseMode // safe default
43
+ }
44
+ gin.SetMode(ginMode)
45
+
46
+ r := gin.New()
47
+ r.Use(gin.Recovery()) // see GN009 for Recovery middleware
48
+ // ... register routes
49
+ r.Run(":" + os.Getenv("PORT"))
50
+ }
51
+ ```
52
+
53
+ Or rely on Gin's built-in env var support: Gin automatically reads `GIN_MODE` — set it to `release` in your container/systemd environment and `gin.SetMode` is not even needed explicitly.
54
+
55
+ ```bash
56
+ # Production container / systemd
57
+ GIN_MODE=release
58
+ ```
59
+
60
+ ## Notes
61
+
62
+ - `gin.Default()` adds `gin.Logger()` and `gin.Recovery()` automatically in debug mode — in production, use `gin.New()` and add your structured logger and Recovery explicitly (see GN009).
63
+ - Never call `gin.SetMode` after `gin.New()` — the mode must be set before the engine is created.
64
+ - In tests, set `gin.SetMode(gin.TestMode)` in `TestMain` to silence route debug output.
@@ -0,0 +1,90 @@
1
+ ---
2
+ title: "GN008 – Use Struct Validation Binding Tags"
3
+ impact: medium
4
+ impactDescription: "Manual if/else validation in handler code is incomplete, inconsistent, and untested; binding tags enforce validation at the deserialization boundary."
5
+ tags: [go, gin, validation, correctness]
6
+ ---
7
+
8
+ # GN008 – Use Struct Validation Binding Tags
9
+
10
+ ## Rule
11
+
12
+ Define all input validation rules using `binding:` struct tags on request structs. Do not write manual validation logic in handler functions. Handle binding errors as described in GN003.
13
+
14
+ ## Why
15
+
16
+ Gin uses the `go-playground/validator` library under the hood for `ShouldBind*`. Struct tags declare constraints once, close to the field, and are validated automatically when binding. Scattered `if req.Age < 18` checks in handlers are often incomplete and inconsistent.
17
+
18
+ ## Wrong
19
+
20
+ ```go
21
+ type CreateUserRequest struct {
22
+ Name string `json:"name"`
23
+ Email string `json:"email"`
24
+ Age int `json:"age"`
25
+ Role string `json:"role"`
26
+ }
27
+
28
+ // Handler with manual validation — verbose, incomplete, inconsistent
29
+ func (h *UserHandler) Create(c *gin.Context) {
30
+ var req CreateUserRequest
31
+ c.ShouldBindJSON(&req)
32
+
33
+ if req.Name == "" {
34
+ c.JSON(400, gin.H{"error": "name required"}) // ❌ still runs if bind errored
35
+ return
36
+ }
37
+ if !strings.Contains(req.Email, "@") { // ❌ incomplete email check
38
+ c.JSON(400, gin.H{"error": "invalid email"})
39
+ return
40
+ }
41
+ // ❌ forgot to validate Age and Role
42
+ }
43
+ ```
44
+
45
+ ## Correct
46
+
47
+ ```go
48
+ type CreateUserRequest struct {
49
+ Name string `json:"name" binding:"required,min=2,max=100"`
50
+ Email string `json:"email" binding:"required,email"`
51
+ Age int `json:"age" binding:"required,min=18,max=120"`
52
+ Role string `json:"role" binding:"required,oneof=user admin moderator"`
53
+ Phone string `json:"phone" binding:"omitempty,e164"` // optional but validated if present
54
+ Website string `json:"website" binding:"omitempty,url"`
55
+ }
56
+
57
+ func (h *UserHandler) Create(c *gin.Context) {
58
+ var req CreateUserRequest
59
+ if err := c.ShouldBindJSON(&req); err != nil {
60
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
61
+ "error": "validation failed",
62
+ "details": formatValidationErrors(err), // see note below
63
+ })
64
+ return
65
+ }
66
+ // req is fully validated here — safe to use
67
+ user, err := h.service.CreateUser(c.Request.Context(), req)
68
+ // ...
69
+ }
70
+ ```
71
+
72
+ ## Common binding tags
73
+
74
+ | Tag | Meaning |
75
+ |---|---|
76
+ | `required` | Field must be present and non-zero |
77
+ | `omitempty` | Skip validation if field is absent/zero |
78
+ | `min=N,max=M` | Min/max length for strings, min/max value for numbers |
79
+ | `email` | Valid email address format |
80
+ | `url` | Valid URL |
81
+ | `oneof=a b c` | Value must be one of the listed options |
82
+ | `uuid4` | Valid UUID v4 |
83
+ | `e164` | Valid international phone number |
84
+ | `gt=0` | Greater than 0 (useful for IDs) |
85
+
86
+ ## Notes
87
+
88
+ - To return human-readable errors, type-assert `err` to `validator.ValidationErrors` and format field names.
89
+ - Custom validators: `binding.Validator.RegisterValidation("custom_rule", myFunc)`.
90
+ - For query parameters: `c.ShouldBindQuery(&req)` with `form:"field" binding:"required"` tags.
@@ -0,0 +1,68 @@
1
+ ---
2
+ title: "GN009 – Add gin.Recovery() Middleware in Production"
3
+ impact: high
4
+ impactDescription: "Without Recovery(), an unhandled panic terminates the goroutine and returns a broken TCP connection or empty response to the client; with it, the server stays up and returns 500."
5
+ tags: [go, gin, reliability, middleware]
6
+ ---
7
+
8
+ # GN009 – Add `gin.Recovery()` Middleware in Production
9
+
10
+ ## Rule
11
+
12
+ Always include `gin.Recovery()` (or a custom recovery middleware) in the production middleware stack. Use `gin.New()` rather than `gin.Default()` and add middleware explicitly so you control what runs.
13
+
14
+ ## Why
15
+
16
+ Go panics propagate up the call stack and terminate the goroutine. In a Gin HTTP handler, an unrecovered panic kills the request-handling goroutine and the server returns a broken connection. `gin.Recovery()` catches the panic, logs a stack trace, and returns `500 Internal Server Error`, keeping the server alive.
17
+
18
+ ## Wrong
19
+
20
+ ```go
21
+ func main() {
22
+ r := gin.New() // ❌ no Recovery — a panic crashes the goroutine silently
23
+ r.GET("/users/:id", userHandler.Get)
24
+ r.Run(":8080")
25
+ }
26
+
27
+ // gin.Default() adds Logger + Recovery but also adds noise to structured log setups
28
+ r := gin.Default() // acceptable but not recommended for production structured logging
29
+ ```
30
+
31
+ ## Correct
32
+
33
+ ```go
34
+ func main() {
35
+ gin.SetMode(os.Getenv("GIN_MODE")) // see GN007
36
+
37
+ r := gin.New()
38
+
39
+ // Custom structured logger (replace gin.Logger())
40
+ r.Use(RequestLogger(logger))
41
+
42
+ // Recovery — MUST be registered before route handlers
43
+ r.Use(gin.RecoveryWithWriter(gin.DefaultErrorWriter))
44
+ // OR a custom recovery that logs stack traces to your logger:
45
+ r.Use(CustomRecovery(logger))
46
+
47
+ registerRoutes(r)
48
+ r.Run(":" + cfg.Port)
49
+ }
50
+
51
+ // Custom recovery middleware with structured logging
52
+ func CustomRecovery(logger *slog.Logger) gin.HandlerFunc {
53
+ return gin.CustomRecoveryWithWriter(gin.DefaultErrorWriter, func(c *gin.Context, recovered any) {
54
+ logger.Error("panic recovered",
55
+ slog.Any("error", recovered),
56
+ slog.String("path", c.Request.URL.Path),
57
+ slog.String("method", c.Request.Method),
58
+ )
59
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
60
+ })
61
+ }
62
+ ```
63
+
64
+ ## Notes
65
+
66
+ - Recovery middleware must be registered **before** route handlers — Gin runs middleware in registration order.
67
+ - Don't expose the panic value or stack trace in the HTTP response — log it server-side only.
68
+ - For observability, send panic events to your error tracker (Sentry, Rollbar) inside the custom recovery handler.
@@ -0,0 +1,68 @@
1
+ ---
2
+ title: "GN010 – Do Not Store gin.Context in Goroutines Beyond the Handler"
3
+ impact: high
4
+ impactDescription: "gin.Context is recycled after the handler returns; storing it in a goroutine creates a data race on pool-recycled memory."
5
+ tags: [go, gin, concurrency, correctness]
6
+ ---
7
+
8
+ # GN010 – Do Not Store `gin.Context` in Goroutines Beyond the Handler
9
+
10
+ ## Rule
11
+
12
+ Never pass `*gin.Context` to a goroutine that outlives the handler function. Copy the values you need (request data, context) before spawning the goroutine.
13
+
14
+ ## Why
15
+
16
+ Gin uses `sync.Pool` to reuse `*gin.Context` instances for performance. Once the handler function returns, Gin resets and recycles the context object. A goroutine holding a reference to the old `*gin.Context` now reads from a recycled object used by a completely different request — this is an undetected data race and a source of subtle, hard-to-reproduce bugs.
17
+
18
+ ## Wrong
19
+
20
+ ```go
21
+ func (h *NotificationHandler) Send(c *gin.Context) {
22
+ var req SendNotificationRequest
23
+ if err := c.ShouldBindJSON(&req); err != nil {
24
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
25
+ return
26
+ }
27
+
28
+ // ❌ goroutine holds *gin.Context — races after handler returns
29
+ go func() {
30
+ userID := c.GetString("user_id") // c is already recycled here
31
+ h.service.Notify(context.Background(), userID, req)
32
+ }()
33
+
34
+ c.JSON(http.StatusAccepted, gin.H{"status": "queued"})
35
+ }
36
+ ```
37
+
38
+ ## Correct
39
+
40
+ ```go
41
+ func (h *NotificationHandler) Send(c *gin.Context) {
42
+ var req SendNotificationRequest
43
+ if err := c.ShouldBindJSON(&req); err != nil {
44
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
45
+ return
46
+ }
47
+
48
+ // ✅ Copy values from context BEFORE spawning the goroutine
49
+ userID := c.GetString("user_id") // copy string value
50
+ ctx := c.Request.Context() // copy the stdlib context (safe to pass to goroutine)
51
+
52
+ go func() {
53
+ // Use copied values — no reference to *gin.Context
54
+ if err := h.service.Notify(ctx, userID, req); err != nil {
55
+ h.logger.Error("notification failed", slog.String("user_id", userID), slog.Any("error", err))
56
+ }
57
+ }()
58
+
59
+ c.JSON(http.StatusAccepted, gin.H{"status": "queued"})
60
+ }
61
+ ```
62
+
63
+ ## Notes
64
+
65
+ - `c.Request.Context()` is a standard `context.Context` backed by the HTTP request — safe to copy and pass to goroutines.
66
+ - Extract all required values (`c.Param()`, `c.GetHeader()`, `c.Get()`) into local variables before the goroutine.
67
+ - The Go race detector (`go test -race`) will catch this violation in tests if the goroutine runs fast enough — enable it in CI.
68
+ - Prefer `c.Copy()` if you absolutely need a snapshot of the whole context, but this is memory-heavier than copying specific fields.
@@ -0,0 +1,92 @@
1
+ ---
2
+ title: "GN011 – Apply Cross-Cutting Concerns via Middleware"
3
+ impact: medium
4
+ impactDescription: "Duplicating auth checks, rate limiting, or logging inside individual handlers makes enforcement inconsistent and maintenance expensive."
5
+ tags: [go, gin, middleware, architecture]
6
+ ---
7
+
8
+ # GN011 – Apply Cross-Cutting Concerns via Middleware
9
+
10
+ ## Rule
11
+
12
+ Authentication, authorization, rate limiting, request logging, request ID injection, and CORS must be implemented as Gin middleware functions and applied at the router/group level — not duplicated inside individual handlers.
13
+
14
+ ## Why
15
+
16
+ When authentication logic lives inside each handler, a single missed handler creates a security gap. Middleware ensures uniform enforcement. It also separates concerns: handlers contain only business logic; infrastructure concerns live in middleware.
17
+
18
+ ## Wrong
19
+
20
+ ```go
21
+ // ❌ Auth duplicated in every handler
22
+ func (h *OrderHandler) List(c *gin.Context) {
23
+ token := c.GetHeader("Authorization")
24
+ userID, err := h.auth.ValidateToken(token)
25
+ if err != nil {
26
+ c.AbortWithStatusJSON(401, gin.H{"error": "unauthorized"})
27
+ return
28
+ }
29
+ // ... business logic
30
+ }
31
+
32
+ func (h *OrderHandler) Create(c *gin.Context) {
33
+ token := c.GetHeader("Authorization") // ❌ duplicated
34
+ userID, err := h.auth.ValidateToken(token)
35
+ // ...
36
+ }
37
+ ```
38
+
39
+ ## Correct
40
+
41
+ ```go
42
+ // middleware/auth.go
43
+ func JWTAuth(tokenValidator TokenValidator) gin.HandlerFunc {
44
+ return func(c *gin.Context) {
45
+ token := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ")
46
+ claims, err := tokenValidator.Validate(token)
47
+ if err != nil {
48
+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
49
+ return
50
+ }
51
+ c.Set("user_id", claims.UserID)
52
+ c.Set("user_role", claims.Role)
53
+ c.Next()
54
+ }
55
+ }
56
+
57
+ // middleware/request_id.go
58
+ func RequestID() gin.HandlerFunc {
59
+ return func(c *gin.Context) {
60
+ id := c.GetHeader("X-Request-ID")
61
+ if id == "" {
62
+ id = uuid.New().String()
63
+ }
64
+ c.Set("request_id", id)
65
+ c.Header("X-Request-ID", id)
66
+ c.Next()
67
+ }
68
+ }
69
+
70
+ // main.go — applied at group level (see GN005)
71
+ r := gin.New()
72
+ r.Use(gin.Recovery(), RequestID(), StructuredLogger(logger))
73
+
74
+ api := r.Group("/api/v1")
75
+ api.Use(JWTAuth(tokenValidator))
76
+ api.Use(RateLimit(100, time.Minute))
77
+
78
+ api.GET("/orders", orderHandler.List) // auth + rate limit automatically applied
79
+ api.POST("/orders", orderHandler.Create)
80
+
81
+ // handlers are clean — no auth logic
82
+ func (h *OrderHandler) List(c *gin.Context) {
83
+ userID := c.GetString("user_id") // set by middleware
84
+ // ... just business logic
85
+ }
86
+ ```
87
+
88
+ ## Notes
89
+
90
+ - Middleware should only set values in the context and call `c.Next()` or `c.Abort()` — no response writing for non-auth middleware.
91
+ - Return factory functions (`func JWTAuth(dep Dep) gin.HandlerFunc`) so middleware is injectable and testable.
92
+ - Always document what a middleware sets in the context (e.g. `"user_id"`, `"request_id"`) so handlers know what keys are available.
@@ -0,0 +1,84 @@
1
+ ---
2
+ title: "GN012 – Do Not Log Request Bodies Containing Sensitive Data"
3
+ impact: high
4
+ impactDescription: "Logging raw request bodies exposes passwords, tokens, credit card numbers, and PII to anyone with log access, violating GDPR, PCI-DSS, and basic security hygiene."
5
+ tags: [go, gin, security, logging]
6
+ ---
7
+
8
+ # GN012 – Do Not Log Request Bodies Containing Sensitive Data
9
+
10
+ ## Rule
11
+
12
+ Never log the raw HTTP request body. Log only request metadata (method, path, status, duration, request ID). If you must log body fields for debugging, explicitly allowlist safe fields and mask or omit sensitive ones.
13
+
14
+ ## Why
15
+
16
+ Request bodies for auth endpoints contain passwords. Payment endpoints contain card data. Profile endpoints contain PII. Logging bodies stores this data in plaintext log files, log aggregators, and monitoring tools — where it lives far beyond the request lifetime and is often accessible to many people.
17
+
18
+ ## Wrong
19
+
20
+ ```go
21
+ // ❌ Logging the full request body
22
+ func RequestBodyLogger() gin.HandlerFunc {
23
+ return func(c *gin.Context) {
24
+ body, _ := io.ReadAll(c.Request.Body)
25
+ log.Printf("Request body: %s", body) // ❌ logs passwords, tokens, PII
26
+ c.Request.Body = io.NopCloser(bytes.NewBuffer(body))
27
+ c.Next()
28
+ }
29
+ }
30
+
31
+ // ❌ Logging bound request struct directly (may contain passwords)
32
+ func (h *AuthHandler) Login(c *gin.Context) {
33
+ var req LoginRequest
34
+ c.ShouldBindJSON(&req)
35
+ log.Printf("Login attempt: %+v", req) // ❌ req.Password logged in plaintext
36
+ }
37
+ ```
38
+
39
+ ## Correct
40
+
41
+ ```go
42
+ // ✅ Log only safe metadata — never raw body
43
+ func RequestLogger(logger *slog.Logger) gin.HandlerFunc {
44
+ return func(c *gin.Context) {
45
+ start := time.Now()
46
+ c.Next()
47
+ logger.Info("request",
48
+ slog.String("method", c.Request.Method),
49
+ slog.String("path", c.Request.URL.Path),
50
+ slog.Int("status", c.Writer.Status()),
51
+ slog.Duration("duration", time.Since(start)),
52
+ slog.String("ip", c.ClientIP()),
53
+ slog.String("request_id", c.GetString("request_id")),
54
+ // ✅ no body, no headers that might contain auth tokens
55
+ )
56
+ }
57
+ }
58
+
59
+ // ✅ Log only safe fields from struct - never password/token fields
60
+ func (h *AuthHandler) Login(c *gin.Context) {
61
+ var req LoginRequest
62
+ if err := c.ShouldBindJSON(&req); err != nil {
63
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
64
+ return
65
+ }
66
+ // ✅ Log only the email, not the password
67
+ h.logger.Info("login attempt", slog.String("email", req.Email))
68
+ // ...
69
+ }
70
+ ```
71
+
72
+ ## Sensitive fields to never log
73
+
74
+ - `password`, `password_confirmation`, `current_password`
75
+ - `token`, `access_token`, `refresh_token`, `api_key`, `secret`
76
+ - `card_number`, `cvv`, `expiry`
77
+ - `ssn`, `national_id`, `date_of_birth` (PII)
78
+ - `Authorization` header value
79
+
80
+ ## Notes
81
+
82
+ - If you need request body logging for debugging, create a separate debug middleware that is only enabled when `GIN_MODE=debug`.
83
+ - Use log scrubbing libraries (e.g., `go-sanitize`) as a last-resort safety net, but don't rely on them — fix the root cause.
84
+ - `c.Request.Header` can contain `Authorization: Bearer <token>` — log header names only, never values.