@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,89 @@
1
+ ---
2
+ title: Tách biệt logic xử lý và truy cập dữ liệu trong tầng service
3
+ impact: HIGH
4
+ impactDescription: Kết hợp logic nghiệp vụ và data access trong cùng một class làm khó test, khó thay đổi storage layer và vi phạm Single Responsibility.
5
+ tags: swift, ios, architecture, repository-pattern, service, code-quality
6
+ ---
7
+
8
+ ## Tách biệt logic xử lý và truy cập dữ liệu trong tầng service
9
+
10
+ ViewModel/Presenter không nên gọi trực tiếp Core Data, UserDefaults, hay network. Tách thành **Repository** (data access) và **UseCase/Service** (business logic). Điều này tuân theo kiến trúc Clean Architecture được Apple khuyến nghị.
11
+
12
+ **Incorrect (trộn lẫn nghiệp vụ và data access):**
13
+
14
+ ```swift
15
+ class OrderViewModel {
16
+ func placeOrder(items: [CartItem], userId: String) {
17
+ // Gọi API trực tiếp trong ViewModel
18
+ let request = URLRequest(url: URL(string: "https://api.example.com/orders")!)
19
+ URLSession.shared.dataTask(with: request) { data, _, error in
20
+ // Xử lý JSON thủ công trong ViewModel
21
+ guard let data = data,
22
+ let order = try? JSONDecoder().decode(Order.self, from: data) else { return }
23
+
24
+ // Lưu Core Data trực tiếp trong ViewModel
25
+ let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
26
+ let entity = OrderEntity(context: context)
27
+ entity.id = order.id
28
+ try? context.save()
29
+ }.resume()
30
+ }
31
+ }
32
+ ```
33
+
34
+ **Correct (Repository + UseCase tách biệt):**
35
+
36
+ ```swift
37
+ // Repository: chỉ lo data access
38
+ protocol OrderRepositoryProtocol {
39
+ func createOrder(request: CreateOrderRequest) async throws -> Order
40
+ func saveOrderLocally(_ order: Order) throws
41
+ }
42
+
43
+ class OrderRepository: OrderRepositoryProtocol {
44
+ private let apiClient: APIClientProtocol
45
+ private let coreDataManager: CoreDataManagerProtocol
46
+
47
+ init(apiClient: APIClientProtocol, coreDataManager: CoreDataManagerProtocol) {
48
+ self.apiClient = apiClient
49
+ self.coreDataManager = coreDataManager
50
+ }
51
+
52
+ func createOrder(request: CreateOrderRequest) async throws -> Order {
53
+ return try await apiClient.post("/orders", body: request)
54
+ }
55
+
56
+ func saveOrderLocally(_ order: Order) throws {
57
+ try coreDataManager.insert(order, entityType: OrderEntity.self)
58
+ }
59
+ }
60
+
61
+ // UseCase: chỉ lo nghiệp vụ
62
+ class PlaceOrderUseCase {
63
+ private let orderRepository: OrderRepositoryProtocol
64
+ private let inventoryService: InventoryServiceProtocol
65
+
66
+ func execute(items: [CartItem], userId: String) async throws -> Order {
67
+ // Kiểm tra tồn kho
68
+ try await inventoryService.validateAvailability(items: items)
69
+ let request = CreateOrderRequest(items: items, userId: userId)
70
+ let order = try await orderRepository.createOrder(request: request)
71
+ try orderRepository.saveOrderLocally(order)
72
+ return order
73
+ }
74
+ }
75
+
76
+ // ViewModel: chỉ gọi UseCase
77
+ class OrderViewModel {
78
+ private let placeOrderUseCase: PlaceOrderUseCase
79
+
80
+ func placeOrder(items: [CartItem]) async {
81
+ do {
82
+ order = try await placeOrderUseCase.execute(items: items, userId: currentUserId)
83
+ } catch { /* handle */ }
84
+ }
85
+ }
86
+ ```
87
+
88
+ **Tools:** Code Review, Unit Tests (mock repository dễ dàng)
89
+
@@ -0,0 +1,66 @@
1
+ ---
2
+ title: Log đầy đủ context khi xử lý lỗi
3
+ impact: HIGH
4
+ impactDescription: Thiếu context trong log làm mất khả năng correlate sự kiện, không tái hiện được lỗi production và tăng MTTR (Mean Time To Resolve).
5
+ tags: swift, ios, logging, error-handling, debugging, observability
6
+ ---
7
+
8
+ ## Log đầy đủ context khi xử lý lỗi
9
+
10
+ Khi log lỗi, phải bao gồm: định danh entity (userId, orderId), action đang thực hiện, loại lỗi cụ thể. Dùng `os.Logger` với privacy annotation để tránh log PII ra console.
11
+
12
+ **Incorrect (log thiếu context):**
13
+
14
+ ```swift
15
+ private let logger = Logger()
16
+
17
+ func processPayment(_ payment: Payment) async {
18
+ do {
19
+ try await paymentService.charge(payment)
20
+ } catch {
21
+ logger.error("Payment failed") // !! Không biết payment nào, tại sao?
22
+ }
23
+ }
24
+
25
+ func syncUserData() async {
26
+ do {
27
+ try await syncService.fullSync()
28
+ } catch {
29
+ print("Sync error: \(error)") // print thay vì logger, không có user context
30
+ }
31
+ }
32
+ ```
33
+
34
+ **Correct (log đầy đủ context với privacy):**
35
+
36
+ ```swift
37
+ import OSLog
38
+
39
+ private let logger = Logger(subsystem: "com.app.payment", category: "Processing")
40
+
41
+ func processPayment(_ payment: Payment) async {
42
+ do {
43
+ try await paymentService.charge(payment)
44
+ logger.info("Payment succeeded. paymentId=\(payment.id, privacy: .public), amount=\(payment.amount)")
45
+ } catch PaymentError.insufficientFunds {
46
+ // Lỗi business - warning
47
+ logger.warning("Insufficient funds. paymentId=\(payment.id, privacy: .public), userId=\(payment.userId, privacy: .private)")
48
+ } catch PaymentError.cardDeclined(let code) {
49
+ logger.error("Card declined. paymentId=\(payment.id, privacy: .public), declineCode=\(code, privacy: .public)")
50
+ } catch {
51
+ // Lỗi không ngờ - error level với full context
52
+ logger.error("Unexpected payment failure. paymentId=\(payment.id, privacy: .public), error=\(error.localizedDescription, privacy: .public)")
53
+ }
54
+ }
55
+ ```
56
+
57
+ **Privacy annotation cho os.Logger:**
58
+
59
+ | Annotation | Khi nào dùng |
60
+ |-----------|-------------|
61
+ | `.public` | ID kỹ thuật, error code (an toàn để log) |
62
+ | `.private` | Email, tên, phone (ẩn trong log, chỉ hiện khi debug) |
63
+ | `.sensitive` | Token, mật khẩu (luôn ẩn) |
64
+
65
+ **Tools:** `os.Logger` với Privacy API, Code Review
66
+
@@ -0,0 +1,65 @@
1
+ ---
2
+ title: Không hardcode thông tin nhạy cảm trong source code
3
+ impact: CRITICAL
4
+ impactDescription: Secret hardcode trong source sẽ bị lộ qua Git history, binary dump hay reverse engineering, gây rò rỉ toàn bộ hệ thống.
5
+ tags: swift, ios, secrets, security, api-keys, hardcoded
6
+ ---
7
+
8
+ ## Không hardcode thông tin nhạy cảm trong source code
9
+
10
+ API key, token, password, private key không được viết trực tiếp trong `.swift` file hay `.plist`. Dùng biến môi trường (CI/CD), `.xcconfig` file (không commit), hay Keychain để lưu runtime secrets.
11
+
12
+ **Incorrect (hardcode secrets):**
13
+
14
+ ```swift
15
+ // !! API key visible trong source code
16
+ struct APIConfig {
17
+ static let apiKey = "sk_live_AbC123XyZ789"
18
+ static let secretToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
19
+ static let databasePassword = "MyS3cr3tPass!"
20
+ }
21
+
22
+ class AnalyticsService {
23
+ private let firebaseKey = "AIzaSyC-abc123-real-key" // hardcode
24
+ private let mixpanelToken = "a1b2c3d4e5f6" // hardcode
25
+ }
26
+ ```
27
+
28
+ **Correct (lấy secret từ xcconfig hay environment):**
29
+
30
+ ```swift
31
+ // Config.xcconfig (được thêm vào .gitignore)
32
+ // API_KEY = sk_live_AbC123XyZ789
33
+
34
+ // Info.plist (đọc giá trị từ xcconfig - không commit secret)
35
+ // <key>APIKey</key>
36
+ // <string>$(API_KEY)</string>
37
+
38
+ struct APIConfig {
39
+ // Đọc từ Info.plist (value đến từ xcconfig không commit)
40
+ static var apiKey: String {
41
+ Bundle.main.infoDictionary?["APIKey"] as? String ?? ""
42
+ }
43
+ }
44
+
45
+ // Secrets nhạy cảm hơn (user token, refresh token) - lưu Keychain
46
+ class TokenManager {
47
+ func saveAccessToken(_ token: String) throws {
48
+ try KeychainManager.save(key: "access_token", value: token)
49
+ }
50
+
51
+ func retrieveAccessToken() -> String? {
52
+ return try? KeychainManager.load(key: "access_token")
53
+ }
54
+ }
55
+ ```
56
+
57
+ **Kiểm tra trước khi commit:**
58
+
59
+ ```bash
60
+ # Dùng git-secrets hoặc truffleHog để scan
61
+ git secrets --scan
62
+ ```
63
+
64
+ **Tools:** `git-secrets`, `truffleHog`, `.gitignore` cho `.xcconfig`, SwiftLint custom rule
65
+
@@ -0,0 +1,60 @@
1
+ ---
2
+ title: Biến Boolean phải bắt đầu bằng is, has, hoặc should
3
+ impact: LOW
4
+ impactDescription: Tiền tố is/has/should làm rõ biến là kiểu Boolean, tránh nhầm lẫn với biến trạng thái khác và cải thiện tính đọc của code.
5
+ tags: swift, ios, naming, boolean, code-quality, readability
6
+ ---
7
+
8
+ ## Biến Boolean phải bắt đầu bằng is, has, hoặc should
9
+
10
+ Property/biến kiểu `Bool` phải có tên bắt đầu bằng `is`, `has`, `should`, `can`, `will` hoặc `allow`. Điều này là convention của Swift API Design Guidelines.
11
+
12
+ **Incorrect (tên Boolean không rõ):**
13
+
14
+ ```swift
15
+ class UserSession {
16
+ var login: Bool = false // isLoggedIn?
17
+ var premium: Bool = false // isPremium?
18
+ var notification: Bool = true // hasNotification? isNotificationEnabled?
19
+ var error: Bool = false // hasError?
20
+ var loaded: Bool = false // isLoaded?
21
+ }
22
+
23
+ struct FeatureFlags {
24
+ var darkMode: Bool = false // isDarkModeEnabled?
25
+ var biometric: Bool = false // isBiometricEnabled?
26
+ var analytics: Bool = true // shouldTrackAnalytics?
27
+ }
28
+ ```
29
+
30
+ **Correct (tên Boolean rõ ràng):**
31
+
32
+ ```swift
33
+ class UserSession {
34
+ var isLoggedIn: Bool = false
35
+ var isPremiumUser: Bool = false
36
+ var hasUnreadNotifications: Bool = true
37
+ var hasError: Bool = false
38
+ var isDataLoaded: Bool = false
39
+ }
40
+
41
+ struct FeatureFlags {
42
+ var isDarkModeEnabled: Bool = false
43
+ var isBiometricEnabled: Bool = false
44
+ var shouldTrackAnalytics: Bool = true
45
+ var canAccessPremiumContent: Bool = false
46
+ var willShowOnboarding: Bool = true
47
+ }
48
+
49
+ // SwiftUI @State
50
+ struct ContentView: View {
51
+ @State private var isShowingAlert = false
52
+ @State private var isLoading = false
53
+ @State private var hasCompletedOnboarding = false
54
+
55
+ var body: some View { ... }
56
+ }
57
+ ```
58
+
59
+ **Tools:** SwiftLint (`identifier_name`), Code Review
60
+
@@ -0,0 +1,67 @@
1
+ ---
2
+ title: Không để logic parsing/mapping trong ViewController
3
+ impact: HIGH
4
+ impactDescription: Logic phân tích dữ liệu trong ViewController làm phình to class, khó test và vi phạm Single Responsibility Principle.
5
+ tags: swift, ios, mvc, mvvm, viewcontroller, parsing, architecture
6
+ ---
7
+
8
+ ## Không để logic parsing/mapping trong ViewController
9
+
10
+ ViewController chỉ nên lo presentation: bind data lên UI, xử lý user interaction. Logic parse JSON, map model sang ViewModel, validate, hay transform dữ liệu phải nằm trong tầng riêng (ViewModel, Mapper, UseCase).
11
+
12
+ **Incorrect (parsing/mapping trong ViewController):**
13
+
14
+ ```swift
15
+ class OrderListViewController: UIViewController {
16
+
17
+ // !! Logic mapping trong VC
18
+ func configureCell(_ cell: OrderCell, with orderData: [String: Any]) {
19
+ let id = orderData["id"] as? String ?? ""
20
+ let status = orderData["status"] as? String ?? "unknown"
21
+ let amount = orderData["total_amount"] as? Double ?? 0.0
22
+ let dateString = orderData["created_at"] as? String ?? ""
23
+
24
+ // Date parsing trực tiếp trong VC
25
+ let formatter = ISO8601DateFormatter()
26
+ let date = formatter.date(from: dateString) ?? Date()
27
+ let displayDate = DateFormatter.medium.string(from: date)
28
+
29
+ cell.orderIdLabel.text = "#\(id)"
30
+ cell.statusLabel.text = status.capitalized
31
+ cell.amountLabel.text = "$\(String(format: "%.2f", amount))"
32
+ cell.dateLabel.text = displayDate
33
+ }
34
+ }
35
+ ```
36
+
37
+ **Correct (VC chỉ bind, ViewModel lo mapping):**
38
+
39
+ ```swift
40
+ // OrderViewModel - lo mapping và formatting
41
+ struct OrderViewModel {
42
+ let displayId: String
43
+ let statusText: String
44
+ let formattedAmount: String
45
+ let displayDate: String
46
+
47
+ init(order: Order) {
48
+ self.displayId = "#\(order.id)"
49
+ self.statusText = order.status.localizedTitle
50
+ self.formattedAmount = NumberFormatter.currency.string(from: NSNumber(value: order.totalAmount)) ?? ""
51
+ self.displayDate = DateFormatter.medium.string(from: order.createdAt)
52
+ }
53
+ }
54
+
55
+ // ViewController - chỉ bind data lên UI
56
+ class OrderListViewController: UIViewController {
57
+ func configureCell(_ cell: OrderCell, with viewModel: OrderViewModel) {
58
+ cell.orderIdLabel.text = viewModel.displayId
59
+ cell.statusLabel.text = viewModel.statusText
60
+ cell.amountLabel.text = viewModel.formattedAmount
61
+ cell.dateLabel.text = viewModel.displayDate
62
+ }
63
+ }
64
+ ```
65
+
66
+ **Tools:** Code Review, Unit Tests (ViewModel test không cần UIKit)
67
+
@@ -0,0 +1,95 @@
1
+ ---
2
+ title: Không đặt quá nhiều logic trong superclass
3
+ impact: MEDIUM
4
+ impactDescription: Superclass phình to tạo coupling cao giữa subclasses, khó bảo trì và dễ vô tình break subclass khi thay đổi base class.
5
+ tags: swift, ios, inheritance, superclass, composition, architecture
6
+ ---
7
+
8
+ ## Không đặt quá nhiều logic trong superclass
9
+
10
+ Thay vì nhồi logic vào `BaseViewController` hay `BaseViewModel`, hãy dùng **composition** qua protocol extension hoặc delegate. Superclass chỉ nên chứa logic thực sự chung cho mọi subclass.
11
+
12
+ **Incorrect (BaseViewController quá nặng):**
13
+
14
+ ```swift
15
+ class BaseViewController: UIViewController {
16
+ // Networking
17
+ func makeAPICall<T>(url: URL, completion: @escaping (Result<T, Error>) -> Void) { ... }
18
+
19
+ // Analytics
20
+ func trackScreenView(name: String) { ... }
21
+ func trackEvent(_ event: String, parameters: [String: Any]) { ... }
22
+
23
+ // Loading
24
+ func showLoading() { ... }
25
+ func hideLoading() { ... }
26
+
27
+ // Alert
28
+ func showAlert(title: String, message: String) { ... }
29
+ func showErrorAlert(_ error: Error) { ... }
30
+
31
+ // Navigation
32
+ func pushViewController(_ vc: UIViewController, animated: Bool) { ... }
33
+
34
+ // Keyboard
35
+ func setupKeyboardDismissal() { ... }
36
+ var keyboardHeight: CGFloat { ... }
37
+
38
+ // Tất cả ViewController đều kế thừa hết dù không cần!
39
+ }
40
+ ```
41
+
42
+ **Correct (composition qua protocol + extension):**
43
+
44
+ ```swift
45
+ // Tách thành protocol có thể opt-in
46
+ protocol LoadingDisplayable: UIViewController {
47
+ func showLoading()
48
+ func hideLoading()
49
+ }
50
+
51
+ extension LoadingDisplayable {
52
+ func showLoading() {
53
+ LoadingOverlay.show(in: view)
54
+ }
55
+ func hideLoading() {
56
+ LoadingOverlay.hide(from: view)
57
+ }
58
+ }
59
+
60
+ protocol ErrorAlertDisplayable: UIViewController {
61
+ func showErrorAlert(_ error: Error)
62
+ }
63
+ extension ErrorAlertDisplayable {
64
+ func showErrorAlert(_ error: Error) {
65
+ let alert = UIAlertController(title: "Lỗi", message: error.localizedDescription, preferredStyle: .alert)
66
+ alert.addAction(UIAlertAction(title: "OK", style: .default))
67
+ present(alert, animated: true)
68
+ }
69
+ }
70
+
71
+ // BaseViewController chỉ giữ thứ thực sự dùng ở MỌI VC
72
+ class BaseViewController: UIViewController {
73
+ override func viewDidLoad() {
74
+ super.viewDidLoad()
75
+ setupNavigationBar()
76
+ }
77
+ private func setupNavigationBar() { /* common nav bar style */ }
78
+ }
79
+
80
+ // Từng VC chỉ opt-in vào tính năng cần
81
+ class OrderListViewController: BaseViewController, LoadingDisplayable, ErrorAlertDisplayable {
82
+ func loadOrders() async {
83
+ showLoading()
84
+ defer { hideLoading() }
85
+ do {
86
+ orders = try await orderService.fetchOrders()
87
+ } catch {
88
+ showErrorAlert(error)
89
+ }
90
+ }
91
+ }
92
+ ```
93
+
94
+ **Tools:** Code Review, SwiftLint (`type_body_length`)
95
+
@@ -0,0 +1,80 @@
1
+ ---
2
+ title: Không hardcode cấu hình trong code - dùng xcconfig hoặc environment
3
+ impact: MEDIUM
4
+ impactDescription: Config hardcode làm khó switch giữa môi trường development/staging/production và dễ gây lỗi khi deploy.
5
+ tags: swift, ios, configuration, environment, xcconfig, code-quality
6
+ ---
7
+
8
+ ## Không hardcode cấu hình trong code - dùng xcconfig hoặc environment
9
+
10
+ URL của API, timeout, feature flags, và các thông số khác biệt giữa môi trường không được hardcode. Dùng `.xcconfig` file theo scheme, đọc qua `Info.plist`, hoặc dùng build setting `$(VAR_NAME)`.
11
+
12
+ **Incorrect (config hardcode):**
13
+
14
+ ```swift
15
+ class APIClient {
16
+ // !! Hardcode URL - phải sửa code khi switch môi trường
17
+ private let baseURL = "https://api.example.com/v2"
18
+ private let timeout: TimeInterval = 30
19
+
20
+ init() {
21
+ // !! Hardcode môi trường
22
+ #if DEBUG
23
+ let url = "https://dev-api.example.com/v2"
24
+ #else
25
+ let url = "https://api.example.com/v2"
26
+ #endif
27
+ }
28
+ }
29
+
30
+ struct Config {
31
+ static let maxRetries = 3 // magic number
32
+ static let sessionTimeout = 1800 // 30 phút? hardcode
33
+ }
34
+ ```
35
+
36
+ **Correct (đọc từ xcconfig / Info.plist theo scheme):**
37
+
38
+ ```swift
39
+ // Development.xcconfig
40
+ // API_BASE_URL = https://dev-api.example.com/v2
41
+ // REQUEST_TIMEOUT = 30
42
+ // MAX_RETRIES = 3
43
+
44
+ // Production.xcconfig
45
+ // API_BASE_URL = https://api.example.com/v2
46
+ // REQUEST_TIMEOUT = 15
47
+ // MAX_RETRIES = 2
48
+
49
+ // Info.plist
50
+ // <key>APIBaseURL</key><string>$(API_BASE_URL)</string>
51
+ // <key>RequestTimeout</key><string>$(REQUEST_TIMEOUT)</string>
52
+
53
+ struct AppConfiguration {
54
+ static var apiBaseURL: URL {
55
+ guard let urlString = Bundle.main.infoDictionary?["APIBaseURL"] as? String,
56
+ let url = URL(string: urlString) else {
57
+ fatalError("APIBaseURL configuration missing")
58
+ }
59
+ return url
60
+ }
61
+
62
+ static var requestTimeout: TimeInterval {
63
+ let value = Bundle.main.infoDictionary?["RequestTimeout"] as? String
64
+ return Double(value ?? "30") ?? 30
65
+ }
66
+
67
+ static var maxRetries: Int {
68
+ let value = Bundle.main.infoDictionary?["MaxRetries"] as? String
69
+ return Int(value ?? "3") ?? 3
70
+ }
71
+ }
72
+
73
+ class APIClient {
74
+ private let baseURL = AppConfiguration.apiBaseURL
75
+ private let timeout = AppConfiguration.requestTimeout
76
+ }
77
+ ```
78
+
79
+ **Tools:** Xcode Build Schemes + xcconfig, `Configuration.swift` helper
80
+
@@ -0,0 +1,65 @@
1
+ ---
2
+ title: Tránh SQL Injection khi dùng SQLite, FMDB hoặc CoreData NSPredicate
3
+ impact: CRITICAL
4
+ impactDescription: Ghép chuỗi trực tiếp vào câu lệnh SQL hoặc NSPredicate format string cho phép attacker đọc, xóa hoặc sửa toàn bộ database của app.
5
+ tags: swift, ios, sql-injection, sqlite, fmdb, coredata, nspredicate, security
6
+ ---
7
+
8
+ ## Tránh SQL Injection khi dùng SQLite, FMDB hoặc CoreData NSPredicate
9
+
10
+ Trong iOS, SQL injection xảy ra khi ghép chuỗi trực tiếp vào câu lệnh SQLite/FMDB, hoặc dùng `NSPredicate(format:)` với giá trị người dùng chưa được escape. Phải dùng parameterized query hoặc `NSPredicate(format:argumentArray:)`.
11
+
12
+ **Incorrect (ghép chuỗi trực tiếp):**
13
+
14
+ ```swift
15
+ import FMDB
16
+
17
+ // !! Ghép chuỗi tên đăng nhập trực tiếp vào SQL
18
+ func findUser(username: String, db: FMDatabase) -> [String: Any]? {
19
+ let query = "SELECT * FROM users WHERE username = '\(username)'" // SQL Injection!
20
+ let result = db.executeQuery(query, withArgumentsIn: [])
21
+ return nil
22
+ }
23
+
24
+ // !! NSPredicate với format string không an toàn
25
+ func fetchMessages(senderName: String, context: NSManagedObjectContext) -> [Message] {
26
+ let predicate = NSPredicate(format: "sender == '\(senderName)'") // Injection!
27
+ let request = Message.fetchRequest()
28
+ request.predicate = predicate
29
+ return (try? context.fetch(request)) ?? []
30
+ }
31
+ ```
32
+
33
+ **Correct (parameterized query):**
34
+
35
+ ```swift
36
+ import FMDB
37
+
38
+ // SAFE: Dùng parameterized query với ?
39
+ func findUser(username: String, db: FMDatabase) -> FMResultSet? {
40
+ let query = "SELECT * FROM users WHERE username = ?"
41
+ return db.executeQuery(query, withArgumentsIn: [username])
42
+ }
43
+
44
+ // SAFE: NSPredicate với argumentArray
45
+ func fetchMessages(senderName: String, context: NSManagedObjectContext) -> [Message] {
46
+ let predicate = NSPredicate(format: "sender == %@", argumentArray: [senderName])
47
+ let request = Message.fetchRequest()
48
+ request.predicate = predicate
49
+ return (try? context.fetch(request)) ?? []
50
+ }
51
+
52
+ // SAFE: SQLite3 với bound parameters
53
+ func insertNote(title: String, body: String, db: OpaquePointer) {
54
+ var stmt: OpaquePointer?
55
+ let sql = "INSERT INTO notes (title, body) VALUES (?, ?)"
56
+ if sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK {
57
+ sqlite3_bind_text(stmt, 1, (title as NSString).utf8String, -1, nil)
58
+ sqlite3_bind_text(stmt, 2, (body as NSString).utf8String, -1, nil)
59
+ sqlite3_step(stmt)
60
+ }
61
+ sqlite3_finalize(stmt)
62
+ }
63
+ ```
64
+
65
+ **Tools:** SwiftLint custom rule, OWASP MASVS-CODE-4, Instruments (SQLite profiler)
@@ -0,0 +1,67 @@
1
+ ---
2
+ title: Không log thông tin nhạy cảm (token, password, PII)
3
+ impact: HIGH
4
+ impactDescription: Log trong iOS có thể đọc được qua Console.app, thiết bị bị jailbreak, hoặc crash logs gửi lên server. Credentials trong log đồng nghĩa với data breach.
5
+ tags: swift, ios, logging, security, credentials, privacy, pii
6
+ ---
7
+
8
+ ## Không log thông tin nhạy cảm (token, password, PII)
9
+
10
+ `NSLog`, `print()`, và `os_log` đều có thể bị capture bởi tool như Console.app hoặc bởi attacker khi thiết bị bị jailbreak. Không bao giờ log: password, access token, refresh token, thẻ tín dụng, số CMND/CCCD, hay thông tin cá nhân nhạy cảm.
11
+
12
+ **Incorrect (log thông tin nhạy cảm):**
13
+
14
+ ```swift
15
+ func login(email: String, password: String) async {
16
+ print("Attempting login with email: \(email), password: \(password)") // !! password trong log
17
+
18
+ do {
19
+ let response = try await authService.login(email: email, password: password)
20
+ // !! Token trong log
21
+ NSLog("Login success. accessToken: %@, refreshToken: %@", response.accessToken, response.refreshToken)
22
+
23
+ UserDefaults.standard.set(response.accessToken, forKey: "access_token")
24
+ } catch {
25
+ // !! Email trong log
26
+ logger.error("Login failed for email: \(email), error: \(error)")
27
+ }
28
+ }
29
+
30
+ func processPayment(cardNumber: String, cvv: String) {
31
+ logger.info("Processing card: \(cardNumber), CVV: \(cvv)") // !! PCI DSS violation
32
+ }
33
+ ```
34
+
35
+ **Correct (log không chứa thông tin nhạy cảm):**
36
+
37
+ ```swift
38
+ import OSLog
39
+
40
+ private let logger = Logger(subsystem: "com.app.auth", category: "Authentication")
41
+
42
+ func login(email: String, password: String) async {
43
+ // Log action không log credentials
44
+ logger.info("Login attempt initiated")
45
+
46
+ do {
47
+ let response = try await authService.login(email: email, password: password)
48
+ // Chỉ log metadata - không log token value
49
+ logger.info("Login succeeded. userId=\(response.userId, privacy: .public), tokenExpiry=\(response.expiresIn)")
50
+
51
+ // Lưu token vào Keychain, không log
52
+ try tokenManager.saveAccessToken(response.accessToken)
53
+ } catch {
54
+ // Log loại lỗi, không log email
55
+ logger.warning("Login failed: \(error.localizedDescription, privacy: .public)")
56
+ }
57
+ }
58
+
59
+ func processPayment(cardNumber: String, cvv: String) {
60
+ // Chỉ log metadata về payment, không log card data
61
+ let maskedCard = String(cardNumber.suffix(4))
62
+ logger.info("Processing payment for card ending in \(maskedCard, privacy: .public)")
63
+ }
64
+ ```
65
+
66
+ **Tools:** Code Review, `os.Logger` privacy API, MobSF static analysis
67
+