Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
Comment on lines +19 to +30

Copy link
Copy Markdown

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 전용 마커 추가로 오탐 축소
-        if raw.contains("Server=") && raw.contains("Database=") {
+        if raw.contains("Server=") && raw.contains("Database=")
+            && raw.lowercased().contains("database.windows.net") {
             return azureResult(service: "Azure SQL", label: "Azure SQL Connection String")
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 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=")
&& raw.lowercased().contains("database.windows.net") {
return azureResult(service: "Azure SQL", label: "Azure SQL Connection String")
}
return nil
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Projects/DVDomain/Sources/Detection/Detector/DatabaseURLDetector.swift`
around lines 19 - 30, Update detectAsAzureConnectionString so the Azure SQL
branch requires an Azure-specific host marker such as .database.windows.net in
addition to Server= and Database=. Preserve the existing Azure Storage and Azure
Service Bus checks, and return nil when the generic SQL markers lack the Azure
host marker.


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 []
}
}
11 changes: 11 additions & 0 deletions Projects/DVDomain/Sources/Detection/Detector/DetectorContext.swift
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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.swift

Repository: 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/Sources

Repository: DevaultProject/Devault-macOS

Length of output: 4330


detect(_:) 재귀 깊이 제한을 추가하세요.

EnvSetDetectorcontext.detect(...)로 VALUE를 다시 파이프라인에 넘기고, DetectorContext.detect(_:)execute(value:)를 그대로 호출합니다. 이 사이클이 반복되면 중첩된 KEY=VALUE 페이로드에서 재귀 깊이가 제한 없이 늘어납니다. DetectorContext.detect(_:) 또는 파이프라인 진입점에 깊이 제한을 두고 그 값을 전달한 뒤, 상한을 초과하면 재귀 감지를 중단하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Projects/DVDomain/Sources/Detection/Detector/DetectorContext.swift` around
lines 9 - 11, DetectorContext.detect(_:)를 통해 재진입하는 감지 파이프라인에 재귀 깊이 상태와 상한을 전달하도록
수정하고, 중첩된 KEY=VALUE 처리로 상한을 초과하면 추가 감지를 즉시 중단하세요. EnvSetDetector의
context.detect(...) 호출과 execute(value:) 진입 경로 모두에서 기존 감지 동작은 제한 이내에 유지하세요.

60 changes: 60 additions & 0 deletions Projects/DVDomain/Sources/Detection/Detector/EnvSetDetector.swift
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

강제 언래핑 제거 필요.

value.first!, value.last!는 직전의 value.count >= 2 검사로 현재는 안전합니다. 하지만 이 함수를 이후 수정할 때 앞의 길이 검사가 바뀌면 크래시로 이어질 위험이 있습니다. guard let 패턴으로 교체해 이 위험을 원천 차단하세요.

경로 지침에 따라 강제 언래핑(!) 사용 시 guard let / if let / ?? 대안을 제시해야 합니다: "강제 언래핑(!) 사용 시 반드시 지적하고 guard let / if let / ?? 대안을 제시하세요."

🛡️ 제안하는 수정
     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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// 앞뒤 짝이 맞는 `"` 또는 `'`만 벗겨낸다. 내부에 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
}
/// 앞뒤 짝이 맞는 `"` 또는 `'`만 벗겨낸다. 내부에 escape된 따옴표(`\"`)는 별도 처리 없이 그대로 남긴다.
private func stripQuotes(_ value: String) -> String {
guard value.count >= 2 else { return value }
guard let first = value.first, let last = value.last else { return value }
if (first == "\"" && last == "\"") || (first == "'" && last == "'") {
return String(value.dropFirst().dropLast())
}
return value
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Projects/DVDomain/Sources/Detection/Detector/EnvSetDetector.swift` around
lines 50 - 59, Remove the forced unwrapping of value.first and value.last in
stripQuotes by binding both properties with guard let and returning value when
either is unavailable. Preserve the existing quote-pair detection and stripping
behavior.

Source: 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
}
}
41 changes: 41 additions & 0 deletions Projects/DVDomain/Sources/Detection/Detector/JWTDetector.swift
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
))
)
}
}
}
28 changes: 28 additions & 0 deletions Projects/DVDomain/Sources/Detection/Detector/PEMDetector.swift
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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:

Swift 5.7 SE-0230 throw/throws? optional return flatten try?

💡 Result:

In Swift, the behavior requested in SE-0230 was implemented in Swift 5.0 [1][2]. This change ensures that the try? operator does not produce redundant, nested optionals when applied to an expression that already returns an optional value [1][3]. Before this change, applying try? to an expression that returned an optional (e.g., T?) would result in a double-optional (e.g., T??), which was often considered counter-intuitive and required manual flattening [4][5]. Since Swift 5.0, the try? operator exhibits the following behavior: - If the sub-expression returns a non-optional value, it is wrapped in an Optional (e.g., T becomes T?) [1][3]. - If the sub-expression already returns an Optional, no additional level of optionality is added; the result remains at a single level of optionality (e.g., T? remains T?) [1][3][2]. This aligns try? with other Swift features like optional chaining (?.) and conditional type casting (as?), which similarly collapse optional types rather than nesting them [2][6]. There is no automatic flattening of multiple existing levels of optionality (e.g., T?? would not become T? solely via try?); the operator simply ensures it does not introduce a new level of nesting when the underlying expression is already optional [7].

Citations:


중복된 nil 체크를 제거하세요.

Swift의 try? regex.wholeMatch(in: raw)는 이미 Match?를 반환하므로 guard let match = ... 뒤에서 match는 optional이 아닙니다. 이 뒤의 match != nil은 항상 참이라 제거하고, 존재 여부만 검사하세요. match는 이후 값으로도 사용하지 않습니다.

🧹 제안 수정
 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
)
])
}
for (rule, regex) in compiledRegex {
guard (try? regex.wholeMatch(in: raw)) != nil else { continue }
return DetectionResult(candidates: [
ServiceCandidate(
service: rule.service,
displayLabel: rule.displayLabel,
confidence: rule.confidence
)
])
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Projects/DVDomain/Sources/Detection/Detector/PrefixRegexDetector.swift`
around lines 41 - 50, Update the guard condition in the compiledRegex loop to
unwrap the result of regex.wholeMatch(in:) without the redundant match != nil
check, since the unwrapped match is only used for presence validation and not
referenced afterward.

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)
}
}
11 changes: 11 additions & 0 deletions Projects/DVDomain/Sources/Detection/Detector/SecretDetector.swift
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?
}
Loading