rds_ssm_connect 1.7.17 → 1.8.1

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.17' }
17
+ const packageJson = { name: 'rds_ssm_connect', version: '1.8.1' }
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
 
@@ -495,12 +505,12 @@ function getLocalPort(ENV, projectConfig) {
495
505
  async function getConnectionCredentials(ENV, projectConfig) {
496
506
  const { region, secretPrefix, database } = projectConfig
497
507
 
498
- const secretsListCommand = `aws-vault exec ${ENV} -- aws secretsmanager list-secrets --region ${region} --query "SecretList[?starts_with(Name, '${secretPrefix}')].Name | [0]" --output text`
508
+ const secretsListCommand = `aws-vault exec ${ENV} -- aws secretsmanager list-secrets --region ${region} --query "SecretList[?contains(Name, '${secretPrefix}')].Name | [0]" --output text`
499
509
  const SECRET_NAME = await runCommand(secretsListCommand)
500
510
 
501
511
  if (!SECRET_NAME || SECRET_NAME === 'None') {
502
512
  throw new Error(
503
- `No secret found with name starting with '${secretPrefix}'.`,
513
+ `No secret found with name containing '${secretPrefix}'.`,
504
514
  )
505
515
  }
506
516
 
@@ -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,
@@ -951,7 +985,7 @@ async function main() {
951
985
 
952
986
  // Get RDS credentials from Secrets Manager
953
987
  console.log('\n⏳ Getting credentials...')
954
- const secretsListCommand = `aws-vault exec ${ENV} -- aws secretsmanager list-secrets --region ${region} --query "SecretList[?starts_with(Name, '${secretPrefix}')].Name | [0]" --output text`
988
+ const secretsListCommand = `aws-vault exec ${ENV} -- aws secretsmanager list-secrets --region ${region} --query "SecretList[?contains(Name, '${secretPrefix}')].Name | [0]" --output text`
955
989
  const SECRET_NAME = await runCommand(secretsListCommand)
956
990
 
957
991
  if (!SECRET_NAME || SECRET_NAME === 'None') {
@@ -988,6 +1022,11 @@ async function main() {
988
1022
  return
989
1023
  }
990
1024
 
1025
+ if (!INSTANCE_ID_PATTERN.test(INSTANCE_ID)) {
1026
+ console.error('❌ Invalid instance ID format:', INSTANCE_ID)
1027
+ return
1028
+ }
1029
+
991
1030
  // Get RDS endpoint
992
1031
  console.log('⏳ Getting RDS endpoint...')
993
1032
  const RDS_ENDPOINT = await getRdsEndpoint(ENV, projectConfig)
@@ -997,6 +1036,11 @@ async function main() {
997
1036
  return
998
1037
  }
999
1038
 
1039
+ if (!HOSTNAME_PATTERN.test(RDS_ENDPOINT)) {
1040
+ console.error('❌ Invalid RDS endpoint format:', RDS_ENDPOINT)
1041
+ return
1042
+ }
1043
+
1000
1044
  // Get RDS port (remote port)
1001
1045
  const rdsPort = await getRdsPort(ENV, projectConfig)
1002
1046
 
@@ -1045,6 +1089,7 @@ export {
1045
1089
  findBastionInstance,
1046
1090
  getRdsEndpoint,
1047
1091
  getRdsPort,
1092
+ getDefaultPortForEngine,
1048
1093
  getLocalPort,
1049
1094
  connect,
1050
1095
  ipcEmitter,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rds_ssm_connect",
3
- "version": "1.7.17",
3
+ "version": "1.8.1",
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.17"
3060
+ version = "1.8.0"
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.17"
3
+ version = "1.8.1"
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.17",
4
+ "version": "1.8.1",
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
  })