@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,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
+
@@ -0,0 +1,73 @@
1
+ ---
2
+ title: Luôn kiểm tra quyền truy cập phía server - không tin vào UI ẩn
3
+ impact: HIGH
4
+ impactDescription: Ẩn nút/màn hình ở client không ngăn được attacker gọi trực tiếp vào API. Server phải luôn là nơi kiểm tra quyền cuối cùng.
5
+ tags: swift, ios, authorization, server-side, api-security, security
6
+ ---
7
+
8
+ ## Luôn kiểm tra quyền truy cập phía server - không tin vào UI ẩn
9
+
10
+ iOS app thường ẩn tính năng theo role, nhưng đây chỉ là UX. Server phải enforce authorization: kiểm tra token/role trong mỗi API request và trả về 401/403 khi vi phạm. Client không được là người quyết định quyền cuối cùng.
11
+
12
+ **Incorrect (chỉ kiểm tra phía client):**
13
+
14
+ ```swift
15
+ // !! Chỉ ẩn nút theo role cục bộ - không gọi server check
16
+ struct AdminDashboardView: View {
17
+ @EnvironmentObject var authManager: AuthManager
18
+
19
+ var body: some View {
20
+ if authManager.currentUser?.role == "admin" { // Check phía client
21
+ Button("Delete User") { deleteUser() }
22
+ }
23
+ }
24
+
25
+ // API bên trong không gửi kèm authorization header
26
+ func deleteUser() {
27
+ let url = URL(string: "https://api.example.com/admin/users/1")!
28
+ var request = URLRequest(url: url)
29
+ request.httpMethod = "DELETE"
30
+ // Không có Authorization header!
31
+ URLSession.shared.dataTask(with: request).resume()
32
+ }
33
+ }
34
+ ```
35
+
36
+ **Correct (server enforces authorization):**
37
+
38
+ ```swift
39
+ struct AdminDashboardView: View {
40
+ @EnvironmentObject var authManager: AuthManager
41
+
42
+ var body: some View {
43
+ // UI ẩn chỉ là UX - server vẫn sẽ reject nếu gọi sai
44
+ if authManager.currentUser?.role == "admin" {
45
+ Button("Delete User") { deleteUser() }
46
+ }
47
+ }
48
+
49
+ // SAFE: Luôn gửi Bearer token để server kiểm tra quyền
50
+ func deleteUser() {
51
+ guard let token = authManager.accessToken else { return }
52
+ let url = URL(string: "https://api.example.com/admin/users/1")!
53
+ var request = URLRequest(url: url)
54
+ request.httpMethod = "DELETE"
55
+ request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
56
+ URLSession.shared.dataTask(with: request) { data, response, error in
57
+ guard let httpResponse = response as? HTTPURLResponse else { return }
58
+ if httpResponse.statusCode == 403 {
59
+ // Server từ chối - xử lý gracefully
60
+ DispatchQueue.main.async {
61
+ authManager.handleUnauthorized()
62
+ }
63
+ }
64
+ }.resume()
65
+ }
66
+ }
67
+
68
+ // Server-side (ví dụ Vapor): kiểm tra role trong middleware
69
+ // app.grouped(RoleMiddleware(requiredRole: "admin"))
70
+ // .delete("admin", "users", ":userId", use: deleteUserHandler)
71
+ ```
72
+
73
+ **Tools:** Proxyman (intercept requests), OWASP MASVS-AUTH-1, Burp Suite Mobile
@@ -0,0 +1,76 @@
1
+ ---
2
+ title: Không để credential mặc định hoặc hardcode mật khẩu trong app
3
+ impact: HIGH
4
+ impactDescription: Credential mặc định hoặc mật khẩu hardcode trong source code có thể bị phát hiện qua reverse engineering hoặc static analysis, cho phép truy cập trái phép.
5
+ tags: swift, ios, credentials, hardcode, default-password, security
6
+ ---
7
+
8
+ ## Không để credential mặc định hoặc hardcode mật khẩu trong app
9
+
10
+ Không hardcode username/password, API key hoặc encryption key dưới dạng string literals trong code Swift. Đây là lỗi phổ biến nhất trong mobile app security. Dùng Keychain hoặc load từ server sau khi authenticate.
11
+
12
+ **Incorrect (hardcode credential):**
13
+
14
+ ```swift
15
+ // !! Credential hardcode trong code
16
+ struct AuthService {
17
+ private let adminUsername = "admin"
18
+ private let adminPassword = "admin123" // Hardcode!
19
+
20
+ func loginAdmin() {
21
+ let credentials = "\(adminUsername):\(adminPassword)"
22
+ .data(using: .utf8)!
23
+ .base64EncodedString()
24
+ // Bất kỳ ai có binary app đều đọc được qua strings command
25
+ }
26
+ }
27
+
28
+ // !! API key hardcode
29
+ class AnalyticsService {
30
+ private let apiKey = "AIzaSyD-hardcoded-key-abc123" // Lộ sau khi decompile!
31
+ private let encryptionKey = "mySecretKey1234" // Nguy hiểm!
32
+ }
33
+
34
+ // !! Default profile cho testing còn sót lại production
35
+ let defaultPIN = "1234"
36
+ let testAccountPassword = "Test@123"
37
+ ```
38
+
39
+ **Correct (load từ secure source):**
40
+
41
+ ```swift
42
+ // SAFE: Load API key từ xcconfig / remote config
43
+ class AnalyticsService {
44
+ private let apiKey: String
45
+
46
+ init() {
47
+ // Load từ Info.plist (giá trị inject qua xcconfig trong CI/CD)
48
+ guard let key = Bundle.main.infoDictionary?["ANALYTICS_API_KEY"] as? String,
49
+ !key.isEmpty else {
50
+ fatalError("ANALYTICS_API_KEY not configured")
51
+ }
52
+ self.apiKey = key
53
+ }
54
+ }
55
+
56
+ // SAFE: Encryption key sinh ngẫu nhiên và lưu Keychain
57
+ class EncryptionKeyManager {
58
+ private let keychainKey = "com.app.encryptionKey"
59
+
60
+ func getOrCreateKey() throws -> SymmetricKey {
61
+ if let keyData = try KeychainHelper.read(key: keychainKey) {
62
+ return SymmetricKey(data: keyData)
63
+ }
64
+ // Sinh key ngẫu nhiên, không hardcode
65
+ let newKey = SymmetricKey(size: .bits256)
66
+ let keyData = newKey.withUnsafeBytes { Data($0) }
67
+ try KeychainHelper.save(key: keychainKey, data: keyData)
68
+ return newKey
69
+ }
70
+ }
71
+
72
+ // Test credentials phải dùng environment variable trong CI
73
+ // Không commit file chứa test password vào Git
74
+ ```
75
+
76
+ **Tools:** SwiftLint (no_hardcoded_strings custom rule), `strings` binary analysis, OWASP MASVS-STORAGE-2, detect-secrets