Skip to content

Commit 593815a

Browse files
dhasani23David Hasani
andauthored
fix(amazonq): validate YAML file more strictly (#8136)
## Problem When users provide a YAML config file, we were not validating the contents of it very strictly. ## Solution Improve validation logic to be more strict so that invalid YAML files are rejected client-side with a relevant error message. --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Co-authored-by: David Hasani <[email protected]>
1 parent dbc58d9 commit 593815a

File tree

4 files changed

+78
-15
lines changed

4 files changed

+78
-15
lines changed

packages/core/src/amazonqGumby/chat/controller/controller.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -580,11 +580,11 @@ export class GumbyController {
580580
return
581581
}
582582
const fileContents = await fs.readFileText(fileUri[0].fsPath)
583-
const missingKey = await validateCustomVersionsFile(fileContents)
583+
const errorMessage = validateCustomVersionsFile(fileContents)
584584

585-
if (missingKey) {
585+
if (errorMessage) {
586586
this.messenger.sendMessage(
587-
CodeWhispererConstants.invalidCustomVersionsFileMessage(missingKey),
587+
CodeWhispererConstants.invalidCustomVersionsFileMessage(errorMessage),
588588
message.tabID,
589589
'ai-prompt'
590590
)

packages/core/src/codewhisperer/models/constants.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -588,8 +588,8 @@ export const invalidMetadataFileUnsupportedSourceDB =
588588
export const invalidMetadataFileUnsupportedTargetDB =
589589
'I can only convert SQL for migrations to Aurora PostgreSQL or Amazon RDS for PostgreSQL target databases. The provided .sct file indicates another target database for this migration.'
590590

591-
export const invalidCustomVersionsFileMessage = (missingKey: string) =>
592-
`The dependency upgrade file provided is missing required field \`${missingKey}\`. Check that it is configured properly and try again. For an example of the required dependency upgrade file format, see the [documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/code-transformation.html#dependency-upgrade-file).`
591+
export const invalidCustomVersionsFileMessage = (errorMessage: string) =>
592+
`The dependency upgrade file provided is malformed: ${errorMessage}. Check that it is configured properly and try again. For an example of the required dependency upgrade file format, see the [documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/code-transformation.html#dependency-upgrade-file).`
593593

594594
export const invalidMetadataFileErrorParsing =
595595
"It looks like the .sct file you provided isn't valid. Make sure that you've uploaded the .zip file you retrieved from your schema conversion in AWS DMS."

packages/core/src/codewhisperer/service/transformByQ/transformFileHandler.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import * as vscode from 'vscode'
77
import * as path from 'path'
88
import * as os from 'os'
9+
import * as YAML from 'js-yaml'
910
import xml2js = require('xml2js')
1011
import * as CodeWhispererConstants from '../../models/constants'
1112
import { existsSync, readFileSync, writeFileSync } from 'fs' // eslint-disable-line no-restricted-imports
@@ -119,15 +120,52 @@ export async function parseBuildFile() {
119120
return undefined
120121
}
121122

122-
// return the first missing key in the custom versions file, or undefined if all required keys are present
123-
export async function validateCustomVersionsFile(fileContents: string) {
123+
// return an error message, or undefined if YAML file is valid
124+
export function validateCustomVersionsFile(fileContents: string) {
124125
const requiredKeys = ['dependencyManagement', 'identifier', 'targetVersion', 'originType']
125126
for (const key of requiredKeys) {
126127
if (!fileContents.includes(key)) {
127128
getLogger().info(`CodeTransformation: .YAML file is missing required key: ${key}`)
128-
return key
129+
return `Missing required key: \`${key}\``
129130
}
130131
}
132+
try {
133+
const yaml = YAML.load(fileContents) as any
134+
const dependencies = yaml?.dependencyManagement?.dependencies || []
135+
const plugins = yaml?.dependencyManagement?.plugins || []
136+
const dependenciesAndPlugins = dependencies.concat(plugins)
137+
138+
if (dependenciesAndPlugins.length === 0) {
139+
getLogger().info('CodeTransformation: .YAML file must contain at least dependencies or plugins')
140+
return `YAML file must contain at least \`dependencies\` or \`plugins\` under \`dependencyManagement\``
141+
}
142+
for (const item of dependenciesAndPlugins) {
143+
const errorMessage = validateItem(item)
144+
if (errorMessage) {
145+
return errorMessage
146+
}
147+
}
148+
return undefined
149+
} catch (err: any) {
150+
getLogger().info(`CodeTransformation: Invalid YAML format: ${err.message}`)
151+
return `Invalid YAML format: ${err.message}`
152+
}
153+
}
154+
155+
// return an error message, or undefined if item is valid
156+
function validateItem(item: any, validOriginTypes: string[] = ['FIRST_PARTY', 'THIRD_PARTY']) {
157+
if (!/^[^\s:]+:[^\s:]+$/.test(item.identifier)) {
158+
getLogger().info(`CodeTransformation: Invalid identifier format: ${item.identifier}`)
159+
return `Invalid identifier format: \`${item.identifier}\`. Must be in format \`groupId:artifactId\` without spaces`
160+
}
161+
if (!validOriginTypes.includes(item.originType)) {
162+
getLogger().info(`CodeTransformation: Invalid originType: ${item.originType}`)
163+
return `Invalid originType: \`${item.originType}\`. Must be either \`FIRST_PARTY\` or \`THIRD_PARTY\``
164+
}
165+
if (!item.targetVersion?.trim()) {
166+
getLogger().info(`CodeTransformation: Missing targetVersion in: ${item.identifier}`)
167+
return `Missing \`targetVersion\` in: \`${item.identifier}\``
168+
}
131169
return undefined
132170
}
133171

packages/core/src/test/codewhisperer/commands/transformByQ.test.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ dependencyManagement:
6767
plugins:
6868
- identifier: "com.example:plugin"
6969
targetVersion: "1.2.0"
70-
versionProperty: "plugin.version" # Optional`
70+
versionProperty: "plugin.version" # Optional
71+
originType: "FIRST_PARTY" # or "THIRD_PARTY"`
7172

7273
const validSctFile = `<?xml version="1.0" encoding="UTF-8"?>
7374
<tree>
@@ -570,15 +571,39 @@ dependencyManagement:
570571
assert.strictEqual(expectedWarning, warningMessage)
571572
})
572573

573-
it(`WHEN validateCustomVersionsFile on fully valid .yaml file THEN passes validation`, async function () {
574-
const missingKey = await validateCustomVersionsFile(validCustomVersionsFile)
575-
assert.strictEqual(missingKey, undefined)
574+
it(`WHEN validateCustomVersionsFile on fully valid .yaml file THEN passes validation`, function () {
575+
const errorMessage = validateCustomVersionsFile(validCustomVersionsFile)
576+
assert.strictEqual(errorMessage, undefined)
576577
})
577578

578-
it(`WHEN validateCustomVersionsFile on invalid .yaml file THEN fails validation`, async function () {
579+
it(`WHEN validateCustomVersionsFile on .yaml file with missing key THEN fails validation`, function () {
579580
const invalidFile = validCustomVersionsFile.replace('dependencyManagement', 'invalidKey')
580-
const missingKey = await validateCustomVersionsFile(invalidFile)
581-
assert.strictEqual(missingKey, 'dependencyManagement')
581+
const errorMessage = validateCustomVersionsFile(invalidFile)
582+
assert.strictEqual(errorMessage, `Missing required key: \`dependencyManagement\``)
583+
})
584+
585+
it(`WHEN validateCustomVersionsFile on .yaml file with invalid identifier format THEN fails validation`, function () {
586+
const invalidFile = validCustomVersionsFile.replace('com.example:library1', 'com.example-library1')
587+
const errorMessage = validateCustomVersionsFile(invalidFile)
588+
assert.strictEqual(
589+
errorMessage,
590+
`Invalid identifier format: \`com.example-library1\`. Must be in format \`groupId:artifactId\` without spaces`
591+
)
592+
})
593+
594+
it(`WHEN validateCustomVersionsFile on .yaml file with invalid originType THEN fails validation`, function () {
595+
const invalidFile = validCustomVersionsFile.replace('FIRST_PARTY', 'INVALID_TYPE')
596+
const errorMessage = validateCustomVersionsFile(invalidFile)
597+
assert.strictEqual(
598+
errorMessage,
599+
`Invalid originType: \`INVALID_TYPE\`. Must be either \`FIRST_PARTY\` or \`THIRD_PARTY\``
600+
)
601+
})
602+
603+
it(`WHEN validateCustomVersionsFile on .yaml file with missing targetVersion THEN fails validation`, function () {
604+
const invalidFile = validCustomVersionsFile.replace('targetVersion: "2.1.0"', '')
605+
const errorMessage = validateCustomVersionsFile(invalidFile)
606+
assert.strictEqual(errorMessage, `Missing \`targetVersion\` in: \`com.example:library1\``)
582607
})
583608

584609
it(`WHEN validateMetadataFile on fully valid .sct file THEN passes validation`, async function () {

0 commit comments

Comments
 (0)