Skip to content

Commit 812bde3

Browse files
stevehansenclaude
andcommitted
feat: Add cross-platform credential store for secure connection string management
Store database credentials in OS keychain (Windows Credential Manager, macOS Keychain, Linux libsecret) via `credentials add/list/remove` subcommand. Connection strings without explicit credentials auto-inject stored values, eliminating passwords from CLI args, config files, and process lists. Centralizes connection string normalization into ConnectionStringHelper.Resolve() across all 5 entry points. Mitigates STRIDE findings I-1 and I-2. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b871f65 commit 812bde3

15 files changed

Lines changed: 1177 additions & 38 deletions

CLAUDE.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,22 @@ Conditionally compiled (`#if !RELEASELIBRARY`) alongside the Optimize subsystem.
9999
- **Derived table flattening**: When `FlattenDerivedTables` is enabled, `DerivedTableFlattener` runs as a post-processing step after stripping. It replaces eligible `QueryDerivedTable` nodes with their inner `FROM` tree (single table or JOIN tree), rewrites column references, and merges WHERE clauses. Uses `OuterScopeColumnReferenceCollector` that stops at `QueryDerivedTable` boundaries to prevent corrupting shared AST object references.
100100
- **`ParametersToIgnore`**: Maps SQL functions (e.g., DATEADD) to parameter indexes that should be excluded from column reference analysis.
101101

102+
### Credential subsystem (`Optimize/` directory)
103+
104+
Conditionally compiled (`#if !RELEASELIBRARY`) alongside the Optimize subsystem.
105+
106+
22. **ICredentialStore** — Interface for platform-specific credential storage. Defines `Store()`, `Retrieve()`, `Remove()`, and `List()`. `StoredCredential` holds username + password. `CredentialStoreFactory.Create()` returns the platform-appropriate implementation (or null with a warning). `CredentialStoreFactory.BuildKey()` normalizes server/database into a `"server\database"` key (lowercase, trimmed).
107+
108+
23. **WindowsCredentialStore** — P/Invoke to `advapi32.dll` (Credential Manager). Uses `CRED_TYPE_GENERIC` with target prefix `"sqlinliner:"`. Username in `CREDENTIAL.UserName`, password as UTF-16 bytes in `CredentialBlob`. `List()` uses `CredEnumerate("sqlinliner:*")`.
109+
110+
24. **MacCredentialStore** — Wraps macOS `security` CLI (Keychain). Uses a JSON index file (`~/.sqlinliner/credentials.json`) for username lookup and listing (no passwords in index).
111+
112+
25. **LinuxCredentialStore** — Wraps `secret-tool` CLI (libsecret). Uses a JSON index file (`~/.sqlinliner/credentials.json`) for username lookup and listing. Constructor checks for `secret-tool` availability with install instructions.
113+
114+
26. **ConnectionStringHelper** — Static `Resolve(connectionString, store)` method that replaces the duplicated `SqlConnectionStringBuilder` normalization blocks across all 5 entry points. Sets `ApplicationName`, injects stored credentials if no explicit credentials or Integrated Security, falls back to Integrated Security.
115+
116+
27. **CredentialsCommand** — System.CommandLine subcommand (`credentials`) with `add`, `list`, and `remove` sub-commands. `add` prompts for username/password with masked input. `list` displays server/database/username table (never shows passwords). `remove` deletes from the OS credential store. Does not take `configOption`.
117+
102118
## Testing Patterns
103119

104120
Tests use **NUnit** with **Shouldly** assertions. The standard pattern:

README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,69 @@ Two tables are displayed:
514514

515515
Plus a summary of views skipped because they have no nested view references.
516516

517+
## Credential management
518+
519+
The `credentials` subcommand stores database credentials in your OS credential store (Windows Credential Manager, macOS Keychain, or Linux libsecret). This eliminates the need to put passwords in connection strings, CLI arguments, or config files.
520+
521+
### Storing credentials
522+
523+
```bash
524+
# Store credentials (prompts for password with masked input)
525+
sqlinliner credentials add -s myserver -d mydb -u myuser
526+
527+
# Store with prompted username too
528+
sqlinliner credentials add -s myserver -d mydb
529+
```
530+
531+
### Auto-injection
532+
533+
Once credentials are stored, connection strings without explicit credentials will automatically use the stored values:
534+
535+
```bash
536+
# Password-free connection string — credentials auto-injected from store
537+
sqlinliner -cs "Server=myserver;Database=mydb" -vn dbo.VHeavy
538+
539+
# Also works with subcommands
540+
sqlinliner validate -cs "Server=myserver;Database=mydb"
541+
sqlinliner optimize -cs "Server=myserver;Database=mydb"
542+
```
543+
544+
The resolution order is:
545+
1. Explicit `User Id` and `Password` in the connection string — used as-is
546+
2. `Integrated Security=true` in the connection string — used as-is
547+
3. Stored credentials matching the server/database — injected automatically
548+
4. Fallback — `Integrated Security=true` (Windows Authentication)
549+
550+
### Listing and removing credentials
551+
552+
```bash
553+
# List all stored credentials (passwords are never shown)
554+
sqlinliner credentials list
555+
556+
# Remove stored credentials
557+
sqlinliner credentials remove -s myserver -d mydb
558+
```
559+
560+
### Config files without passwords
561+
562+
With the credential store, config files can omit passwords entirely:
563+
564+
```json
565+
{
566+
"connectionString": "Server=myserver;Database=mydb"
567+
}
568+
```
569+
570+
### Platform support
571+
572+
| Platform | Backend |
573+
|----------|---------|
574+
| Windows | Credential Manager (advapi32.dll) |
575+
| macOS | Keychain (`security` CLI) |
576+
| Linux | libsecret (`secret-tool` CLI) |
577+
578+
On Linux, `secret-tool` must be installed (`sudo apt install libsecret-tools` on Ubuntu/Debian).
579+
517580
## Feature details
518581

519582
### Column stripping

STRIDE.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
| Field | Value |
66
|-------|-------|
77
| Application | sql-inliner |
8-
| Version | v1 |
8+
| Version | v2 |
99
| Created | 2026-02-28 |
1010
| Last Updated | 2026-02-28 |
1111
| Next Review | 2029-02-28 |
@@ -147,17 +147,18 @@ sql-inliner is a .NET CLI tool and NuGet library that optimizes SQL Server views
147147

148148
| ID | Threat | Attack Path | Likelihood | Impact | Score | Mitigation |
149149
|----|--------|-------------|------------|--------|-------|------------|
150-
| I-1 | Connection string exposure via CLI arguments | `--connection-string` visible in process list, shell history, CI/CD logs | 3 | 3 | **9** | **Partially mitigated.** Config file and environment-based alternatives exist, but CLI still accepts plaintext. No built-in redaction. |
151-
| I-2 | Connection strings in config files | `sqlinliner.json` stores connection strings with embedded passwords; file not gitignored by default | 3 | 3 | **9** | **Partially mitigated.** Users can use Windows Authentication (no password). Config file not gitignored — could be committed accidentally. |
150+
| I-1 | Connection string exposure via CLI arguments | `--connection-string` visible in process list, shell history, CI/CD logs | 3 | 3 | **9** | **Mitigated.** Built-in credential store stores credentials in OS keychain (Windows Credential Manager, macOS Keychain, Linux libsecret). Passwords never appear in CLI args, process lists, or shell history. |
151+
| I-2 | Connection strings in config files | `sqlinliner.json` stores connection strings with embedded passwords; file not gitignored by default | 3 | 3 | **9** | **Mitigated.** Config files can specify `Server=...;Database=...` without passwords. Credentials injected at runtime from OS credential store. |
152152
| I-3 | Session files contain view SQL and metadata | Session directories store full view SQL, execution plans, table names, machine/user names | 2 | 2 | 4 | Files are local to the user's machine. Default OS permissions apply. No automatic cleanup. |
153153
| I-4 | Exception messages may leak paths or SQL | Error messages expose file paths and database errors to the user | 2 | 1 | 2 | Acceptable for a CLI tool. Errors are displayed to the authenticated user only. |
154154
| I-5 | Benchmark reports contain environment info | HTML reports include `Environment.MachineName`, `Environment.UserName`, database version, table names | 2 | 2 | 4 | Reports are local files. Users should treat them as internal artifacts. |
155155

156156
**Countermeasures:**
157+
- Built-in credential store (`credentials` subcommand) stores credentials in OS keychain — passwords never appear in CLI args or config files
157158
- Windows Authentication / Integrated Security eliminates password exposure
159+
- `ConnectionStringHelper.Resolve()` auto-injects stored credentials at runtime
158160
- `SqlConnectionStringBuilder` normalizes connection strings (does not add passwords)
159161
- Session files are local with OS-level access control
160-
- **Gap:** `sqlinliner.json` is not in `.gitignore` by default — credential leak risk
161162
- **Gap:** No automatic cleanup of session directories
162163

163164
### D — Denial of Service
@@ -196,13 +197,13 @@ sql-inliner is a .NET CLI tool and NuGet library that optimizes SQL Server views
196197
| ID | Threat | Score | Status |
197198
|----|--------|-------|--------|
198199
| T-1 | SQL injection via `--view-name` in `DatabaseConnection.cs` | 8 | Mitigated ([#93](https://github.com/stevehansen/sql-inliner/issues/93)) |
199-
| I-1 | Connection string exposure via CLI arguments | 9 | Partially mitigated |
200-
| I-2 | Connection strings in config files (not gitignored) | 9 | Partially mitigated |
200+
| I-1 | Connection string exposure via CLI arguments | 9 | Mitigated (credential store) |
201+
| I-2 | Connection strings in config files (not gitignored) | 9 | Mitigated (credential store) |
201202

202203
### Residual Risks
203204

204205
- **T-1 (SQL Injection):** Fixed in [#93](https://github.com/stevehansen/sql-inliner/issues/93). `GetViewDefinition()` and `TryGetRawViewDefinition()` now use Dapper parameterized queries instead of string interpolation.
205-
- **I-1/I-2 (Credential Exposure):** Connection strings with embedded passwords can leak via process lists, shell history, CI logs, or accidentally committed config files. The recommended approach is Windows Authentication (no password in connection string). Adding `sqlinliner.json` to the `.gitignore` template would reduce the config file risk.
206+
- **I-1/I-2 (Credential Exposure):** Mitigated by built-in credential store (`credentials` subcommand). Credentials are stored in the OS keychain and auto-injected at runtime. Connection strings in CLI args and config files no longer need embedded passwords.
206207

207208
## 4. Security Controls Summary
208209

@@ -212,7 +213,7 @@ sql-inliner is a .NET CLI tool and NuGet library that optimizes SQL Server views
212213
| Authorization | Delegates to SQL Server permissions; tool requires VIEW DEFINITION, SELECT, and optionally CREATE/ALTER/DROP VIEW |
213214
| Input Validation | ScriptDom AST parsing validates SQL structure; `SqlConnectionStringBuilder` validates connection strings |
214215
| Output Encoding | SQL output generated by `Sql150ScriptGenerator` with proper escaping |
215-
| Secrets Management | Connection strings via CLI args or config file; no built-in secrets vault integration |
216+
| Secrets Management | Built-in OS credential store (`credentials` subcommand) for Windows Credential Manager, macOS Keychain, and Linux libsecret; auto-injects stored credentials into connection strings at runtime |
216217
| Audit Logging | Session logs with timestamps; `validate-errors.log` for batch operations |
217218
| Error Handling | Exceptions caught at command level; error messages displayed to user |
218219
| Dependency Security | CodeQL analysis in CI; no known vulnerable dependencies |
@@ -224,6 +225,7 @@ sql-inliner is a .NET CLI tool and NuGet library that optimizes SQL Server views
224225
| Version | Date | Reviewer | Changes |
225226
|---------|------|----------|---------|
226227
| v1 | 2026-02-28 | Claude Code (STRIDE analysis) | Initial threat model |
228+
| v2 | 2026-02-28 | Claude Code | Mitigated I-1, I-2: Added built-in OS credential store |
227229

228230
## 6. References
229231

0 commit comments

Comments
 (0)