@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.
- package/config/rules/rules-registry-generated.json +1717 -282
- package/core/architecture-integration.js +57 -15
- package/core/cli-action-handler.js +51 -36
- package/core/config-manager.js +6 -0
- package/core/config-merger.js +33 -0
- package/core/config-validator.js +37 -2
- package/core/file-targeting-service.js +148 -15
- package/core/init-command.js +118 -70
- package/core/output-service.js +12 -3
- package/core/project-detector.js +517 -0
- package/core/scoring-service.js +12 -6
- package/core/summary-report-service.js +9 -4
- package/core/tui-select.js +245 -0
- package/engines/arch-detect/rules/layered/l001-presentation-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l002-business-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l003-data-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l004-model-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l005-layer-separation.js +22 -2
- package/engines/arch-detect/rules/layered/l006-dependency-direction.js +8 -5
- package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +67 -29
- package/engines/arch-detect/rules/presentation/pr001-view-layer.js +16 -9
- package/engines/arch-detect/rules/presentation/pr006-router-layer.js +33 -8
- package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +35 -6
- package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +56 -10
- package/engines/impact/cli.js +54 -39
- package/engines/impact/config/default-config.js +105 -5
- package/engines/impact/core/impact-analyzer.js +12 -15
- package/engines/impact/core/utils/gitignore-parser.js +123 -0
- package/engines/impact/core/utils/method-call-graph.js +272 -87
- package/origin-rules/dart-en.md +1 -1
- package/origin-rules/go-en.md +231 -0
- package/origin-rules/php-en.md +107 -0
- package/origin-rules/python-en.md +113 -0
- package/origin-rules/ruby-en.md +607 -0
- package/package.json +1 -1
- package/scripts/copy-arch-detect.js +5 -1
- package/scripts/copy-impact-analyzer.js +5 -1
- package/scripts/generate-rules-registry.js +30 -14
- package/skill-assets/sunlint-code-quality/SKILL.md +3 -2
- package/skill-assets/sunlint-code-quality/rules/dart/C006-verb-noun-functions.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C013-no-dead-code.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C014-dependency-injection.md +92 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C017-no-constructor-logic.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C018-generic-errors.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C019-error-log-level.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C020-no-unused-imports.md +46 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C022-no-unused-variables.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C023-no-duplicate-names.md +56 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C024-centralize-constants.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C029-catch-log-root-cause.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C030-custom-error-classes.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C033-separate-data-access.md +90 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C035-error-context-logging.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C041-no-hardcoded-secrets.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C042-boolean-naming.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C052-widget-parsing.md +84 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C060-superclass-logic.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C067-no-hardcoded-config.md +108 -0
- package/skill-assets/sunlint-code-quality/rules/go/G001-explicit-error-handling.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/go/G002-context-first-argument.md +44 -0
- package/skill-assets/sunlint-code-quality/rules/go/G003-receiver-consistency.md +38 -0
- package/skill-assets/sunlint-code-quality/rules/go/G004-avoid-panic.md +49 -0
- package/skill-assets/sunlint-code-quality/rules/go/G005-goroutine-leak-prevention.md +49 -0
- package/skill-assets/sunlint-code-quality/rules/go/G006-interface-consumer-definition.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN001-gin-binding-validation.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN002-gin-error-response.md +48 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN003-graceful-shutdown.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN004-gin-route-logical-grouping.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/AGENTS.md +149 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN001-abort-after-response.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN002-request-context.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN003-bind-error-handling.md +70 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN004-dependency-injection.md +78 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN005-route-groups-middleware.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN006-http-status-codes.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN007-release-mode.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN008-struct-validation-tags.md +90 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN009-recovery-middleware.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN010-context-scope.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN011-middleware-concerns.md +92 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN012-no-log-sensitive.md +84 -0
- package/skill-assets/sunlint-code-quality/rules/java/J001-try-with-resources.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/java/J002-equals-and-hashcode.md +88 -0
- package/skill-assets/sunlint-code-quality/rules/java/J003-string-comparison.md +72 -0
- package/skill-assets/sunlint-code-quality/rules/java/J004-use-java-time.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/java/J005-no-print-stack-trace.md +80 -0
- package/skill-assets/sunlint-code-quality/rules/java/J006-no-system-println.md +89 -0
- package/skill-assets/sunlint-code-quality/rules/java/J007-proper-logger.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/java/J008-thread-safe-singleton.md +119 -0
- package/skill-assets/sunlint-code-quality/rules/java/J009-utility-class-constructor.md +82 -0
- package/skill-assets/sunlint-code-quality/rules/java/J010-preserve-stack-trace.md +119 -0
- package/skill-assets/sunlint-code-quality/rules/java/J011-null-safe-compare.md +88 -0
- package/skill-assets/sunlint-code-quality/rules/java/J012-use-enum-collections.md +104 -0
- package/skill-assets/sunlint-code-quality/rules/java/J013-return-empty-not-null.md +102 -0
- package/skill-assets/sunlint-code-quality/rules/java/J014-hardcoded-crypto-key.md +108 -0
- package/skill-assets/sunlint-code-quality/rules/java/J015-optional-instead-of-null.md +109 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/AGENTS.md +124 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV001-form-request-validation.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV002-eager-load-no-n-plus-1.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV003-config-not-env.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV004-fillable-mass-assignment.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV005-policies-gates-authorization.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV006-queue-heavy-tasks.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV007-hash-passwords.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV008-route-model-binding.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV009-api-resources.md +72 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV010-chunk-large-datasets.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV011-db-transactions.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV012-service-layer.md +78 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV013-testing-factories.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV014-service-container.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/python/P001-mutable-default-argument.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/python/P002-specify-file-encoding.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/python/P003-context-manager-for-resources.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/python/P004-no-bare-except.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/python/P005-use-isinstance.md +60 -0
- package/skill-assets/sunlint-code-quality/rules/python/P006-timezone-aware-datetime.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/python/P007-use-pathlib.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/python/P008-no-wildcard-import.md +52 -0
- package/skill-assets/sunlint-code-quality/rules/python/P009-logging-lazy-format.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/python/P010-exception-chaining.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/python/P011-subprocess-check.md +59 -0
- package/skill-assets/sunlint-code-quality/rules/python/P012-requests-timeout.md +70 -0
- package/skill-assets/sunlint-code-quality/rules/python/P013-no-global-statement.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/python/P014-no-modify-collection-while-iterating.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/python/P015-prefer-fstrings.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/AGENTS.md +121 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR001-strong-parameters.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR002-eager-load-includes.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR003-service-objects.md +99 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR004-active-job-background.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR005-pagination.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR006-find-each-batches.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR007-http-status-codes.md +76 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR008-before-action-auth.md +77 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR009-rails-credentials.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR010-scopes.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR011-counter-cache.md +59 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR012-render-json-status.md +42 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C006-verb-noun-functions.md +37 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C013-no-dead-code.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C014-dependency-injection.md +69 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C017-no-constructor-logic.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C018-generic-errors.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C019-error-log-level.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C020-no-unused-imports.md +47 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C022-no-unused-variables.md +46 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C023-no-duplicate-names.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C024-centralize-constants.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C029-catch-log-root-cause.md +69 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C030-custom-error-classes.md +77 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C033-separate-data-access.md +89 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C035-error-context-logging.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C041-no-hardcoded-secrets.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C042-boolean-naming.md +60 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C052-controller-parsing.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C060-superclass-logic.md +95 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C067-no-hardcoded-config.md +80 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S003-sql-injection.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S004-no-log-credentials.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S005-server-authorization.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S006-default-credentials.md +76 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S007-output-encoding.md +96 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S009-approved-crypto.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S010-csprng.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S011-insecure-deserialization.md +74 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S012-secrets-management.md +81 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S013-tls-connections.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S017-parameterized-queries.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S019-session-management.md +131 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S020-kvc-injection.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S025-input-validation.md +125 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S029-brute-force-protection.md +120 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S036-path-traversal.md +102 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S039-tls-certificate-validation.md +109 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S041-logout-invalidation.md +103 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S043-password-hashing.md +116 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S044-critical-changes-reauth.md +145 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S045-debug-info-exposure.md +116 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S046-unvalidated-redirect.md +140 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S051-token-expiry.md +134 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S053-jwt-validation.md +139 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S059-background-snapshot-protection.md +113 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S060-data-protection-api.md +106 -0
- 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
|
+
|