| name | update-codeql-query-dataflow-swift |
|---|---|
| description | Update CodeQL queries for Swift from legacy v1 dataflow API to modern v2 shared dataflow API. Use this skill when migrating Swift queries to use DataFlow::ConfigSig modules, ensuring query results remain equivalent through TDD. |
This skill guides you through migrating Swift CodeQL queries from the legacy v1 (language-specific) dataflow API to the modern v2 (shared) dataflow API while ensuring query results remain equivalent.
- Migrating Swift queries using deprecated
DataFlow::ConfigurationorTaintTracking::Configurationclasses - Updating queries to use
DataFlow::ConfigSigmodules - Modernizing Swift queries to use the shared dataflow library
- Ensuring query result equivalence during dataflow API migration
- Existing Swift CodeQL query using v1 dataflow API that you want to migrate
- Existing unit tests for the query
- Understanding of the query's detection purpose
- Access to CodeQL Development MCP Server tools
- macOS environment - Swift CodeQL analysis requires macOS and Xcode
Important: Swift CodeQL analysis requires macOS because the Swift extractor depends on
xcodebuildand macOS SDK frameworks.
v1 (Legacy):
class MyConfig extends TaintTracking::Configuration {
MyConfig() { this = "MyConfig" }
override predicate isSource(DataFlow::Node source) { ... }
override predicate isSink(DataFlow::Node sink) { ... }
override predicate isSanitizer(DataFlow::Node node) { ... }
override predicate isAdditionalTaintStep(DataFlow::Node n1, DataFlow::Node n2) { ... }
}v2 (Modern):
module MyConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) { ... }
predicate isSink(DataFlow::Node sink) { ... }
predicate isBarrier(DataFlow::Node node) { ... }
predicate isAdditionalFlowStep(DataFlow::Node n1, DataFlow::Node n2) { ... }
}
module MyFlow = TaintTracking::Global<MyConfig>;| v1 API | v2 API | Purpose |
|---|---|---|
DataFlow::Configuration |
DataFlow::ConfigSig |
Configuration signature |
isSanitizer |
isBarrier |
Stop data flow propagation |
isAdditionalTaintStep |
isAdditionalFlowStep |
Custom flow steps |
this.hasFlow(source, sink) |
MyFlow::flow(source, sink) |
Query flow paths |
Swift dataflow uses these core node representations:
ExprNode: AST expression nodes (function calls, member access)ParameterNode: Function parameter nodesPatternNode: Pattern binding nodes (let/var bindings)SsaDefinitionNode: SSA definition nodes for variable trackingCaptureNode: Closure capture nodesInoutReturnNode: Return flow through inout parameters
Critical: Before any code changes, capture current query behavior.
Use codeql_test_run to establish baseline:
{
"testPath": "<query-pack>/test/{QueryName}",
"searchPath": ["<query-pack>"]
}Save the output - this is your reference for query result equivalence.
Create a reference file with current results:
cp <query-pack>/test/{QueryName}/{QueryName}.expected \
<query-pack>/test/{QueryName}/{QueryName}.expected.v1-baselineThis ensures you can verify equivalence after migration.
Review the query for v1 API usage:
class X extends DataFlow::Configurationorclass X extends TaintTracking::ConfigurationisSanitizerpredicatesisAdditionalTaintSteppredicatesthis.hasFlow(source, sink)queries
Identify how the query uses Swift dataflow constructs:
- FlowSource: Base class for all flow sources
- RemoteFlowSource: Network sources (URLSession, Alamofire, etc.)
- LocalFlowSource: Local sources (UserDefaults, Keychain, etc.)
- Swift sinks: SQL injection, command injection, path traversal, XSS
- Framework-specific flows: Foundation, UIKit, CryptoKit, Alamofire, Realm
Before:
import swift
import codeql.swift.dataflow.DataFlow
import codeql.swift.dataflow.TaintTracking
class SqlInjectionConfig extends TaintTracking::Configuration {
SqlInjectionConfig() { this = "SqlInjectionConfig" }
override predicate isSource(DataFlow::Node source) {
source instanceof RemoteFlowSource
}
override predicate isSink(DataFlow::Node sink) {
exists(CallExpr call |
call.getStaticTarget().getName().matches("%raw%SQL%") and
sink.asExpr() = call.getAnArgument().getExpr()
)
}
override predicate isSanitizer(DataFlow::Node node) {
exists(CallExpr call |
call.getStaticTarget().getName() = "sanitize" and
node.asExpr() = call
)
}
}
from SqlInjectionConfig cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "SQL injection from $@", source.getNode(), "user input"After:
import swift
import codeql.swift.dataflow.DataFlow
import codeql.swift.dataflow.TaintTracking
module SqlInjectionConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
source instanceof RemoteFlowSource
}
predicate isSink(DataFlow::Node sink) {
exists(CallExpr call |
call.getStaticTarget().getName().matches("%raw%SQL%") and
sink.asExpr() = call.getAnArgument().getExpr()
)
}
predicate isBarrier(DataFlow::Node node) {
exists(CallExpr call |
call.getStaticTarget().getName() = "sanitize" and
node.asExpr() = call
)
}
}
module SqlInjectionFlow = TaintTracking::Global<SqlInjectionConfig>;
from SqlInjectionFlow::PathNode source, SqlInjectionFlow::PathNode sink
where SqlInjectionFlow::flowPath(source, sink)
select sink.getNode(), source, sink, "SQL injection from $@", source.getNode(), "user input"isSanitizer→isBarrier: Change method name only, logic unchangedisAdditionalTaintStep→isAdditionalFlowStep: Change method name only
Replace cfg.hasFlow(source, sink) with MyFlow::flow(source, sink):
- Remove configuration variable from
fromclause - Use module flow predicate directly
- For path queries, use
MyFlow::PathNodeandMyFlow::flowPath(source, sink)
Swift provides built-in flow source classes in codeql.swift.dataflow.FlowSources:
import codeql.swift.dataflow.FlowSources
predicate isSource(DataFlow::Node source) {
// Any remote flow source (network, URL schemes, etc.)
source instanceof RemoteFlowSource
or
// Local flow sources (file system, preferences, etc.)
source instanceof LocalFlowSource
or
// All flow sources
source instanceof FlowSource
}Remote Flow Source Examples:
- URLSession delegate methods
- Alamofire response handlers
- WebKit navigation/JavaScript
- Custom URL scheme handlers
- Push notification payloads
Local Flow Source Examples:
- UserDefaults values
- Keychain data
- File system operations
- Pasteboard contents
Common Swift security sinks:
predicate isSink(DataFlow::Node sink) {
// SQL Injection (SQLite, GRDB, Realm raw queries)
exists(CallExpr call |
call.getStaticTarget().getName().matches(["%execute%", "%raw%", "%prepare%"]) and
sink.asExpr() = call.getAnArgument().getExpr()
)
or
// Command Injection (Process, NSTask)
exists(CallExpr call |
call.getStaticTarget().getName() in ["launch", "run"] and
call.getStaticTarget().getDeclaringDecl().getName() = "Process" and
sink.asExpr() = call.getQualifier().(MemberRefExpr).getBase()
)
or
// Path Traversal (FileManager, URL file operations)
exists(CallExpr call |
call.getStaticTarget().getName().matches(["%contentsOfFile%", "%write%", "%createFile%"]) and
sink.asExpr() = call.getAnArgument().getExpr()
)
or
// JavaScript Injection (WKWebView evaluateJavaScript)
exists(CallExpr call |
call.getStaticTarget().getName() = "evaluateJavaScript" and
sink.asExpr() = call.getArgument(0).getExpr()
)
or
// Predicate Injection (NSPredicate)
exists(CallExpr call |
call.getStaticTarget().getName() = "init" and
call.getStaticTarget().getDeclaringDecl().getName() = "NSPredicate" and
sink.asExpr() = call.getArgument(0).getExpr()
)
}Track flows through iOS/macOS framework constructs:
Alamofire Flows:
predicate isAdditionalFlowStep(DataFlow::Node n1, DataFlow::Node n2) {
// Alamofire response data
exists(CallExpr call |
call.getStaticTarget().getName() in ["responseJSON", "responseData", "responseString"] and
n1.asExpr() = call.getQualifier() and
n2.asExpr() = call
)
}Realm Swift Flows:
predicate isAdditionalFlowStep(DataFlow::Node n1, DataFlow::Node n2) {
// Realm object property access
exists(MemberRefExpr access |
access.getBase().getType().getName().matches("%Object") and
n1.asExpr() = access.getBase() and
n2.asExpr() = access
)
}CryptoKit Flows:
predicate isAdditionalFlowStep(DataFlow::Node n1, DataFlow::Node n2) {
// Encryption/decryption flows
exists(CallExpr call |
call.getStaticTarget().getName() in ["seal", "open", "combined"] and
n1.asExpr() = call.getAnArgument().getExpr() and
n2.asExpr() = call
)
}Common sanitization patterns in Swift:
predicate isBarrier(DataFlow::Node node) {
// String validation with regex
exists(CallExpr call |
call.getStaticTarget().getName() in ["matches", "range"] and
call.getStaticTarget().getDeclaringDecl().getName() = "NSRegularExpression" and
node.asExpr() = call.getQualifier()
)
or
// URL encoding
exists(CallExpr call |
call.getStaticTarget().getName() = "addingPercentEncoding" and
node.asExpr() = call
)
or
// Guard statement validation
exists(GuardStmt guard |
node.asExpr() = guard.getACondition().(CallExpr)
)
}Use codeql_query_compile to check for errors:
{
"queryPath": "<query-pack>/src/{QueryName}/{QueryName}.ql",
"searchPath": ["<query-pack>"]
}Fix any compilation errors before testing.
Use codeql_test_run on migrated query:
{
"testPath": "<query-pack>/test/{QueryName}",
"searchPath": ["<query-pack>"]
}Compare results with v1 baseline:
diff <query-pack>/test/{QueryName}/{QueryName}.expected \
<query-pack>/test/{QueryName}/{QueryName}.expected.v1-baselineResults should be identical. Any difference indicates:
- Migration error in v2 configuration
- Bug in v1 query that was unintentionally corrected
- Intentional improvement (document this)
If tests pass and results match baseline, the migration is complete.
If there are differences:
- Analyze each difference - Is it a bug fix or regression?
- Update tests only if you've confirmed behavior improvement
- Document changes in query comments
Once migration is verified:
rm <query-pack>/test/{QueryName}/{QueryName}.expected.v1-baselineUpdate QLDoc comments:
/**
* @name SQL Injection
* @description Using unsanitized user input in SQL queries...
* @kind path-problem
* @problem.severity error
* @precision high
* @id swift/sql-injection
* @tags security
* external/cwe/cwe-089
*/codeql_query_compile: Compile and validate query syntaxcodeql_query_format: Format CodeQL query filescodeql_query_run: Run query against databases
codeql_test_run: Run query unit testscodeql_test_extract: Extract test databasescodeql_test_accept: Accept test results as baseline
codeql_bqrs_decode: Decode query resultscodeql_bqrs_interpret: Interpret results in various formats
codeql_pack_install: Install query pack dependencies
import swift // Main Swift library
import codeql.swift.dataflow.DataFlow // Core dataflow
import codeql.swift.dataflow.TaintTracking // Taint tracking
import codeql.swift.dataflow.FlowSources // Source definitionsimport codeql.swift.controlflow.ControlFlowGraph // CFG nodes
import codeql.swift.controlflow.BasicBlocks // Basic blocksimport codeql.swift.elements // All AST elements
import codeql.swift.elements.expr.CallExpr // Call expressions
import codeql.swift.elements.decl.Function // Functionsimport codeql.swift.security.SqlInjectionExtensions
import codeql.swift.security.CommandInjectionExtensions
import codeql.swift.security.PathInjectionExtensions
import codeql.swift.security.XXEExtensions
import codeql.swift.security.CleartextStorageDatabaseExtensionsEnsure all required predicates are imported:
// Include flow definitions
module MyFlow = TaintTracking::Global<MyConfig>;
import MyFlow::PathGraph // For path-problem queries// Wrong: Using DataFlow::PathNode directly
from DataFlow::PathNode source, DataFlow::PathNode sink
// Correct: Using module-qualified PathNode
from MyFlow::PathNode source, MyFlow::PathNode sinkThe most common error - ensure all isSanitizer → isBarrier:
// v1 (wrong in v2)
predicate isSanitizer(DataFlow::Node node) { ... }
// v2 (correct)
predicate isBarrier(DataFlow::Node node) { ... }For @kind path-problem queries:
module MyFlow = TaintTracking::Global<MyConfig>;
import MyFlow::PathGraph // Required for path visualization- Check predicate names: Ensure
isSanitizer→isBarrier, etc. - Verify flow module usage: Use
MyFlow::flow()notcfg.hasFlow() - Compare flow paths: V2 may find additional valid paths
- Review test cases: Ensure test Swift code compiles correctly
- Check import statements: Ensure all modules are imported
- Verify module instantiation:
module MyFlow = TaintTracking::Global<MyConfig> - Check predicate signatures: Must match
DataFlow::ConfigSigexactly
- Extraction fails: Ensure running on macOS with Xcode installed
- Swift version mismatch: CodeQL supports Swift 5.4 through 6.2
- Framework not found: Check Swift SDK availability
Before considering migration complete:
- v1 baseline captured before any changes
- All
isSanitizerrenamed toisBarrier - All
isAdditionalTaintSteprenamed toisAdditionalFlowStep - Configuration class converted to module
- Flow module instantiated correctly
-
hasFlowreplaced with module flow predicate - Query compiles without errors
- All tests pass
- Results match v1 baseline (or differences documented)
- Query documentation updated
- v1 backup files removed
- Tests verified on macOS environment
- Analyzing Data Flow in Swift - Official Swift dataflow guide
- Swift Standard Library Reference - Swift CodeQL API reference
- Swift Built-in Queries - Reference implementations
- CodeQL TDD Generic Skill - General test-driven development workflow
- Swift Unit Test Skill - Creating Swift query tests
Your Swift dataflow migration is successful when:
- ✅ Query uses v2
DataFlow::ConfigSigmodule pattern - ✅ All predicate names follow v2 conventions
- ✅ Query compiles without errors
- ✅ All unit tests pass
- ✅ Results match v1 baseline (or improvements documented)
- ✅ Query documentation reflects v2 patterns
- ✅ Tests verified on macOS environment