-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/#63 - 시크릿 자동 감지 UseCase (DVDomain) #65
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
27a13e4
a066d10
642b24f
9e4f4ce
e184084
4e33d18
9416aaf
e8f69ed
65d0eb9
08b051d
465db79
57ea0ac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| // Copyright © 2026 Devault. All rights reserved | ||
|
|
||
| import Foundation | ||
|
|
||
| /// URL 형태 데이터베이스 · 메시지 큐 · 캐시 접속 문자열 감지. | ||
| /// | ||
| /// 등록된 scheme(`postgres` · `mongodb+srv` 등)에만 매칭. host suffix로 관리형 서비스 후보를 함께 부여. | ||
| struct DatabaseURLDetector: SecretDetector { | ||
| let schemes: [DatabaseSchemeRule] | ||
|
|
||
| func detect(_ value: SensitiveString, context: DetectorContext) -> DetectionResult? { | ||
| value.withUnsafeAccess { raw in | ||
| if let result = detectAsURL(raw) { return result } | ||
| return detectAsAzureConnectionString(raw) | ||
| } | ||
| } | ||
|
|
||
| /// URL 파싱이 실패하는 Azure 스타일 `Key=Value;` 커넥션 문자열 매칭. metadata는 부여하지 않는다. | ||
| private func detectAsAzureConnectionString(_ raw: String) -> DetectionResult? { | ||
| if raw.contains("DefaultEndpointsProtocol=") && raw.contains("AccountName=") { | ||
| return azureResult(service: "Azure Storage", label: "Azure Storage Connection String") | ||
| } | ||
| if raw.contains("Endpoint=sb://") { | ||
| return azureResult(service: "Azure Service Bus", label: "Azure Service Bus Connection String") | ||
| } | ||
| if raw.contains("Server=") && raw.contains("Database=") { | ||
| return azureResult(service: "Azure SQL", label: "Azure SQL Connection String") | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| private func azureResult(service: String, label: String) -> DetectionResult { | ||
| DetectionResult(candidates: [.init(service: service, displayLabel: label, confidence: .high)]) | ||
| } | ||
|
|
||
| private func detectAsURL(_ raw: String) -> DetectionResult? { | ||
| guard let comps = URLComponents(string: raw), | ||
| let scheme = comps.scheme?.lowercased(), | ||
| let rule = schemes.first(where: { $0.scheme == scheme }) else { | ||
| return nil | ||
| } | ||
|
|
||
| let host = comps.host | ||
| let port = comps.port ?? rule.defaultPort | ||
| let databaseName: String? = { | ||
| let path = comps.path.trimmingCharacters(in: CharacterSet(charactersIn: "/")) | ||
| return path.isEmpty ? nil : path | ||
| }() | ||
|
|
||
| let info = DetectedMetadata.DatabaseInfo( | ||
| scheme: scheme, | ||
| host: host, | ||
| port: port, | ||
| databaseName: databaseName, | ||
| username: comps.user | ||
| ) | ||
|
|
||
| let candidates = hostCandidates(for: host) | ||
| return DetectionResult(candidates: candidates, metadata: .database(info)) | ||
| } | ||
|
|
||
| private func hostCandidates(for host: String?) -> [ServiceCandidate] { | ||
| guard let host = host?.lowercased() else { return [] } | ||
| if host == "neon.tech" || host.hasSuffix(".neon.tech") { | ||
| return [.init(service: "Neon", displayLabel: "Neon Postgres", confidence: .high)] | ||
| } | ||
| if host.hasSuffix(".supabase.co") { | ||
| return [.init(service: "Supabase DB", displayLabel: "Supabase Postgres", confidence: .high)] | ||
| } | ||
| return [] | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| // Copyright © 2026 Devault. All rights reserved | ||
|
|
||
| import Foundation | ||
|
|
||
| /// Detector가 재귀적으로 파이프라인을 호출할 때 사용하는 콜백. | ||
| /// | ||
| /// 주 사용처: `EnvSetDetector`가 각 KEY=VALUE의 VALUE에 대해 재귀 감지 필요. | ||
| /// UseCase Impl로의 직접 참조를 피하기 위한 seam. | ||
| protocol DetectorContext: Sendable { | ||
| func detect(_ value: SensitiveString) -> DetectionResult | ||
| } | ||
|
Comment on lines
+9
to
+11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
fd DetectSecretUseCaseImpl.swift EnvSetDetector.swift
rg -n -B2 -A20 'func detect\(' Projects/DVDomain/Sources/UseCase/Impl/Secret/DetectSecretUseCaseImpl.swift
rg -n -B2 -A30 'struct EnvSetDetector' Projects/DVDomain/Sources/Detection/Detector/EnvSetDetector.swiftRepository: DevaultProject/Devault-macOS Length of output: 2085 🏁 Script executed: #!/bin/bash
set -e
sed -n '1,120p' Projects/DVDomain/Sources/UseCase/Impl/Secret/DetectSecretUseCaseImpl.swift
printf '\n--- SecretDetector definitions/usages ---\n'
rg -n 'protocol SecretDetector|struct EnvSetDetector|func detect\(_ value: SensitiveString, context: DetectorContext\)|class|struct.*SecretDetector|DetectSecretUseCaseImpl' Projects/DVDomain/SourcesRepository: DevaultProject/Devault-macOS Length of output: 4330
🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,60 @@ | ||||||||||||||||||||||||||||||||||||||||
| // Copyright © 2026 Devault. All rights reserved | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| import Foundation | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /// 여러 줄 KEY=VALUE 형태(`.env` 파일 등) 감지. 각 VALUE는 파이프라인을 재귀 호출해 세부 감지. | ||||||||||||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||||||||||||
| /// 매칭 조건: 2줄 이상 · KEY는 `UPPER_SNAKE_CASE` (첫 문자 대문자) · `#` 주석 라인 무시 · 빈 라인 무시. | ||||||||||||||||||||||||||||||||||||||||
| /// 위 조건 중 하나라도 어긋나면(파싱된 유효 라인이 2건 미만) `nil` 반환 → 다음 detector로 fall-through. | ||||||||||||||||||||||||||||||||||||||||
| struct EnvSetDetector: SecretDetector { | ||||||||||||||||||||||||||||||||||||||||
| func detect(_ value: SensitiveString, context: DetectorContext) -> DetectionResult? { | ||||||||||||||||||||||||||||||||||||||||
| value.withUnsafeAccess { raw in | ||||||||||||||||||||||||||||||||||||||||
| let entries = parseEntries(raw) | ||||||||||||||||||||||||||||||||||||||||
| guard entries.count >= 2 else { return nil } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| let subDetections = entries.map { entry in | ||||||||||||||||||||||||||||||||||||||||
| SubDetection( | ||||||||||||||||||||||||||||||||||||||||
| key: entry.key, | ||||||||||||||||||||||||||||||||||||||||
| result: context.detect(SensitiveString(entry.value)) | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| return DetectionResult( | ||||||||||||||||||||||||||||||||||||||||
| candidates: [], | ||||||||||||||||||||||||||||||||||||||||
| metadata: .envSet(keys: entries.map(\.key)), | ||||||||||||||||||||||||||||||||||||||||
| subDetections: subDetections | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| private func parseEntries(_ raw: String) -> [(key: String, value: String)] { | ||||||||||||||||||||||||||||||||||||||||
| raw.components(separatedBy: .newlines).compactMap { line in | ||||||||||||||||||||||||||||||||||||||||
| let trimmed = line.trimmingCharacters(in: .whitespaces) | ||||||||||||||||||||||||||||||||||||||||
| guard !trimmed.isEmpty, !trimmed.hasPrefix("#") else { return nil } | ||||||||||||||||||||||||||||||||||||||||
| guard let equalsIndex = trimmed.firstIndex(of: "=") else { return nil } | ||||||||||||||||||||||||||||||||||||||||
| let key = String(trimmed[..<equalsIndex]) | ||||||||||||||||||||||||||||||||||||||||
| guard isValidEnvKey(key) else { return nil } | ||||||||||||||||||||||||||||||||||||||||
| let rawValue = String(trimmed[trimmed.index(after: equalsIndex)...]) | ||||||||||||||||||||||||||||||||||||||||
| return (key, stripQuotes(rawValue)) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| private func isValidEnvKey(_ key: String) -> Bool { | ||||||||||||||||||||||||||||||||||||||||
| guard let first = key.unicodeScalars.first, | ||||||||||||||||||||||||||||||||||||||||
| CharacterSet.uppercaseLetters.contains(first) else { return false } | ||||||||||||||||||||||||||||||||||||||||
| let allowed = CharacterSet.uppercaseLetters | ||||||||||||||||||||||||||||||||||||||||
| .union(CharacterSet.decimalDigits) | ||||||||||||||||||||||||||||||||||||||||
| .union(CharacterSet(charactersIn: "_")) | ||||||||||||||||||||||||||||||||||||||||
| return key.unicodeScalars.allSatisfy { allowed.contains($0) } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /// 앞뒤 짝이 맞는 `"` 또는 `'`만 벗겨낸다. 내부에 escape된 따옴표(`\"`)는 별도 처리 없이 그대로 남긴다. | ||||||||||||||||||||||||||||||||||||||||
| private func stripQuotes(_ value: String) -> String { | ||||||||||||||||||||||||||||||||||||||||
| guard value.count >= 2 else { return value } | ||||||||||||||||||||||||||||||||||||||||
| let first = value.first! | ||||||||||||||||||||||||||||||||||||||||
| let last = value.last! | ||||||||||||||||||||||||||||||||||||||||
| if (first == "\"" && last == "\"") || (first == "'" && last == "'") { | ||||||||||||||||||||||||||||||||||||||||
| return String(value.dropFirst().dropLast()) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| return value | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+50
to
+59
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 강제 언래핑 제거 필요.
경로 지침에 따라 강제 언래핑( 🛡️ 제안하는 수정 private func stripQuotes(_ value: String) -> String {
guard value.count >= 2 else { return value }
- let first = value.first!
- let last = value.last!
+ guard let first = value.first, let last = value.last else { return value }
if (first == "\"" && last == "\"") || (first == "'" && last == "'") {
return String(value.dropFirst().dropLast())
}
return value
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Path instructions |
||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| // Copyright © 2026 Devault. All rights reserved | ||
|
|
||
| import Foundation | ||
|
|
||
| /// JSON credential 문서 감지. `JSONSerialization`으로 파싱한 뒤 구조적 힌트로 종류를 판별한다. | ||
| /// | ||
| /// 판별 우선순위: | ||
| /// 1. `type == "service_account"` → GCP · (project_id · client_email에 `firebase` 포함 시) Firebase Service Account | ||
| /// 2. `installed` 또는 `web` 하위 오브젝트 → Google OAuth Client | ||
| /// 3. 최상위 또는 하위 오브젝트에 `aws_access_key_id` → AWS Credentials | ||
| /// 4. 최상위에 `client_id` + `client_secret` → Generic OAuth Client Credentials | ||
| /// | ||
| /// 지원 범위: 최상위가 dict인 JSON만 인식. 배열 · 스칼라 루트는 감지 대상이 아님. | ||
| struct JSONCredentialDetector: SecretDetector { | ||
| func detect(_ value: SensitiveString, context: DetectorContext) -> DetectionResult? { | ||
| value.withUnsafeAccess { raw in | ||
| guard let data = raw.data(using: .utf8), | ||
| let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { | ||
| return nil | ||
| } | ||
|
|
||
| if let result = detectServiceAccount(root: root) { return result } | ||
| if let result = detectGoogleOAuthClient(root: root) { return result } | ||
| if let result = detectAWSCredentials(root: root) { return result } | ||
| if let result = detectGenericOAuth(root: root) { return result } | ||
| return nil | ||
| } | ||
| } | ||
|
|
||
| private func detectServiceAccount(root: [String: Any]) -> DetectionResult? { | ||
| guard (root["type"] as? String) == "service_account" else { return nil } | ||
| let projectId = root["project_id"] as? String | ||
| let clientEmail = root["client_email"] as? String | ||
| let isFirebase = (projectId?.lowercased().contains("firebase") == true) | ||
| || (clientEmail?.lowercased().contains("firebase") == true) | ||
| let kind: DetectedMetadata.JSONCredentialInfo.Kind = isFirebase | ||
| ? .firebaseServiceAccount | ||
| : .gcpServiceAccount | ||
| let service = isFirebase ? "Firebase Service Account" : "GCP Service Account" | ||
| let info = DetectedMetadata.JSONCredentialInfo( | ||
| kind: kind, | ||
| projectId: projectId, | ||
| clientEmail: clientEmail | ||
| ) | ||
| return DetectionResult( | ||
| candidates: [.init(service: service, displayLabel: service, confidence: .high)], | ||
| metadata: .json(info) | ||
| ) | ||
| } | ||
|
|
||
| private func detectGoogleOAuthClient(root: [String: Any]) -> DetectionResult? { | ||
| for key in ["installed", "web"] { | ||
| guard let sub = root[key] as? [String: Any], | ||
| let clientId = sub["client_id"] as? String else { continue } | ||
| let info = DetectedMetadata.JSONCredentialInfo( | ||
| kind: .googleOAuthClient, | ||
| clientId: clientId, | ||
| redirectUris: sub["redirect_uris"] as? [String] ?? [] | ||
| ) | ||
| return DetectionResult( | ||
| candidates: [.init( | ||
| service: "Google OAuth Client", | ||
| displayLabel: "Google OAuth Client", | ||
| confidence: .high | ||
| )], | ||
| metadata: .json(info) | ||
| ) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| private func detectAWSCredentials(root: [String: Any]) -> DetectionResult? { | ||
| guard hasKey("aws_access_key_id", in: root) else { return nil } | ||
| let info = DetectedMetadata.JSONCredentialInfo(kind: .awsCredentials) | ||
| return DetectionResult( | ||
| candidates: [.init( | ||
| service: "AWS Credentials", | ||
| displayLabel: "AWS Credentials", | ||
| confidence: .high | ||
| )], | ||
| metadata: .json(info) | ||
| ) | ||
| } | ||
|
|
||
| private func detectGenericOAuth(root: [String: Any]) -> DetectionResult? { | ||
| guard let clientId = root["client_id"] as? String, | ||
| root["client_secret"] != nil else { return nil } | ||
| let info = DetectedMetadata.JSONCredentialInfo(kind: .generic, clientId: clientId) | ||
| return DetectionResult( | ||
| candidates: [.init( | ||
| service: "OAuth Client Credentials", | ||
| displayLabel: "OAuth Client Credentials", | ||
| confidence: .medium | ||
| )], | ||
| metadata: .json(info) | ||
| ) | ||
| } | ||
|
|
||
| /// 최상위 dict, 또는 한 단계 아래의 dict 값에서만 키 존재를 확인. 그보다 깊은 중첩은 스캔하지 않는다. | ||
| private func hasKey(_ target: String, in root: [String: Any]) -> Bool { | ||
| if root[target] != nil { return true } | ||
| for value in root.values { | ||
| if let sub = value as? [String: Any], sub[target] != nil { return true } | ||
| } | ||
| return false | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| // Copyright © 2026 Devault. All rights reserved | ||
|
|
||
| import Foundation | ||
|
|
||
| /// `eyJ` prefix + `.` 3-part base64URL 형태의 JWT 감지. header/payload를 파싱해 알고리즘 · issuer · exp 등을 추출. | ||
| /// | ||
| /// 후보 chip은 부여하지 않는다. JWT는 포맷 정보일 뿐 서비스 식별자가 아니므로 metadata만 채운다. | ||
| /// 서비스가 있는 JWT 기반 토큰(예: Mapbox `pk.eyJ`)은 앞선 `PrefixRegexDetector`에서 이미 매칭된다. | ||
| struct JWTDetector: SecretDetector { | ||
| func detect(_ value: SensitiveString, context: DetectorContext) -> DetectionResult? { | ||
| value.withUnsafeAccess { raw in | ||
| guard raw.hasPrefix("eyJ") else { return nil } | ||
| let parts = raw.split(separator: ".", omittingEmptySubsequences: false) | ||
| guard parts.count == 3, | ||
| !parts[0].isEmpty, | ||
| !parts[1].isEmpty, | ||
| let headerData = Base64URL.decode(String(parts[0])), | ||
| let payloadData = Base64URL.decode(String(parts[1])), | ||
| let header = try? JSONSerialization.jsonObject(with: headerData) as? [String: Any], | ||
| let payload = try? JSONSerialization.jsonObject(with: payloadData) as? [String: Any] | ||
| else { return nil } | ||
|
|
||
| let algorithm = header["alg"] as? String | ||
| let issuer = payload["iss"] as? String | ||
| let subject = payload["sub"] as? String | ||
| let expiresAt: Date? = (payload["exp"] as? NSNumber).map { | ||
| Date(timeIntervalSince1970: $0.doubleValue) | ||
| } | ||
|
|
||
| return DetectionResult( | ||
| candidates: [], | ||
| metadata: .jwt(.init( | ||
| algorithm: algorithm, | ||
| issuer: issuer, | ||
| subject: subject, | ||
| expiresAt: expiresAt | ||
| )) | ||
| ) | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| // Copyright © 2026 Devault. All rights reserved | ||
|
|
||
| import Foundation | ||
|
|
||
| struct PEMDetector: SecretDetector { | ||
| let rules: [PEMHeaderRule] | ||
|
|
||
| func detect(_ value: SensitiveString, context: DetectorContext) -> DetectionResult? { | ||
| value.withUnsafeAccess { raw in | ||
| let normalized = raw.trimmingCharacters(in: .whitespacesAndNewlines) | ||
| guard let rule = rules.first(where: { normalized.contains($0.header) }) else { | ||
| return nil | ||
| } | ||
| let metadata: DetectedMetadata | ||
| if rule.isCertificate { | ||
| metadata = .certificate(.init()) | ||
| } else { | ||
| let algorithm: String? = { | ||
| guard rule.keyType == "OpenSSH", | ||
| normalized.lowercased().contains("ed25519") else { return nil } | ||
| return "ed25519" | ||
| }() | ||
| metadata = .pemKey(.init(keyType: rule.keyType, algorithm: algorithm)) | ||
| } | ||
| return DetectionResult(candidates: [], metadata: metadata) | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,62 @@ | ||||||||||||||||||||||||||||||||||||||||||
| // Copyright © 2026 Devault. All rights reserved | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| import Foundation | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| struct PrefixRegexDetector: SecretDetector { | ||||||||||||||||||||||||||||||||||||||||||
| let prefixRules: [PrefixRule] | ||||||||||||||||||||||||||||||||||||||||||
| /// Init 시점에 한 번 컴파일해서 재사용. 매 keystroke마다 재컴파일하지 않도록. | ||||||||||||||||||||||||||||||||||||||||||
| private let compiledRegex: [(rule: RegexRule, regex: Regex<AnyRegexOutput>)] | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| init(prefixRules: [PrefixRule], regexRules: [RegexRule]) { | ||||||||||||||||||||||||||||||||||||||||||
| self.prefixRules = prefixRules | ||||||||||||||||||||||||||||||||||||||||||
| self.compiledRegex = regexRules.compactMap { rule in | ||||||||||||||||||||||||||||||||||||||||||
| guard let regex = try? Regex(rule.pattern) else { return nil } | ||||||||||||||||||||||||||||||||||||||||||
| return (rule, regex) | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| func detect(_ value: SensitiveString, context: DetectorContext) -> DetectionResult? { | ||||||||||||||||||||||||||||||||||||||||||
| value.withUnsafeAccess { raw in | ||||||||||||||||||||||||||||||||||||||||||
| let prefixMatches = prefixRules.filter { rule in | ||||||||||||||||||||||||||||||||||||||||||
| guard raw.hasPrefix(rule.prefix) else { return false } | ||||||||||||||||||||||||||||||||||||||||||
| if let min = rule.minLength, raw.count < min { return false } | ||||||||||||||||||||||||||||||||||||||||||
| if let ctx = rule.requiresContext, !containsContext(raw, ctx: ctx) { | ||||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| if !prefixMatches.isEmpty { | ||||||||||||||||||||||||||||||||||||||||||
| let candidates = prefixMatches | ||||||||||||||||||||||||||||||||||||||||||
| .map { r in | ||||||||||||||||||||||||||||||||||||||||||
| ServiceCandidate( | ||||||||||||||||||||||||||||||||||||||||||
| service: r.service, | ||||||||||||||||||||||||||||||||||||||||||
| displayLabel: r.displayLabel, | ||||||||||||||||||||||||||||||||||||||||||
| confidence: r.confidence | ||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| .sorted { $0.confidence > $1.confidence } | ||||||||||||||||||||||||||||||||||||||||||
| return DetectionResult(candidates: candidates) | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| for (rule, regex) in compiledRegex { | ||||||||||||||||||||||||||||||||||||||||||
| guard let match = try? regex.wholeMatch(in: raw), match != nil else { continue } | ||||||||||||||||||||||||||||||||||||||||||
| return DetectionResult(candidates: [ | ||||||||||||||||||||||||||||||||||||||||||
| ServiceCandidate( | ||||||||||||||||||||||||||||||||||||||||||
| service: rule.service, | ||||||||||||||||||||||||||||||||||||||||||
| displayLabel: rule.displayLabel, | ||||||||||||||||||||||||||||||||||||||||||
| confidence: rule.confidence | ||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||
| ]) | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+41
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== locate file =="
git ls-files | rg 'Projects/DVDomain/Sources/Detection/Detector/PrefixRegexDetector.swift|swift' | head -50 || true
echo "== file excerpt =="
if [ -f Projects/DVDomain/Sources/Detection/Detector/PrefixRegexDetector.swift ]; then
nl -ba Projects/DVDomain/Sources/Detection/Detector/PrefixRegexDetector.swift | sed -n '1,120p'
fi
echo "== Swift availability =="
if command -v swift >/dev/null 2>&1; then
swift --version
tmpdir="$(mktemp -d)"
cat > "$tmpdir/Probe.swift" <<'SWIFT'
public struct DetectionResult: Equatable {
public var candidates: [String]
}
`@available`(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
`@available`(macCatalyst 15.0, *)
public func probe<Regex: RegexComponent>(target: String, regex: Regex) -> DetectionResult {
do {
for (rule, r) in [(0, regex)] {
guard let match = try? r.wholeMatch(in: target), match != nil else { continue }
return DetectionResult(candidates: ["ok"])
}
} catch {
}
return DetectionResult(candidates: [])
}
`@available`(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
`@available`(macCatalyst 15.0, *)
let result = probe(target: "abc", regex: #"abc"#)
print(result)
SWIFT
swiftc "$tmpdir/Probe.swift" -o "$tmpdir/Probe" 2>&1 || true
else
echo "swift not available"
fi
echo "== Search related patterns =="
rg -n "guard let match = try? .*wholeMatch|wholeMatch\\(in:|Match\\?|Match! nil" .Repository: DevaultProject/Devault-macOS Length of output: 3440 🌐 Web query:
💡 Result: In Swift, the behavior requested in SE-0230 was implemented in Swift 5.0 [1][2]. This change ensures that the Citations:
중복된 nil 체크를 제거하세요. Swift의 🧹 제안 수정 for (rule, regex) in compiledRegex {
- guard let match = try? regex.wholeMatch(in: raw), match != nil else { continue }
+ guard (try? regex.wholeMatch(in: raw)) != nil else { continue }
return DetectionResult(candidates: [📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /// context 매칭. context에 letter가 없으면 (심볼·숫자만) raw 전체 lowercasing을 생략해 대용량 입력에서의 복사 비용을 아낀다. | ||||||||||||||||||||||||||||||||||||||||||
| private func containsContext(_ raw: String, ctx: String) -> Bool { | ||||||||||||||||||||||||||||||||||||||||||
| let needsFolding = ctx.contains(where: { $0.isLetter }) | ||||||||||||||||||||||||||||||||||||||||||
| return needsFolding | ||||||||||||||||||||||||||||||||||||||||||
| ? raw.lowercased().contains(ctx.lowercased()) | ||||||||||||||||||||||||||||||||||||||||||
| : raw.contains(ctx) | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| // Copyright © 2026 Devault. All rights reserved | ||
|
|
||
| import Foundation | ||
|
|
||
| /// 파이프라인의 각 감지 스텝. | ||
| /// | ||
| /// - `nil`을 반환하면 다음 detector로 fall-through. | ||
| /// - non-nil을 반환하면 그 결과가 최종 결과 (뒤 detector는 실행 안 됨). | ||
| protocol SecretDetector: Sendable { | ||
| func detect(_ value: SensitiveString, context: DetectorContext) -> DetectionResult? | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Azure SQL 판별 조건이 너무 넓어 오탐 위험이 있습니다.
"Server="와"Database="만으로 Azure SQL을 판별하면, 온프레미스 SQL Server나 다른 ADO.NET 커넥션 문자열도 Azure SQL로 잘못 분류될 수 있습니다. 다른 두 조건(DefaultEndpointsProtocol=,Endpoint=sb://)은 Azure 고유 마커라 안전하지만, 이 조건만 예외입니다. PR 목표가 서비스 자동 채움이므로, 잘못된 서비스가 채워지면 사용자 경험에 직접 영향을 줍니다..database.windows.net같은 Azure 전용 호스트 마커를 추가 조건으로 요구하세요.🎯 Azure 전용 마커 추가로 오탐 축소
📝 Committable suggestion
🤖 Prompt for AI Agents