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
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,9 @@ Cargo.lock linguist-generated
linux/data/licenses/** linguist-generated
linux/build-aux/debian/copyright linguist-generated

# Other apps' files as those apps write them, binary plists and encrypted blobs included: kept
# byte for byte, so a Windows checkout does not turn their line endings into CRLF.
core/crates/foreign-import/tests/fixtures/** -text

# The Windows interop bindings, written by uniffi-bindgen-cs and csbindgen (windows/AGENTS.md, Bindings)
windows/src/TablePro.Interop/Generated/** linguist-generated eol=lf
5 changes: 4 additions & 1 deletion .github/workflows/core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,16 @@ jobs:
run: |
cargo check --locked --target "$TARGET" --features "$FEATURES" \
-p tablepro-core -p tablepro-net -p tablepro-storage -p tablepro-ssh -p tablepro-session \
-p tablepro-ai -p tablepro-mcp -p tablepro-test-support -p tablepro-test-fixtures \
-p tablepro-foreign-import -p tablepro-ai -p tablepro-mcp -p tablepro-test-support -p tablepro-test-fixtures \
-p tablepro-driver-cassandra -p tablepro-driver-clickhouse -p tablepro-driver-duckdb \
-p tablepro-driver-mongodb -p tablepro-driver-mssql -p tablepro-driver-mysql \
-p tablepro-driver-postgres -p tablepro-driver-redis -p tablepro-driver-sqlite
# The SQL Server driver's own: integrated login differs on every platform.
- name: Core unit tests
run: cargo test --locked --target "$TARGET" --features "$FEATURES" -p tablepro-core -p tablepro-driver-mssql --lib
# Each importer reads its app's files from this platform's own places.
- name: Importers from other apps
run: cargo test --locked --target "$TARGET" -p tablepro-foreign-import

sql-server-windows:
name: SQL Server integrated login (Windows x64)
Expand Down
2 changes: 1 addition & 1 deletion .oss-scanner/threat_model.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ The user is trusted: the SQL they type, the connections they create, the plugins
1. Code in this repository that parses server responses:
- Swift: `apple/Plugins/*DriverPlugin`, the HTTP and REST engines (Trino, ClickHouse, Etcd, Google REST, DynamoDB, Snowflake, Elasticsearch and others), Teradata's TD2 protocol, Kafka with its Snappy decoder, and the C interop that reads cells out of libpq, libmariadb, FreeTDS, libmongoc, hiredis and libcassandra.
- Rust: `core/crates/drivers/*`, `core/crates/net`.
2. File parsers: `apple/Packages/TableProCore/Sources/TableProTabularIO` (its own ZIP reader, XLSX XML, delimited text), `TableProImport` (`.tablepro` bundles, PBKDF2 and AES-GCM), `apple/TablePro/Core/Services/Export/ForeignApp/` (other apps' files, the Navicat cipher, a KDBX reader), `apple/TablePro/Core/Services/ProjectImport/`, `core/crates/core/src/import`.
2. File parsers: `apple/Packages/TableProCore/Sources/TableProTabularIO` (its own ZIP reader, XLSX XML, delimited text), `TableProImport` (`.tablepro` bundles, PBKDF2 and AES-GCM), `apple/TablePro/Core/Services/Export/ForeignApp/` and `core/crates/foreign-import` (other apps' files, the Navicat cipher, a KDBX reader), `apple/TablePro/Core/Services/ProjectImport/`, `core/crates/core/src/import`.
3. Deep links: `apple/TablePro/Core/Services/Infrastructure/DeeplinkParser.swift`, `ExternalConnectionGate.swift`, `apple/TablePro/Core/Utilities/Connection/ConnectionURLParser.swift`.
4. MCP server: `apple/TablePro/Core/MCP/` and `apple/Packages/TableProMacCore/Sources/TableProMCP/` (a hand-written HTTP parser, bearer tokens, PKCE pairing, Origin and Host checks, `MCPStatementGate`).
5. AI tools that run SQL: `apple/TablePro/Core/AI/Chat/Tools/`, `ToolApprovalCenter.swift`.
Expand Down
91 changes: 90 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"core/crates/ai",
"core/crates/core",
"core/crates/ffi",
"core/crates/foreign-import",
"core/crates/mcp",
"core/crates/net",
"core/crates/session",
Expand Down Expand Up @@ -42,6 +43,7 @@ tablepro-driver-mysql = { path = "core/crates/drivers/mysql" }
tablepro-driver-postgres = { path = "core/crates/drivers/postgres" }
tablepro-driver-redis = { path = "core/crates/drivers/redis" }
tablepro-driver-sqlite = { path = "core/crates/drivers/sqlite" }
tablepro-foreign-import = { path = "core/crates/foreign-import" }
tablepro-mcp = { path = "core/crates/mcp" }
tablepro-net = { path = "core/crates/net" }
tablepro-linux-platform = { path = "linux/crates/platform" }
Expand Down Expand Up @@ -150,6 +152,19 @@ url = "2"
percent-encoding = "2"
# The Windows Credential Manager, where the core keeps secrets on Windows.
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Security_Credentials"] }
# What other apps' saved connections are kept in: binary and XML property
# lists, XML, and the fixed-key ciphers those apps use for passwords.
plist = { version = "1.7", default-features = false }
roxmltree = "0.20"
aes = "0.8"
cbc = { version = "0.1", features = ["alloc"] }
blowfish = "0.9"
salsa20 = "0.10"
chacha20 = "0.9"
sha1 = "0.11"
sha2 = "0.11"
hex = "0.4"
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
zeroize = "1.9"
# The volume and index that name a file on Windows, which std reads only on nightly.
winapi-util = "0.1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,23 @@ public struct CollectedImport: Sendable {
public let unsuggestedQueries: Set<BundleRef>
public let oversizedQueries: [OversizedSavedQuery]
public let credentialsAborted: Bool
public let connectionNotes: [BundleRef: [String]]

public init(
bundle: ConnectionBundle,
source: ImportSource,
unsuggestedConnections: Set<BundleRef> = [],
unsuggestedQueries: Set<BundleRef> = [],
oversizedQueries: [OversizedSavedQuery] = [],
credentialsAborted: Bool = false
credentialsAborted: Bool = false,
connectionNotes: [BundleRef: [String]] = [:]
) {
self.bundle = bundle
self.source = source
self.unsuggestedConnections = unsuggestedConnections
self.unsuggestedQueries = unsuggestedQueries
self.oversizedQueries = oversizedQueries
self.credentialsAborted = credentialsAborted
self.connectionNotes = connectionNotes
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ public enum ConnectionImportAnalyzer {
groupPath: Array(groupPath),
tagNames: connection.tagNames,
duplicate: duplicate,
warnings: warnings(for: settings, canonicalTypeId: canonicalTypeId, environment: environment),
warnings: warnings(for: settings, canonicalTypeId: canonicalTypeId, environment: environment)
+ (collected.connectionNotes[connection.ref] ?? []),
unsupportedTypeId: canonicalTypeId == nil ? connection.settings.type : nil,
savedQueryCount: savedQueryCount,
resolutions: resolutions(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,31 @@ struct ConnectionImportAnalyzerTests {
#expect(row.isSelectedByDefault)
}

@Test("A connection's notes follow its own warnings, on that row only")
func connectionNotesFollowWarnings() throws {
var settings = Fixtures.settings()
settings.sslConfig = ExportableSSLConfig(mode: "sometimes")
let collected = CollectedImport(
bundle: try Fixtures.makeBundle(connections: [
BundleConnection(ref: "c1", settings: settings),
BundleConnection(ref: "c2", settings: Fixtures.settings(host: "other"))
]),
source: .foreignApp(name: "Sample App"),
connectionNotes: ["c1": ["SSL is on by default"]]
)

let preview = ConnectionImportAnalyzer.analyze(
collected,
library: ImportLibrarySnapshot(),
environment: Fixtures.makeEnvironment()
)

#expect(preview.connections[0].warnings.count == 2)
#expect(preview.connections[0].warnings.last == "SSL is on by default")
#expect(preview.connections[0].isSelectedByDefault)
#expect(preview.connections[1].warnings.isEmpty)
}

@Test("An iOS SSL spelling is recognized and does not warn")
func iOSSpellingDoesNotWarn() throws {
var settings = Fixtures.settings()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ struct BeekeeperStudioImporter: ForeignAppImporter {
return ForeignAppInventory(connections: 0, savedQueries: 0)
}
defer { sqlite3_close(db) }
// Only the rows collect imports: one with no connection type is skipped there.
let rows = (try? readSavedConnections(db: db)) ?? []
return ForeignAppInventory(
connections: (try? readSavedConnections(db: db).count) ?? 0,
connections: rows.count { Self.mapDriver($0.connectionType) != nil },
savedQueries: BeekeeperSavedQueryReader.count(db: db)
)
}
Expand All @@ -60,6 +62,7 @@ struct BeekeeperStudioImporter: ForeignAppImporter {
Self.logger.warning("Skipping a Beekeeper connection with no connection type")
continue
}
var notes: [String] = []
let settings = ExportableConnection(
name: row.name.isEmpty ? "Untitled" : row.name,
host: row.host.isEmpty ? "localhost" : row.host,
Expand All @@ -68,14 +71,15 @@ struct BeekeeperStudioImporter: ForeignAppImporter {
username: row.username ?? "",
type: type,
sshConfig: row.sshEnabled ? Self.buildSSHConfig(row) : nil,
sslConfig: row.ssl ? Self.buildSSLConfig(row) : nil,
sslConfig: buildSSLConfig(row, type: type, notes: &notes),
color: Self.mapColor(row.labelColor)
)
records.append(ForeignConnectionRecord(
sourceId: String(row.id),
settings: settings,
groupPath: row.connectionFolderId.flatMap { folderMap[$0] }.map { [$0] } ?? [],
credentials: userKey.flatMap { Self.extractCredentials(row: row, key: $0) }
credentials: userKey.flatMap { Self.extractCredentials(row: row, key: $0) },
notes: notes
))
}

Expand Down Expand Up @@ -156,18 +160,40 @@ struct BeekeeperStudioImporter: ForeignAppImporter {

// MARK: - SSL Mapping

private static func buildSSLConfig(_ row: SavedConnectionRow) -> ExportableSSLConfig {
let mode = row.sslRejectUnauthorized && !row.trustServerCertificate
? "Verify Identity"
: "Required"
private func buildSSLConfig(_ row: SavedConnectionRow, type: String, notes: inout [String]) -> ExportableSSLConfig? {
guard row.ssl else {
// With `ssl` off mssql still hands tedious `encrypt: true`, and the saved `trustServerCertificate`
// decides the certificate check. The other connectors connect in the clear.
guard type == "SQL Server" else { return nil }
let mode: SSLMode = row.trustServerCertificate ? .required : .verifyIdentity
notes.append(ForeignImportNote.tlsFromDriverDefault(mode, appName: displayName))
return ExportableSSLConfig(mode: mode.rawValue)
}
return ExportableSSLConfig(
mode: mode,
mode: Self.verifiesServer(row, type: type) ? SSLMode.verifyIdentity.rawValue : SSLMode.required.rawValue,
caCertificatePath: row.sslCaFile,
clientCertificatePath: row.sslCertFile,
clientKeyPath: row.sslKeyFile
)
}

// What each Beekeeper connector hands its driver (`lib/db/clients/*/*Connector.ts`). The saved
// `trustServerCertificate` is SQL Server's alone, and TLS on replaces it there.
private static func verifiesServer(_ row: SavedConnectionRow, type: String) -> Bool {
let hasCA = !(row.sslCaFile ?? "").isEmpty
let hasCert = !(row.sslCertFile ?? "").isEmpty
let hasKey = !(row.sslKeyFile ?? "").isEmpty
switch type {
case "SQL Server":
return !(hasCA && hasCert && hasKey) || row.sslRejectUnauthorized
// With no certificate file these connectors never check the server.
case "PostgreSQL", "Redshift", "CockroachDB", "MySQL", "MariaDB", "TiDB":
return row.sslRejectUnauthorized && (hasCA || hasCert || hasKey)
default:
return row.sslRejectUnauthorized
}
}

// MARK: - Color

private static func mapColor(_ raw: String?) -> String? {
Expand All @@ -187,7 +213,7 @@ struct BeekeeperStudioImporter: ForeignAppImporter {
// MARK: - Credentials

private func loadUserEncryptionKey() -> String? {
guard let data = try? Data(contentsOf: keyFileURL),
guard let data = try? ForeignAppFile.read(keyFileURL),
let payload = String(data: data, encoding: .utf8),
let decoded = BeekeeperEncryptor.decryptDictionary(payload, key: BeekeeperEncryptor.defaultKey),
let key = decoded["encryptionKey"] as? String else {
Expand Down
Loading
Loading