@@ -19,14 +19,9 @@ final class SecurityScanTests: XCTestCase {
1919 /// Scans all Swift source files for patterns that look like hardcoded API keys.
2020 /// This catches accidental commits of real secrets.
2121 func testNoHardcodedAPIKeysInSource( ) throws {
22- let projectRoot = " /Volumes/Data/xcode/MLX Code/MLX Code "
22+ let projectRoot = try Self . sourceRoot ( )
2323 let fileManager = FileManager . default
2424
25- guard fileManager. fileExists ( atPath: projectRoot) else {
26- // If running on CI without the project directory, skip gracefully
27- throw XCTSkip ( " Project source directory not available " )
28- }
29-
3025 let enumerator = fileManager. enumerator ( atPath: projectRoot)
3126 var violations : [ String ] = [ ]
3227
@@ -76,19 +71,17 @@ final class SecurityScanTests: XCTestCase {
7671 /// Verifies that secrets are stored via KeychainManager (SecItem*), not UserDefaults.
7772 /// Scans non-test Swift files for patterns like UserDefaults.set(...apiKey...).
7873 func testSecretsNotInUserDefaults( ) throws {
79- let projectRoot = " /Volumes/Data/xcode/MLX Code/MLX Code "
74+ let projectRoot = try Self . sourceRoot ( )
8075 let fileManager = FileManager . default
8176
82- guard fileManager. fileExists ( atPath: projectRoot) else {
83- throw XCTSkip ( " Project source directory not available " )
84- }
85-
8677 let enumerator = fileManager. enumerator ( atPath: projectRoot)
8778 var violations : [ String ] = [ ]
8879
89- // Words that suggest a secret is being stored
80+ // Words that suggest a secret is being stored. Matched as whole words so that
81+ // identifiers like `maxTokens` (contains "token") or `credentialScanOnPush`
82+ // (contains "credential") are not flagged as secrets.
9083 let secretKeywords = [
91- " apikey " , " api_key " , " apiKey " ,
84+ " apikey " , " api_key " ,
9285 " secret " , " password " , " token " ,
9386 " credential " , " bearer " ,
9487 ]
@@ -105,13 +98,13 @@ final class SecurityScanTests: XCTestCase {
10598 let lines = content. components ( separatedBy: . newlines)
10699 for (lineNum, line) in lines. enumerated ( ) {
107100 let lower = line. lowercased ( )
101+ // Explicit, reviewed allowance for non-secret values (e.g. anti-CSRF tokens)
102+ if lower. contains ( " nosec " ) { continue }
108103 // Check for UserDefaults.standard.set or userDefaults.set with secret keywords
109104 if ( lower. contains ( " userdefaults " ) && lower. contains ( " .set " ) ) ||
110105 ( lower. contains ( " userdefaults " ) && lower. contains ( " forkey " ) ) {
111- for keyword in secretKeywords {
112- if lower. contains ( keyword) {
113- violations. append ( " \( file) : \( lineNum + 1 ) - Possible secret ' \( keyword) ' stored in UserDefaults " )
114- }
106+ for keyword in secretKeywords where Self . containsWholeWord ( keyword, in: lower) {
107+ violations. append ( " \( file) : \( lineNum + 1 ) - Possible secret ' \( keyword) ' stored in UserDefaults " )
115108 }
116109 }
117110 }
@@ -125,18 +118,16 @@ final class SecurityScanTests: XCTestCase {
125118
126119 /// Scans source for unsafe C functions that can cause buffer overflows.
127120 func testNoUnsafeCFunctions( ) throws {
128- let projectRoot = " /Volumes/Data/xcode/MLX Code/MLX Code "
121+ let projectRoot = try Self . sourceRoot ( )
129122 let fileManager = FileManager . default
130123
131- guard fileManager. fileExists ( atPath: projectRoot) else {
132- throw XCTSkip ( " Project source directory not available " )
133- }
134-
135124 let enumerator = fileManager. enumerator ( atPath: projectRoot)
136125 var violations : [ String ] = [ ]
137126
138- // Unsafe C functions per CLAUDE.md memory security rules
139- let unsafeFunctions = [ " strcpy( " , " strcat( " , " sprintf( " , " gets( " ]
127+ // Unsafe C functions per CLAUDE.md memory security rules. Matched with a leading
128+ // word boundary so Swift identifiers such as `listTargets(` are not mistaken for
129+ // a call to the C `gets(` function.
130+ let unsafeFunctions = [ " strcpy " , " strcat " , " sprintf " , " gets " ]
140131
141132 while let file = enumerator? . nextObject ( ) as? String {
142133 guard file. hasSuffix ( " .swift " ) || file. hasSuffix ( " .m " ) || file. hasSuffix ( " .h " ) else { continue }
@@ -151,8 +142,8 @@ final class SecurityScanTests: XCTestCase {
151142 if trimmed. hasPrefix ( " // " ) || trimmed. hasPrefix ( " * " ) { continue }
152143
153144 for fn in unsafeFunctions {
154- if line. contains ( fn) {
155- violations. append ( " \( file) : \( lineNum + 1 ) - Unsafe C function: \( fn) " )
145+ if line. range ( of : " \\ b \( fn) \\ s* \\ ( " , options : . regularExpression ) != nil {
146+ violations. append ( " \( file) : \( lineNum + 1 ) - Unsafe C function: \( fn) ( " )
156147 }
157148 }
158149 }
@@ -162,6 +153,28 @@ final class SecurityScanTests: XCTestCase {
162153 " Found unsafe C functions: \n \( violations. joined ( separator: " \n " ) ) " )
163154 }
164155
156+ // MARK: - Source-Scan Helpers
157+
158+ /// Locates the app's source directory relative to this test file so scans are
159+ /// hermetic and run identically on any machine or CI runner (no hardcoded paths).
160+ private static func sourceRoot( ) throws -> String {
161+ // #filePath -> <repo>/MLX Code Tests/SecurityScanTests.swift
162+ let repoRoot = URL ( fileURLWithPath: #filePath)
163+ . deletingLastPathComponent ( ) // MLX Code Tests
164+ . deletingLastPathComponent ( ) // repo root
165+ let source = repoRoot. appendingPathComponent ( " MLX Code " )
166+ guard FileManager . default. fileExists ( atPath: source. path) else {
167+ throw XCTSkip ( " Source directory not found at \( source. path) " )
168+ }
169+ return source. path
170+ }
171+
172+ /// Whole-word, case-insensitive containment check (word chars = [A-Za-z0-9_]).
173+ private static func containsWholeWord( _ word: String , in haystack: String ) -> Bool {
174+ haystack. range ( of: " \\ b \( NSRegularExpression . escapedPattern ( for: word) ) \\ b " ,
175+ options: [ . regularExpression, . caseInsensitive] ) != nil
176+ }
177+
165178 // MARK: - Input Sanitization for User Prompts
166179
167180 func testSanitizeUserInputRemovesNullBytes( ) {
0 commit comments