rds_ssm_connect 1.7.16 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/configLoader.js +12 -0
- package/connect.js +67 -7
- package/package.json +1 -1
- package/src/lib/Settings.svelte +20 -2
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/lib.rs +2 -0
- package/src-tauri/tauri.conf.json +1 -1
- package/test/configLoader.test.js +52 -0
package/configLoader.js
CHANGED
|
@@ -50,8 +50,10 @@ const REQUIRED_FIELDS = [
|
|
|
50
50
|
]
|
|
51
51
|
|
|
52
52
|
const VALID_RDS_TYPES = ['cluster', 'instance']
|
|
53
|
+
const VALID_ENGINES = ['postgres', 'mysql']
|
|
53
54
|
const REGION_PATTERN = /^[a-z]{2}(-[a-z]+-\d+)$/
|
|
54
55
|
const PORT_PATTERN = /^\d+$/
|
|
56
|
+
const SHELL_SAFE_PATTERN = /^[a-zA-Z0-9._!/-]+$/
|
|
55
57
|
|
|
56
58
|
export function validateProjectConfig(config) {
|
|
57
59
|
const errors = []
|
|
@@ -66,6 +68,16 @@ export function validateProjectConfig(config) {
|
|
|
66
68
|
errors.push(`rdsType must be one of: ${VALID_RDS_TYPES.join(', ')}`)
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
if (config.engine !== undefined && config.engine !== null && !VALID_ENGINES.includes(config.engine)) {
|
|
72
|
+
errors.push(`engine must be one of: ${VALID_ENGINES.join(', ')}`)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const field of ['secretPrefix', 'rdsPattern', 'database']) {
|
|
76
|
+
if (config[field] && !SHELL_SAFE_PATTERN.test(config[field])) {
|
|
77
|
+
errors.push(`${field} contains invalid characters (only alphanumeric, dots, underscores, hyphens, slashes, and ! allowed)`)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
69
81
|
if (config.region && !REGION_PATTERN.test(config.region)) {
|
|
70
82
|
errors.push(`Invalid region format: ${config.region}`)
|
|
71
83
|
}
|
package/connect.js
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from './configLoader.js'
|
|
15
15
|
|
|
16
16
|
// Package info for version checking
|
|
17
|
-
const packageJson = { name: 'rds_ssm_connect', version: '1.
|
|
17
|
+
const packageJson = { name: 'rds_ssm_connect', version: '1.8.0' }
|
|
18
18
|
|
|
19
19
|
const execAsync = promisify(exec)
|
|
20
20
|
|
|
@@ -422,6 +422,15 @@ async function _startPortForwarding(
|
|
|
422
422
|
)
|
|
423
423
|
}
|
|
424
424
|
|
|
425
|
+
// Validation patterns for security
|
|
426
|
+
const PROFILE_SAFE_PATTERN = /^[a-zA-Z0-9._-]+$/
|
|
427
|
+
const INSTANCE_ID_PATTERN = /^i-[a-f0-9]{8,17}$/
|
|
428
|
+
const HOSTNAME_PATTERN = /^[a-zA-Z0-9.-]+$/
|
|
429
|
+
|
|
430
|
+
function getDefaultPortForEngine(projectConfig) {
|
|
431
|
+
return projectConfig.engine === 'mysql' ? '3306' : '5432'
|
|
432
|
+
}
|
|
433
|
+
|
|
425
434
|
async function getRdsEndpoint(ENV, projectConfig) {
|
|
426
435
|
const { region, rdsType, rdsPattern } = projectConfig
|
|
427
436
|
|
|
@@ -438,13 +447,14 @@ async function getRdsEndpoint(ENV, projectConfig) {
|
|
|
438
447
|
|
|
439
448
|
async function getRdsPort(ENV, projectConfig) {
|
|
440
449
|
const { region, rdsType, rdsPattern } = projectConfig
|
|
450
|
+
const fallbackPort = getDefaultPortForEngine(projectConfig)
|
|
441
451
|
|
|
442
452
|
if (rdsType === 'cluster') {
|
|
443
453
|
const portCommand = `aws-vault exec ${ENV} -- aws rds describe-db-clusters --region ${region} --query "DBClusters[?Status=='available' && ends_with(DBClusterIdentifier, '${rdsPattern}')].Port | [0]" --output text`
|
|
444
|
-
return (await runCommand(portCommand)) ||
|
|
454
|
+
return (await runCommand(portCommand)) || fallbackPort
|
|
445
455
|
} else {
|
|
446
456
|
const portCommand = `aws-vault exec ${ENV} -- aws rds describe-db-instances --region ${region} --query "DBInstances[?DBInstanceStatus=='available' && contains(DBInstanceIdentifier, '${rdsPattern}')].Endpoint.Port | [0]" --output text`
|
|
447
|
-
return (await runCommand(portCommand)) ||
|
|
457
|
+
return (await runCommand(portCommand)) || fallbackPort
|
|
448
458
|
}
|
|
449
459
|
}
|
|
450
460
|
|
|
@@ -587,6 +597,10 @@ async function getProfilesForProjectKey(projectKey) {
|
|
|
587
597
|
// Includes keepalive (prevents SSM idle timeout) and auto-reconnect
|
|
588
598
|
// (transparently reconnects on the same port if session drops unexpectedly).
|
|
589
599
|
async function connect(projectKey, profile, options = {}) {
|
|
600
|
+
if (!PROFILE_SAFE_PATTERN.test(profile)) {
|
|
601
|
+
throw new Error(`Invalid profile name: ${profile}`)
|
|
602
|
+
}
|
|
603
|
+
|
|
590
604
|
const PROJECT_CONFIGS = await loadProjectConfigs()
|
|
591
605
|
const projectConfig = PROJECT_CONFIGS[projectKey]
|
|
592
606
|
if (!projectConfig) {
|
|
@@ -626,6 +640,10 @@ async function connect(projectKey, profile, options = {}) {
|
|
|
626
640
|
emit('status', { message: 'Finding bastion instance...' })
|
|
627
641
|
let currentInstanceId = await findBastionInstance(profile, region)
|
|
628
642
|
|
|
643
|
+
if (!INSTANCE_ID_PATTERN.test(currentInstanceId)) {
|
|
644
|
+
throw new Error(`Invalid instance ID format: ${currentInstanceId}`)
|
|
645
|
+
}
|
|
646
|
+
|
|
629
647
|
emit('status', { message: 'Getting RDS endpoint...' })
|
|
630
648
|
let currentRdsEndpoint = await getRdsEndpoint(profile, projectConfig)
|
|
631
649
|
|
|
@@ -633,6 +651,10 @@ async function connect(projectKey, profile, options = {}) {
|
|
|
633
651
|
throw new Error('Failed to find the RDS endpoint.')
|
|
634
652
|
}
|
|
635
653
|
|
|
654
|
+
if (!HOSTNAME_PATTERN.test(currentRdsEndpoint)) {
|
|
655
|
+
throw new Error(`Invalid RDS endpoint format: ${currentRdsEndpoint}`)
|
|
656
|
+
}
|
|
657
|
+
|
|
636
658
|
emit('status', { message: 'Getting RDS port...' })
|
|
637
659
|
const rdsPort = await getRdsPort(profile, projectConfig)
|
|
638
660
|
|
|
@@ -812,6 +834,12 @@ async function main() {
|
|
|
812
834
|
message: 'RDS type:',
|
|
813
835
|
choices: ['cluster', 'instance'],
|
|
814
836
|
},
|
|
837
|
+
{
|
|
838
|
+
type: 'select',
|
|
839
|
+
name: 'engine',
|
|
840
|
+
message: 'Database engine:',
|
|
841
|
+
choices: ['postgres', 'mysql'],
|
|
842
|
+
},
|
|
815
843
|
{
|
|
816
844
|
type: 'input',
|
|
817
845
|
name: 'rdsPattern',
|
|
@@ -827,7 +855,7 @@ async function main() {
|
|
|
827
855
|
type: 'input',
|
|
828
856
|
name: 'defaultPort',
|
|
829
857
|
message: 'Default local port:',
|
|
830
|
-
default: '5432',
|
|
858
|
+
default: (ctx) => ctx.engine === 'mysql' ? '3306' : '5432',
|
|
831
859
|
},
|
|
832
860
|
])
|
|
833
861
|
|
|
@@ -855,6 +883,7 @@ async function main() {
|
|
|
855
883
|
database: answers.database,
|
|
856
884
|
secretPrefix: answers.secretPrefix,
|
|
857
885
|
rdsType: answers.rdsType,
|
|
886
|
+
engine: answers.engine,
|
|
858
887
|
rdsPattern: answers.rdsPattern,
|
|
859
888
|
profileFilter: answers.profileFilter || null,
|
|
860
889
|
envPortMapping: envPortMappingInput,
|
|
@@ -940,6 +969,11 @@ async function main() {
|
|
|
940
969
|
|
|
941
970
|
const ENV = envAnswer.ENV
|
|
942
971
|
|
|
972
|
+
if (!PROFILE_SAFE_PATTERN.test(ENV)) {
|
|
973
|
+
console.error('❌ Invalid profile name:', ENV)
|
|
974
|
+
return
|
|
975
|
+
}
|
|
976
|
+
|
|
943
977
|
// Determine local port number
|
|
944
978
|
const allEnvSuffixes = Object.keys(envPortMapping).sort(
|
|
945
979
|
(a, b) => b.length - a.length,
|
|
@@ -950,10 +984,12 @@ async function main() {
|
|
|
950
984
|
const portNumber = envPortMapping[matchedSuffix] || defaultPort
|
|
951
985
|
|
|
952
986
|
// Get RDS credentials from Secrets Manager
|
|
987
|
+
console.log('\n⏳ Getting credentials...')
|
|
953
988
|
const secretsListCommand = `aws-vault exec ${ENV} -- aws secretsmanager list-secrets --region ${region} --query "SecretList[?starts_with(Name, '${secretPrefix}')].Name | [0]" --output text`
|
|
954
989
|
const SECRET_NAME = await runCommand(secretsListCommand)
|
|
955
990
|
|
|
956
991
|
if (!SECRET_NAME || SECRET_NAME === 'None') {
|
|
992
|
+
console.error('❌ No secret found with prefix:', secretPrefix)
|
|
957
993
|
return
|
|
958
994
|
}
|
|
959
995
|
|
|
@@ -961,6 +997,7 @@ async function main() {
|
|
|
961
997
|
const secretString = await runCommand(secretsGetCommand)
|
|
962
998
|
|
|
963
999
|
if (!secretString) {
|
|
1000
|
+
console.error('❌ Failed to retrieve secret value')
|
|
964
1001
|
return
|
|
965
1002
|
}
|
|
966
1003
|
|
|
@@ -971,30 +1008,52 @@ async function main() {
|
|
|
971
1008
|
throw new Error('Missing username or password in credentials')
|
|
972
1009
|
}
|
|
973
1010
|
} catch (_error) {
|
|
1011
|
+
console.error('❌ Failed to parse credentials')
|
|
974
1012
|
return
|
|
975
1013
|
}
|
|
976
1014
|
|
|
977
|
-
const _USERNAME = CREDENTIALS.username
|
|
978
|
-
const _PASSWORD = CREDENTIALS.password
|
|
979
|
-
|
|
980
1015
|
// Find bastion instance
|
|
1016
|
+
console.log('⏳ Finding bastion instance...')
|
|
981
1017
|
const instanceIdCommand = `aws-vault exec ${ENV} -- aws ec2 describe-instances --region ${region} --filters "Name=tag:Name,Values='*bastion*'" "Name=instance-state-name,Values=running" --query "Reservations[].Instances[].[InstanceId] | [0][0]" --output text`
|
|
982
1018
|
const INSTANCE_ID = await runCommand(instanceIdCommand)
|
|
983
1019
|
|
|
984
1020
|
if (!INSTANCE_ID || INSTANCE_ID === 'None') {
|
|
1021
|
+
console.error('❌ No running bastion instance found')
|
|
1022
|
+
return
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
if (!INSTANCE_ID_PATTERN.test(INSTANCE_ID)) {
|
|
1026
|
+
console.error('❌ Invalid instance ID format:', INSTANCE_ID)
|
|
985
1027
|
return
|
|
986
1028
|
}
|
|
987
1029
|
|
|
988
1030
|
// Get RDS endpoint
|
|
1031
|
+
console.log('⏳ Getting RDS endpoint...')
|
|
989
1032
|
const RDS_ENDPOINT = await getRdsEndpoint(ENV, projectConfig)
|
|
990
1033
|
|
|
991
1034
|
if (!RDS_ENDPOINT || RDS_ENDPOINT === 'None') {
|
|
1035
|
+
console.error('❌ Failed to find RDS endpoint')
|
|
1036
|
+
return
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
if (!HOSTNAME_PATTERN.test(RDS_ENDPOINT)) {
|
|
1040
|
+
console.error('❌ Invalid RDS endpoint format:', RDS_ENDPOINT)
|
|
992
1041
|
return
|
|
993
1042
|
}
|
|
994
1043
|
|
|
995
1044
|
// Get RDS port (remote port)
|
|
996
1045
|
const rdsPort = await getRdsPort(ENV, projectConfig)
|
|
997
1046
|
|
|
1047
|
+
// Print connection details
|
|
1048
|
+
console.log('\n✅ Connection details:')
|
|
1049
|
+
console.log(` Host: localhost`)
|
|
1050
|
+
console.log(` Port: ${portNumber}`)
|
|
1051
|
+
console.log(` Username: ${CREDENTIALS.username}`)
|
|
1052
|
+
console.log(` Password: ${CREDENTIALS.password}`)
|
|
1053
|
+
console.log(` Database: ${projectConfig.database}`)
|
|
1054
|
+
console.log(`\n⏳ Starting port forwarding...`)
|
|
1055
|
+
console.log(' Press Ctrl+C to disconnect\n')
|
|
1056
|
+
|
|
998
1057
|
await startPortForwardingWithConfig(
|
|
999
1058
|
ENV,
|
|
1000
1059
|
INSTANCE_ID,
|
|
@@ -1030,6 +1089,7 @@ export {
|
|
|
1030
1089
|
findBastionInstance,
|
|
1031
1090
|
getRdsEndpoint,
|
|
1032
1091
|
getRdsPort,
|
|
1092
|
+
getDefaultPortForEngine,
|
|
1033
1093
|
getLocalPort,
|
|
1034
1094
|
connect,
|
|
1035
1095
|
ipcEmitter,
|
package/package.json
CHANGED
package/src/lib/Settings.svelte
CHANGED
|
@@ -26,6 +26,7 @@ let projectRegion = $state('us-east-1')
|
|
|
26
26
|
let projectDatabase = $state('')
|
|
27
27
|
let projectSecretPrefix = $state('')
|
|
28
28
|
let projectRdsType = $state('cluster')
|
|
29
|
+
let projectEngine = $state('postgres')
|
|
29
30
|
let projectRdsPattern = $state('')
|
|
30
31
|
let projectProfileFilter = $state('')
|
|
31
32
|
let projectDefaultPort = $state('5432')
|
|
@@ -162,6 +163,11 @@ async function saveRawConfig() {
|
|
|
162
163
|
|
|
163
164
|
// ---- Project config functions ----
|
|
164
165
|
|
|
166
|
+
function handleEngineChange(e) {
|
|
167
|
+
projectEngine = e.target.value
|
|
168
|
+
projectDefaultPort = projectEngine === 'mysql' ? '3306' : '5432'
|
|
169
|
+
}
|
|
170
|
+
|
|
165
171
|
function openAddProject() {
|
|
166
172
|
editingProject = { isNew: true }
|
|
167
173
|
projectKey = ''
|
|
@@ -170,6 +176,7 @@ function openAddProject() {
|
|
|
170
176
|
projectDatabase = ''
|
|
171
177
|
projectSecretPrefix = 'rds!cluster'
|
|
172
178
|
projectRdsType = 'cluster'
|
|
179
|
+
projectEngine = 'postgres'
|
|
173
180
|
projectRdsPattern = ''
|
|
174
181
|
projectProfileFilter = ''
|
|
175
182
|
projectDefaultPort = '5432'
|
|
@@ -184,6 +191,7 @@ function openEditProject(key, config) {
|
|
|
184
191
|
projectDatabase = config.database
|
|
185
192
|
projectSecretPrefix = config.secretPrefix
|
|
186
193
|
projectRdsType = config.rdsType
|
|
194
|
+
projectEngine = config.engine || 'postgres'
|
|
187
195
|
projectRdsPattern = config.rdsPattern
|
|
188
196
|
projectProfileFilter = config.profileFilter || ''
|
|
189
197
|
projectDefaultPort = config.defaultPort
|
|
@@ -226,6 +234,7 @@ async function saveProject() {
|
|
|
226
234
|
database: projectDatabase.trim(),
|
|
227
235
|
secretPrefix: projectSecretPrefix.trim(),
|
|
228
236
|
rdsType: projectRdsType,
|
|
237
|
+
engine: projectEngine,
|
|
229
238
|
rdsPattern: projectRdsPattern.trim(),
|
|
230
239
|
profileFilter: projectProfileFilter.trim() || null,
|
|
231
240
|
envPortMapping,
|
|
@@ -393,6 +402,7 @@ onDestroy(() => {
|
|
|
393
402
|
<div class="profile-details">
|
|
394
403
|
<span class="detail">{config.region}</span>
|
|
395
404
|
<span class="detail">{config.rdsType}</span>
|
|
405
|
+
<span class="detail">{config.engine || 'postgres'}</span>
|
|
396
406
|
<span class="detail">{config.database}</span>
|
|
397
407
|
<span class="detail">{Object.keys(config.envPortMapping || {}).length} port mappings</span>
|
|
398
408
|
</div>
|
|
@@ -578,11 +588,19 @@ onDestroy(() => {
|
|
|
578
588
|
</select>
|
|
579
589
|
</div>
|
|
580
590
|
<div class="form-group">
|
|
581
|
-
<label for="project-
|
|
582
|
-
<
|
|
591
|
+
<label for="project-engine">Engine</label>
|
|
592
|
+
<select id="project-engine" value={projectEngine} onchange={handleEngineChange}>
|
|
593
|
+
<option value="postgres">PostgreSQL</option>
|
|
594
|
+
<option value="mysql">MySQL</option>
|
|
595
|
+
</select>
|
|
583
596
|
</div>
|
|
584
597
|
</div>
|
|
585
598
|
|
|
599
|
+
<div class="form-group">
|
|
600
|
+
<label for="project-rds-pattern">RDS Pattern</label>
|
|
601
|
+
<input id="project-rds-pattern" type="text" bind:value={projectRdsPattern} placeholder="-rds-aurora" />
|
|
602
|
+
</div>
|
|
603
|
+
|
|
586
604
|
<div class="form-row">
|
|
587
605
|
<div class="form-group">
|
|
588
606
|
<label for="project-profile-filter">Profile Filter</label>
|
package/src-tauri/Cargo.lock
CHANGED
package/src-tauri/Cargo.toml
CHANGED
package/src-tauri/src/lib.rs
CHANGED
|
@@ -431,6 +431,8 @@ pub struct ProjectConfig {
|
|
|
431
431
|
pub secret_prefix: String,
|
|
432
432
|
#[serde(rename = "rdsType")]
|
|
433
433
|
pub rds_type: String,
|
|
434
|
+
#[serde(default)]
|
|
435
|
+
pub engine: Option<String>,
|
|
434
436
|
#[serde(rename = "rdsPattern")]
|
|
435
437
|
pub rds_pattern: String,
|
|
436
438
|
#[serde(rename = "profileFilter")]
|
|
@@ -146,5 +146,57 @@ describe('configLoader', () => {
|
|
|
146
146
|
assert.equal(result.valid, false)
|
|
147
147
|
assert.ok(result.errors.some((e) => e.includes('Port for "dev"')))
|
|
148
148
|
})
|
|
149
|
+
|
|
150
|
+
// Engine validation tests
|
|
151
|
+
it('should accept config with engine=postgres', () => {
|
|
152
|
+
const result = validateProjectConfig({ ...validConfig, engine: 'postgres' })
|
|
153
|
+
assert.equal(result.valid, true)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('should accept config with engine=mysql', () => {
|
|
157
|
+
const result = validateProjectConfig({ ...validConfig, engine: 'mysql' })
|
|
158
|
+
assert.equal(result.valid, true)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('should accept config without engine field (backward compat)', () => {
|
|
162
|
+
const { engine: _, ...configWithoutEngine } = validConfig
|
|
163
|
+
const result = validateProjectConfig(configWithoutEngine)
|
|
164
|
+
assert.equal(result.valid, true)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('should reject invalid engine value', () => {
|
|
168
|
+
const result = validateProjectConfig({ ...validConfig, engine: 'sqlite' })
|
|
169
|
+
assert.equal(result.valid, false)
|
|
170
|
+
assert.ok(result.errors.some((e) => e.includes('engine')))
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
// Shell-safe validation tests
|
|
174
|
+
it('should reject secretPrefix with shell metacharacters', () => {
|
|
175
|
+
const result = validateProjectConfig({ ...validConfig, secretPrefix: 'rds;rm -rf /' })
|
|
176
|
+
assert.equal(result.valid, false)
|
|
177
|
+
assert.ok(result.errors.some((e) => e.includes('secretPrefix')))
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('should reject rdsPattern with shell metacharacters', () => {
|
|
181
|
+
const result = validateProjectConfig({ ...validConfig, rdsPattern: '$(whoami)' })
|
|
182
|
+
assert.equal(result.valid, false)
|
|
183
|
+
assert.ok(result.errors.some((e) => e.includes('rdsPattern')))
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('should reject database with shell metacharacters', () => {
|
|
187
|
+
const result = validateProjectConfig({ ...validConfig, database: 'db&echo' })
|
|
188
|
+
assert.equal(result.valid, false)
|
|
189
|
+
assert.ok(result.errors.some((e) => e.includes('database')))
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('should accept safe special characters in fields', () => {
|
|
193
|
+
const result = validateProjectConfig({
|
|
194
|
+
...validConfig,
|
|
195
|
+
secretPrefix: 'rds!cluster-my/prefix',
|
|
196
|
+
rdsPattern: 'my-rds_pattern.v2',
|
|
197
|
+
database: 'my_db.prod',
|
|
198
|
+
})
|
|
199
|
+
assert.equal(result.valid, true)
|
|
200
|
+
})
|
|
149
201
|
})
|
|
150
202
|
})
|