@sun-asterisk/sunlint 1.3.47 → 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 (185) hide show
  1. package/config/rules/rules-registry-generated.json +1717 -282
  2. package/core/architecture-integration.js +57 -15
  3. package/core/cli-action-handler.js +51 -36
  4. package/core/config-manager.js +6 -0
  5. package/core/config-merger.js +33 -0
  6. package/core/config-validator.js +37 -2
  7. package/core/file-targeting-service.js +148 -15
  8. package/core/init-command.js +118 -70
  9. package/core/output-service.js +12 -3
  10. package/core/project-detector.js +517 -0
  11. package/core/scoring-service.js +12 -6
  12. package/core/summary-report-service.js +9 -4
  13. package/core/tui-select.js +245 -0
  14. package/engines/arch-detect/rules/layered/l001-presentation-layer.js +7 -15
  15. package/engines/arch-detect/rules/layered/l002-business-layer.js +7 -15
  16. package/engines/arch-detect/rules/layered/l003-data-layer.js +7 -15
  17. package/engines/arch-detect/rules/layered/l004-model-layer.js +7 -15
  18. package/engines/arch-detect/rules/layered/l005-layer-separation.js +22 -2
  19. package/engines/arch-detect/rules/layered/l006-dependency-direction.js +8 -5
  20. package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +67 -29
  21. package/engines/arch-detect/rules/presentation/pr001-view-layer.js +16 -9
  22. package/engines/arch-detect/rules/presentation/pr006-router-layer.js +33 -8
  23. package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +35 -6
  24. package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +56 -10
  25. package/engines/impact/cli.js +54 -39
  26. package/engines/impact/config/default-config.js +105 -5
  27. package/engines/impact/core/impact-analyzer.js +12 -15
  28. package/engines/impact/core/utils/gitignore-parser.js +123 -0
  29. package/engines/impact/core/utils/method-call-graph.js +272 -87
  30. package/origin-rules/dart-en.md +1 -1
  31. package/origin-rules/go-en.md +231 -0
  32. package/origin-rules/php-en.md +107 -0
  33. package/origin-rules/python-en.md +113 -0
  34. package/origin-rules/ruby-en.md +607 -0
  35. package/package.json +1 -1
  36. package/scripts/copy-arch-detect.js +5 -1
  37. package/scripts/copy-impact-analyzer.js +5 -1
  38. package/scripts/generate-rules-registry.js +30 -14
  39. package/skill-assets/sunlint-code-quality/SKILL.md +3 -2
  40. package/skill-assets/sunlint-code-quality/rules/dart/C006-verb-noun-functions.md +45 -0
  41. package/skill-assets/sunlint-code-quality/rules/dart/C013-no-dead-code.md +53 -0
  42. package/skill-assets/sunlint-code-quality/rules/dart/C014-dependency-injection.md +92 -0
  43. package/skill-assets/sunlint-code-quality/rules/dart/C017-no-constructor-logic.md +62 -0
  44. package/skill-assets/sunlint-code-quality/rules/dart/C018-generic-errors.md +57 -0
  45. package/skill-assets/sunlint-code-quality/rules/dart/C019-error-log-level.md +50 -0
  46. package/skill-assets/sunlint-code-quality/rules/dart/C020-no-unused-imports.md +46 -0
  47. package/skill-assets/sunlint-code-quality/rules/dart/C022-no-unused-variables.md +50 -0
  48. package/skill-assets/sunlint-code-quality/rules/dart/C023-no-duplicate-names.md +56 -0
  49. package/skill-assets/sunlint-code-quality/rules/dart/C024-centralize-constants.md +75 -0
  50. package/skill-assets/sunlint-code-quality/rules/dart/C029-catch-log-root-cause.md +53 -0
  51. package/skill-assets/sunlint-code-quality/rules/dart/C030-custom-error-classes.md +86 -0
  52. package/skill-assets/sunlint-code-quality/rules/dart/C033-separate-data-access.md +90 -0
  53. package/skill-assets/sunlint-code-quality/rules/dart/C035-error-context-logging.md +62 -0
  54. package/skill-assets/sunlint-code-quality/rules/dart/C041-no-hardcoded-secrets.md +75 -0
  55. package/skill-assets/sunlint-code-quality/rules/dart/C042-boolean-naming.md +73 -0
  56. package/skill-assets/sunlint-code-quality/rules/dart/C052-widget-parsing.md +84 -0
  57. package/skill-assets/sunlint-code-quality/rules/dart/C060-superclass-logic.md +91 -0
  58. package/skill-assets/sunlint-code-quality/rules/dart/C067-no-hardcoded-config.md +108 -0
  59. package/skill-assets/sunlint-code-quality/rules/go/G001-explicit-error-handling.md +53 -0
  60. package/skill-assets/sunlint-code-quality/rules/go/G002-context-first-argument.md +44 -0
  61. package/skill-assets/sunlint-code-quality/rules/go/G003-receiver-consistency.md +38 -0
  62. package/skill-assets/sunlint-code-quality/rules/go/G004-avoid-panic.md +49 -0
  63. package/skill-assets/sunlint-code-quality/rules/go/G005-goroutine-leak-prevention.md +49 -0
  64. package/skill-assets/sunlint-code-quality/rules/go/G006-interface-consumer-definition.md +45 -0
  65. package/skill-assets/sunlint-code-quality/rules/go/GN001-gin-binding-validation.md +57 -0
  66. package/skill-assets/sunlint-code-quality/rules/go/GN002-gin-error-response.md +48 -0
  67. package/skill-assets/sunlint-code-quality/rules/go/GN003-graceful-shutdown.md +57 -0
  68. package/skill-assets/sunlint-code-quality/rules/go/GN004-gin-route-logical-grouping.md +54 -0
  69. package/skill-assets/sunlint-code-quality/rules/go-gin/AGENTS.md +149 -0
  70. package/skill-assets/sunlint-code-quality/rules/go-gin/GN001-abort-after-response.md +75 -0
  71. package/skill-assets/sunlint-code-quality/rules/go-gin/GN002-request-context.md +64 -0
  72. package/skill-assets/sunlint-code-quality/rules/go-gin/GN003-bind-error-handling.md +70 -0
  73. package/skill-assets/sunlint-code-quality/rules/go-gin/GN004-dependency-injection.md +78 -0
  74. package/skill-assets/sunlint-code-quality/rules/go-gin/GN005-route-groups-middleware.md +71 -0
  75. package/skill-assets/sunlint-code-quality/rules/go-gin/GN006-http-status-codes.md +91 -0
  76. package/skill-assets/sunlint-code-quality/rules/go-gin/GN007-release-mode.md +64 -0
  77. package/skill-assets/sunlint-code-quality/rules/go-gin/GN008-struct-validation-tags.md +90 -0
  78. package/skill-assets/sunlint-code-quality/rules/go-gin/GN009-recovery-middleware.md +68 -0
  79. package/skill-assets/sunlint-code-quality/rules/go-gin/GN010-context-scope.md +68 -0
  80. package/skill-assets/sunlint-code-quality/rules/go-gin/GN011-middleware-concerns.md +92 -0
  81. package/skill-assets/sunlint-code-quality/rules/go-gin/GN012-no-log-sensitive.md +84 -0
  82. package/skill-assets/sunlint-code-quality/rules/java/J001-try-with-resources.md +86 -0
  83. package/skill-assets/sunlint-code-quality/rules/java/J002-equals-and-hashcode.md +88 -0
  84. package/skill-assets/sunlint-code-quality/rules/java/J003-string-comparison.md +72 -0
  85. package/skill-assets/sunlint-code-quality/rules/java/J004-use-java-time.md +91 -0
  86. package/skill-assets/sunlint-code-quality/rules/java/J005-no-print-stack-trace.md +80 -0
  87. package/skill-assets/sunlint-code-quality/rules/java/J006-no-system-println.md +89 -0
  88. package/skill-assets/sunlint-code-quality/rules/java/J007-proper-logger.md +91 -0
  89. package/skill-assets/sunlint-code-quality/rules/java/J008-thread-safe-singleton.md +119 -0
  90. package/skill-assets/sunlint-code-quality/rules/java/J009-utility-class-constructor.md +82 -0
  91. package/skill-assets/sunlint-code-quality/rules/java/J010-preserve-stack-trace.md +119 -0
  92. package/skill-assets/sunlint-code-quality/rules/java/J011-null-safe-compare.md +88 -0
  93. package/skill-assets/sunlint-code-quality/rules/java/J012-use-enum-collections.md +104 -0
  94. package/skill-assets/sunlint-code-quality/rules/java/J013-return-empty-not-null.md +102 -0
  95. package/skill-assets/sunlint-code-quality/rules/java/J014-hardcoded-crypto-key.md +108 -0
  96. package/skill-assets/sunlint-code-quality/rules/java/J015-optional-instead-of-null.md +109 -0
  97. package/skill-assets/sunlint-code-quality/rules/php-laravel/AGENTS.md +124 -0
  98. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV001-form-request-validation.md +64 -0
  99. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV002-eager-load-no-n-plus-1.md +58 -0
  100. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV003-config-not-env.md +54 -0
  101. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV004-fillable-mass-assignment.md +51 -0
  102. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV005-policies-gates-authorization.md +71 -0
  103. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV006-queue-heavy-tasks.md +68 -0
  104. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV007-hash-passwords.md +51 -0
  105. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV008-route-model-binding.md +67 -0
  106. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV009-api-resources.md +72 -0
  107. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV010-chunk-large-datasets.md +58 -0
  108. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV011-db-transactions.md +73 -0
  109. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV012-service-layer.md +78 -0
  110. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV013-testing-factories.md +75 -0
  111. package/skill-assets/sunlint-code-quality/rules/php-laravel/LV014-service-container.md +61 -0
  112. package/skill-assets/sunlint-code-quality/rules/python/P001-mutable-default-argument.md +55 -0
  113. package/skill-assets/sunlint-code-quality/rules/python/P002-specify-file-encoding.md +45 -0
  114. package/skill-assets/sunlint-code-quality/rules/python/P003-context-manager-for-resources.md +54 -0
  115. package/skill-assets/sunlint-code-quality/rules/python/P004-no-bare-except.md +65 -0
  116. package/skill-assets/sunlint-code-quality/rules/python/P005-use-isinstance.md +60 -0
  117. package/skill-assets/sunlint-code-quality/rules/python/P006-timezone-aware-datetime.md +58 -0
  118. package/skill-assets/sunlint-code-quality/rules/python/P007-use-pathlib.md +62 -0
  119. package/skill-assets/sunlint-code-quality/rules/python/P008-no-wildcard-import.md +52 -0
  120. package/skill-assets/sunlint-code-quality/rules/python/P009-logging-lazy-format.md +50 -0
  121. package/skill-assets/sunlint-code-quality/rules/python/P010-exception-chaining.md +57 -0
  122. package/skill-assets/sunlint-code-quality/rules/python/P011-subprocess-check.md +59 -0
  123. package/skill-assets/sunlint-code-quality/rules/python/P012-requests-timeout.md +70 -0
  124. package/skill-assets/sunlint-code-quality/rules/python/P013-no-global-statement.md +73 -0
  125. package/skill-assets/sunlint-code-quality/rules/python/P014-no-modify-collection-while-iterating.md +66 -0
  126. package/skill-assets/sunlint-code-quality/rules/python/P015-prefer-fstrings.md +61 -0
  127. package/skill-assets/sunlint-code-quality/rules/ruby-rails/AGENTS.md +121 -0
  128. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR001-strong-parameters.md +55 -0
  129. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR002-eager-load-includes.md +51 -0
  130. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR003-service-objects.md +99 -0
  131. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR004-active-job-background.md +67 -0
  132. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR005-pagination.md +53 -0
  133. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR006-find-each-batches.md +53 -0
  134. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR007-http-status-codes.md +76 -0
  135. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR008-before-action-auth.md +77 -0
  136. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR009-rails-credentials.md +61 -0
  137. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR010-scopes.md +57 -0
  138. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR011-counter-cache.md +59 -0
  139. package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR012-render-json-status.md +42 -0
  140. package/skill-assets/sunlint-code-quality/rules/swift/C006-verb-noun-functions.md +37 -0
  141. package/skill-assets/sunlint-code-quality/rules/swift/C013-no-dead-code.md +55 -0
  142. package/skill-assets/sunlint-code-quality/rules/swift/C014-dependency-injection.md +69 -0
  143. package/skill-assets/sunlint-code-quality/rules/swift/C017-no-constructor-logic.md +66 -0
  144. package/skill-assets/sunlint-code-quality/rules/swift/C018-generic-errors.md +64 -0
  145. package/skill-assets/sunlint-code-quality/rules/swift/C019-error-log-level.md +64 -0
  146. package/skill-assets/sunlint-code-quality/rules/swift/C020-no-unused-imports.md +47 -0
  147. package/skill-assets/sunlint-code-quality/rules/swift/C022-no-unused-variables.md +46 -0
  148. package/skill-assets/sunlint-code-quality/rules/swift/C023-no-duplicate-names.md +55 -0
  149. package/skill-assets/sunlint-code-quality/rules/swift/C024-centralize-constants.md +68 -0
  150. package/skill-assets/sunlint-code-quality/rules/swift/C029-catch-log-root-cause.md +69 -0
  151. package/skill-assets/sunlint-code-quality/rules/swift/C030-custom-error-classes.md +77 -0
  152. package/skill-assets/sunlint-code-quality/rules/swift/C033-separate-data-access.md +89 -0
  153. package/skill-assets/sunlint-code-quality/rules/swift/C035-error-context-logging.md +66 -0
  154. package/skill-assets/sunlint-code-quality/rules/swift/C041-no-hardcoded-secrets.md +65 -0
  155. package/skill-assets/sunlint-code-quality/rules/swift/C042-boolean-naming.md +60 -0
  156. package/skill-assets/sunlint-code-quality/rules/swift/C052-controller-parsing.md +67 -0
  157. package/skill-assets/sunlint-code-quality/rules/swift/C060-superclass-logic.md +95 -0
  158. package/skill-assets/sunlint-code-quality/rules/swift/C067-no-hardcoded-config.md +80 -0
  159. package/skill-assets/sunlint-code-quality/rules/swift/S003-sql-injection.md +65 -0
  160. package/skill-assets/sunlint-code-quality/rules/swift/S004-no-log-credentials.md +67 -0
  161. package/skill-assets/sunlint-code-quality/rules/swift/S005-server-authorization.md +73 -0
  162. package/skill-assets/sunlint-code-quality/rules/swift/S006-default-credentials.md +76 -0
  163. package/skill-assets/sunlint-code-quality/rules/swift/S007-output-encoding.md +96 -0
  164. package/skill-assets/sunlint-code-quality/rules/swift/S009-approved-crypto.md +86 -0
  165. package/skill-assets/sunlint-code-quality/rules/swift/S010-csprng.md +71 -0
  166. package/skill-assets/sunlint-code-quality/rules/swift/S011-insecure-deserialization.md +74 -0
  167. package/skill-assets/sunlint-code-quality/rules/swift/S012-secrets-management.md +81 -0
  168. package/skill-assets/sunlint-code-quality/rules/swift/S013-tls-connections.md +67 -0
  169. package/skill-assets/sunlint-code-quality/rules/swift/S017-parameterized-queries.md +86 -0
  170. package/skill-assets/sunlint-code-quality/rules/swift/S019-session-management.md +131 -0
  171. package/skill-assets/sunlint-code-quality/rules/swift/S020-kvc-injection.md +91 -0
  172. package/skill-assets/sunlint-code-quality/rules/swift/S025-input-validation.md +125 -0
  173. package/skill-assets/sunlint-code-quality/rules/swift/S029-brute-force-protection.md +120 -0
  174. package/skill-assets/sunlint-code-quality/rules/swift/S036-path-traversal.md +102 -0
  175. package/skill-assets/sunlint-code-quality/rules/swift/S039-tls-certificate-validation.md +109 -0
  176. package/skill-assets/sunlint-code-quality/rules/swift/S041-logout-invalidation.md +103 -0
  177. package/skill-assets/sunlint-code-quality/rules/swift/S043-password-hashing.md +116 -0
  178. package/skill-assets/sunlint-code-quality/rules/swift/S044-critical-changes-reauth.md +145 -0
  179. package/skill-assets/sunlint-code-quality/rules/swift/S045-debug-info-exposure.md +116 -0
  180. package/skill-assets/sunlint-code-quality/rules/swift/S046-unvalidated-redirect.md +140 -0
  181. package/skill-assets/sunlint-code-quality/rules/swift/S051-token-expiry.md +134 -0
  182. package/skill-assets/sunlint-code-quality/rules/swift/S053-jwt-validation.md +139 -0
  183. package/skill-assets/sunlint-code-quality/rules/swift/S059-background-snapshot-protection.md +113 -0
  184. package/skill-assets/sunlint-code-quality/rules/swift/S060-data-protection-api.md +106 -0
  185. package/skill-assets/sunlint-code-quality/rules/swift/S061-jailbreak-detection.md +132 -0
@@ -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.
@@ -0,0 +1,86 @@
1
+ ---
2
+ title: Always Use try-with-resources for AutoCloseable
3
+ impact: HIGH
4
+ impactDescription: prevents resource leaks by ensuring streams, connections, and other closeable resources are always closed
5
+ tags: resource-management, best-practice, java, error-prone
6
+ ---
7
+
8
+ ## Always Use try-with-resources for AutoCloseable
9
+
10
+ Since Java 7, the `try`-with-resources statement automatically closes any resource that implements `AutoCloseable` or `Closeable`. Using manual `finally` blocks to close resources is verbose, error-prone, and can silently swallow exceptions thrown during close.
11
+
12
+ **Incorrect (manual finally block):**
13
+
14
+ ```java
15
+ public void readFile(String path) throws IOException {
16
+ InputStream in = null;
17
+ try {
18
+ in = new FileInputStream(path);
19
+ // process stream
20
+ int b = in.read();
21
+ } catch (IOException e) {
22
+ logger.error("Failed to read file", e);
23
+ throw e;
24
+ } finally {
25
+ if (in != null) {
26
+ try {
27
+ in.close(); // exception here swallows the original
28
+ } catch (IOException ignored) {}
29
+ }
30
+ }
31
+ }
32
+
33
+ public void queryDatabase(Connection conn) throws SQLException {
34
+ Statement stmt = null;
35
+ ResultSet rs = null;
36
+ try {
37
+ stmt = conn.createStatement();
38
+ rs = stmt.executeQuery("SELECT * FROM users");
39
+ } finally {
40
+ if (rs != null) rs.close(); // may throw, hiding original
41
+ if (stmt != null) stmt.close();
42
+ }
43
+ }
44
+ ```
45
+
46
+ **Correct (try-with-resources):**
47
+
48
+ ```java
49
+ public void readFile(String path) throws IOException {
50
+ try (InputStream in = new FileInputStream(path)) {
51
+ // process stream
52
+ int b = in.read();
53
+ }
54
+ // in is automatically closed even if an exception occurs
55
+ }
56
+
57
+ public void queryDatabase(Connection conn) throws SQLException {
58
+ try (Statement stmt = conn.createStatement();
59
+ ResultSet rs = stmt.executeQuery("SELECT * FROM users")) {
60
+ while (rs.next()) {
61
+ // process results
62
+ }
63
+ }
64
+ // both stmt and rs are closed in reverse declaration order
65
+ }
66
+
67
+ // Also works with custom AutoCloseable resources
68
+ public void processResource() throws Exception {
69
+ try (MyService service = new MyService()) {
70
+ service.execute();
71
+ }
72
+ }
73
+ ```
74
+
75
+ **Key benefits:**
76
+ - Resources are closed in reverse declaration order, automatically.
77
+ - Original exceptions are never swallowed by close failures (suppressed exceptions).
78
+ - Cleaner, less boilerplate code.
79
+
80
+ **Resources that must use try-with-resources:**
81
+ - `InputStream` / `OutputStream` / `Reader` / `Writer`
82
+ - `Connection` / `Statement` / `ResultSet` (JDBC)
83
+ - `HttpClient` / `Socket`
84
+ - Any class implementing `AutoCloseable`
85
+
86
+ **Tools:** PMD (`UseTryWithResources`), SonarQube (`S2093`), IntelliJ Inspections
@@ -0,0 +1,88 @@
1
+ ---
2
+ title: Override equals() and hashCode() Together
3
+ impact: HIGH
4
+ impactDescription: violating the equals-hashCode contract breaks HashMap, HashSet, and other hash-based collections silently
5
+ tags: correctness, contract, java, error-prone
6
+ ---
7
+
8
+ ## Override equals() and hashCode() Together
9
+
10
+ In Java, `equals()` and `hashCode()` share a contract: objects that are equal (via `equals()`) **must** have the same hash code. If you override one without overriding the other, hash-based collections (`HashMap`, `HashSet`, `Hashtable`) will malfunction — objects may become unreachable or duplicates may appear.
11
+
12
+ **Incorrect (only overrides equals):**
13
+
14
+ ```java
15
+ public class User {
16
+ private Long id;
17
+ private String email;
18
+
19
+ @Override
20
+ public boolean equals(Object o) {
21
+ if (this == o) return true;
22
+ if (!(o instanceof User)) return false;
23
+ User user = (User) o;
24
+ return Objects.equals(id, user.id);
25
+ }
26
+ // Missing hashCode! — HashSet will treat two equal Users as different objects
27
+ }
28
+
29
+ // Result: map.put(user1); map.get(user2) returns null even if user1.equals(user2)
30
+ Set<User> set = new HashSet<>();
31
+ set.add(new User(1L, "a@b.com"));
32
+ set.contains(new User(1L, "a@b.com")); // false! Bug!
33
+ ```
34
+
35
+ **Incorrect (only overrides hashCode):**
36
+
37
+ ```java
38
+ public class Order {
39
+ private Long id;
40
+
41
+ @Override
42
+ public int hashCode() {
43
+ return Objects.hash(id);
44
+ }
45
+ // Missing equals! — equals uses identity (==) by default
46
+ }
47
+ ```
48
+
49
+ **Correct (both overridden consistently):**
50
+
51
+ ```java
52
+ public class User {
53
+ private Long id;
54
+ private String email;
55
+
56
+ @Override
57
+ public boolean equals(Object o) {
58
+ if (this == o) return true;
59
+ if (!(o instanceof User)) return false;
60
+ User user = (User) o;
61
+ return Objects.equals(id, user.id);
62
+ }
63
+
64
+ @Override
65
+ public int hashCode() {
66
+ return Objects.hash(id); // same fields as equals
67
+ }
68
+ }
69
+
70
+ // Using records (Java 16+): equals and hashCode are auto-generated
71
+ public record User(Long id, String email) {}
72
+
73
+ // Using Lombok:
74
+ @EqualsAndHashCode(onlyExplicitlyIncluded = true)
75
+ public class User {
76
+ @EqualsAndHashCode.Include
77
+ private Long id;
78
+ private String email;
79
+ }
80
+ ```
81
+
82
+ **Rules to follow:**
83
+ - Use the **same fields** in both `equals()` and `hashCode()`.
84
+ - Prefer `Objects.equals()` and `Objects.hash()` over manual null checks.
85
+ - Consider using Lombok `@EqualsAndHashCode` or Java Records for value objects.
86
+ - Never include mutable fields in `hashCode()` if the object will be stored in a hash collection.
87
+
88
+ **Tools:** PMD (`OverrideBothEqualsAndHashcode`), Checkstyle (`EqualsHashCode`), IntelliJ Inspections
@@ -0,0 +1,72 @@
1
+ ---
2
+ title: Use equals() for String and Object Comparison, Not ==
3
+ impact: HIGH
4
+ impactDescription: using == compares references, not values, causing silent bugs that only appear at runtime
5
+ tags: correctness, java, error-prone, string
6
+ ---
7
+
8
+ ## Use equals() for String and Object Comparison, Not ==
9
+
10
+ In Java, `==` checks **reference equality** (are these the same object in memory?). For Strings and other objects, you almost always want **value equality** — use `.equals()`. While string literals may be interned (sharing references), strings from variables, method returns, or `new String(...)` will have different references, making `==` unreliable.
11
+
12
+ **Incorrect (using == for string comparison):**
13
+
14
+ ```java
15
+ String status = getStatusFromDatabase(); // returns "ACTIVE"
16
+ if (status == "ACTIVE") { // BUG: may be false even when value is "ACTIVE"
17
+ activateUser();
18
+ }
19
+
20
+ String s1 = new String("hello");
21
+ String s2 = new String("hello");
22
+ if (s1 == s2) { // always false — different objects
23
+ // never executed
24
+ }
25
+
26
+ public boolean isAdmin(String role) {
27
+ return role == "ADMIN"; // unreliable
28
+ }
29
+ ```
30
+
31
+ **Correct (using equals for string comparison):**
32
+
33
+ ```java
34
+ String status = getStatusFromDatabase();
35
+ if ("ACTIVE".equals(status)) { // null-safe: won't NPE if status is null
36
+ activateUser();
37
+ }
38
+
39
+ // Or with Objects.equals for null-safety on both sides:
40
+ if (Objects.equals(status, "ACTIVE")) {
41
+ activateUser();
42
+ }
43
+
44
+ public boolean isAdmin(String role) {
45
+ return "ADMIN".equals(role); // preferred: literal first to avoid NPE
46
+ // or: return role != null && role.equals("ADMIN");
47
+ }
48
+
49
+ // For enums: use == (enums are singletons, == is correct and preferred)
50
+ if (status == Status.ACTIVE) { // correct for enums
51
+ activateUser();
52
+ }
53
+ ```
54
+
55
+ **Literal-first pattern:**
56
+
57
+ Placing the literal on the left side of `equals()` is a common defensive pattern — if the variable is `null`, the call returns `false` instead of throwing `NullPointerException`:
58
+
59
+ ```java
60
+ // Risky: may throw NPE if name is null
61
+ if (name.equals("John")) { ... }
62
+
63
+ // Safe: returns false if name is null
64
+ if ("John".equals(name)) { ... }
65
+ ```
66
+
67
+ **Key distinctions:**
68
+ - `String` and all objects: use `.equals()` or `Objects.equals()`
69
+ - `enum` types: use `==` (enums are singleton instances)
70
+ - Primitives (`int`, `boolean`, etc.): use `==`
71
+
72
+ **Tools:** PMD (`CompareObjectsWithEquals`, `UseEqualsToCompareStrings`), FindBugs/SpotBugs (`ES_COMPARING_STRINGS_WITH_EQ`), IntelliJ Inspections