rds_ssm_connect 1.7.17 → 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 +49 -4
- package/package.json +1 -1
- package/src/lib/Settings.svelte +20 -2
- 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,
|
|
@@ -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
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.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
|
})
|