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 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.7.15' }
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)) || '5432'
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)) || '5432'
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rds_ssm_connect",
3
- "version": "1.7.16",
3
+ "version": "1.8.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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-rds-pattern">RDS Pattern</label>
582
- <input id="project-rds-pattern" type="text" bind:value={projectRdsPattern} placeholder="-rds-aurora" />
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>
@@ -3057,7 +3057,7 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
3057
3057
 
3058
3058
  [[package]]
3059
3059
  name = "rds-ssm-connect"
3060
- version = "1.7.16"
3060
+ version = "1.7.17"
3061
3061
  dependencies = [
3062
3062
  "reqwest 0.12.28",
3063
3063
  "semver",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rds-ssm-connect"
3
- version = "1.7.16"
3
+ version = "1.8.0"
4
4
  description = "Secure RDS connections through AWS SSM"
5
5
  authors = ["Iaroslav Pyrogov"]
6
6
  edition = "2024"
@@ -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")]
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://schema.tauri.app/config/2",
3
3
  "productName": "RDS SSM Connect",
4
- "version": "1.7.16",
4
+ "version": "1.8.0",
5
5
  "identifier": "com.rds-ssm-connect.desktop",
6
6
  "build": {
7
7
  "beforeDevCommand": "npm run dev:vite",
@@ -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
  })