From 9c77d9bb158715e020c383e76239093ed435b575 Mon Sep 17 00:00:00 2001 From: mkouzel-yext Date: Tue, 18 Mar 2025 11:25:51 -0400 Subject: [PATCH 1/3] Remove run shell injection vulnerability (#1926) Prevents attackers from injecting their own code into the github actions runner using variable interpolation to steal screts and code. We now use an intermediate environment variable to store input data. --- .github/workflows/build.yml | 4 +++- .github/workflows/build_i18n.yml | 6 ++++-- .github/workflows/deploy.yml | 8 ++++++-- .github/workflows/deploy_hold.yml | 8 ++++++-- .github/workflows/extract_versions.yml | 14 ++++++++------ .github/workflows/should_deploy_major_version.yml | 10 ++++++---- 6 files changed, 33 insertions(+), 17 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cc60cb9bf..e5b5624f9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,9 @@ jobs: node-version: 20 cache: 'npm' - run: npm ci - - run: npm run ${{ inputs.build_script }} + - run: npm run "$BUILD_SCRIPT" + env: + BUILD_SCRIPT: ${{ inputs.build_script }} - name: Create build-output-US artifact uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/build_i18n.yml b/.github/workflows/build_i18n.yml index 119730b6d..6d02ed22f 100644 --- a/.github/workflows/build_i18n.yml +++ b/.github/workflows/build_i18n.yml @@ -45,7 +45,9 @@ jobs: node-version: 20 cache: 'npm' - run: npm ci - - run: LANGUAGE=${{ matrix.language }} CLOUD_REGION=${{ inputs.cloud_region }} npm run ${{ inputs.build_script }} + - run: LANGUAGE=${{ matrix.language }} CLOUD_REGION=${{ inputs.cloud_region }} npm run "$BUILD_SCRIPT" + env: + BUILD_SCRIPT: ${{ inputs.build_script }} - run: | if [ "${{ matrix.language }}" == "en" ]; then npm run size @@ -55,7 +57,7 @@ jobs: with: name: build-output-${{ inputs.cloud_region }}-${{ matrix.language }} path: dist/ - + merge_multiple_artifacts: needs: build runs-on: ubuntu-latest diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 8d6c68608..78c849013 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -38,7 +38,11 @@ jobs: aws-region: us-east-1 - name: Deploy to S3 run: | - aws s3 cp ./dist/ s3://assets.sitescdn.net/${{ inputs.bucket }}/${{ inputs.directory }} \ + aws s3 cp ./dist/ s3://assets.sitescdn.net/"$BUCKET"/"$DIRECTORY" \ --acl public-read \ --recursive \ - --cache-control ${{ inputs.cache-control }} + --cache-control "CACHE_CONTROL" + env: + BUCKET: ${{ inputs.bucket }} + DIRECTORY: ${{ inputs.directory }} + CACHE_CONTROL: ${{ inputs.cache-control }} diff --git a/.github/workflows/deploy_hold.yml b/.github/workflows/deploy_hold.yml index 82c0defa0..f7d84b453 100644 --- a/.github/workflows/deploy_hold.yml +++ b/.github/workflows/deploy_hold.yml @@ -41,10 +41,14 @@ jobs: aws-region: us-east-1 - name: Deploy to S3 run: | - aws s3 cp ./dist/ s3://assets.sitescdn.net/${{ inputs.bucket }}/${{ inputs.directory }} \ + aws s3 cp ./dist/ s3://assets.sitescdn.net/"$BUCKET"/"$DIRECTORY" \ --acl public-read \ --recursive \ - --cache-control ${{ inputs.cache-control }} + --cache-control "$CACHE_CONTROL" + env: + BUCKET: ${{ inputs.bucket }} + DIRECTORY: ${{ inputs.directory }} + CACHE_CONTROL: ${{ inputs.cache-control }} deploy-gcp: runs-on: ubuntu-latest diff --git a/.github/workflows/extract_versions.yml b/.github/workflows/extract_versions.yml index 52a6d5c9d..2fab67e5a 100644 --- a/.github/workflows/extract_versions.yml +++ b/.github/workflows/extract_versions.yml @@ -8,11 +8,11 @@ on: default: '' type: string outputs: - major_version: + major_version: value: ${{ jobs.extract_versions.outputs.major_version }} - minor_version: + minor_version: value: ${{ jobs.extract_versions.outputs.minor_version }} - full_version: + full_version: value: ${{ jobs.extract_versions.outputs.full_version }} jobs: @@ -26,12 +26,14 @@ jobs: - name: extract major and minor version substrings id: vars run: | - MAJOR_VERSION="$(echo "${GITHUB_REF_NAME##${{ inputs.ignore_prefix }}}" | cut -d '.' -f 1)" + MAJOR_VERSION="$(echo "${GITHUB_REF_NAME##"$IGNORE_PREFIX"}" | cut -d '.' -f 1)" echo "Major version: $MAJOR_VERSION" echo major_version=${MAJOR_VERSION} >> $GITHUB_OUTPUT - MINOR_VERSION="$(echo "${GITHUB_REF_NAME##${{ inputs.ignore_prefix }}}" | cut -d '.' -f 1,2)" + MINOR_VERSION="$(echo "${GITHUB_REF_NAME##"$IGNORE_PREFIX"}" | cut -d '.' -f 1,2)" echo "Minor version: $MINOR_VERSION" echo minor_version=${MINOR_VERSION} >> $GITHUB_OUTPUT - FULL_VERSION="${GITHUB_REF_NAME##${{ inputs.ignore_prefix }}}" + FULL_VERSION="${GITHUB_REF_NAME##"$IGNORE_PREFIX"}" echo "Full version: $FULL_VERSION" echo full_version=${FULL_VERSION} >> $GITHUB_OUTPUT + env: + IGNORE_PREFIX: ${{ inputs.ignore_prefix }}} diff --git a/.github/workflows/should_deploy_major_version.yml b/.github/workflows/should_deploy_major_version.yml index 3525dcc91..abdc2362e 100644 --- a/.github/workflows/should_deploy_major_version.yml +++ b/.github/workflows/should_deploy_major_version.yml @@ -8,7 +8,7 @@ on: default: '' type: string outputs: - should_deploy_major_version: + should_deploy_major_version: value: ${{ jobs.should_deploy_major_version.outputs.should_deploy_major_version }} jobs: @@ -23,10 +23,10 @@ jobs: - name: allow for major version deployment if the next minor version from current tag does not exist id: vars run: | - MINOR_VERSION=$(echo "${GITHUB_REF_NAME##${{ inputs.ignore_prefix }}}" | cut -d '.' -f 2) - MAJOR_VERSION=$(echo "${GITHUB_REF_NAME##${{ inputs.ignore_prefix }}}" | cut -d '.' -f 1) + MINOR_VERSION=$(echo "${GITHUB_REF_NAME##"$IGNORE_PREFIX"}" | cut -d '.' -f 2) + MAJOR_VERSION=$(echo "${GITHUB_REF_NAME##"$IGNORE_PREFIX"}" | cut -d '.' -f 1) NEXT_MINOR_VERSION=$(( $MINOR_VERSION + 1 )) - TAGS_FOR_NEXT_MINOR=$(git tag --list "${{ inputs.ignore_prefix }}$MAJOR_VERSION.$NEXT_MINOR_VERSION.*") + TAGS_FOR_NEXT_MINOR=$(git tag --list ""$IGNORE_PREFIX"$MAJOR_VERSION.$NEXT_MINOR_VERSION.*") if [ -z "$TAGS_FOR_NEXT_MINOR" ] then echo 'Major version should be deployed.' @@ -34,3 +34,5 @@ jobs: else echo 'Major version should not be deployed.' fi + env: + IGNORE_PREFIX: ${{ inputs.ignore_prefix }}} From 785c3e4c17da57434dcf06916523c71d600bd168 Mon Sep 17 00:00:00 2001 From: anguyen-yext2 <143001514+anguyen-yext2@users.noreply.github.com> Date: Wed, 19 Mar 2025 11:23:06 -0400 Subject: [PATCH 2/3] Remove run shell injection vulnerability (pt 2) (#1928) Prevents attackers from injecting their own code into the github actions runner using variable interpolation to steal secrets and code. We now use an intermediate environment variable to store input data (pt 2). --- .github/workflows/build_i18n.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build_i18n.yml b/.github/workflows/build_i18n.yml index 6d02ed22f..e976f7d71 100644 --- a/.github/workflows/build_i18n.yml +++ b/.github/workflows/build_i18n.yml @@ -45,8 +45,10 @@ jobs: node-version: 20 cache: 'npm' - run: npm ci - - run: LANGUAGE=${{ matrix.language }} CLOUD_REGION=${{ inputs.cloud_region }} npm run "$BUILD_SCRIPT" + - run: LANGUAGE="$LANGUAGE" CLOUD_REGION="$CLOUD_REGION" npm run "$BUILD_SCRIPT" env: + LANGUAGE: ${{ matrix.language }} + CLOUD_REGION: ${{ inputs.cloud_regiont }} BUILD_SCRIPT: ${{ inputs.build_script }} - run: | if [ "${{ matrix.language }}" == "en" ]; then From 6fc88901c8a8626594336565f63916754ffad16e Mon Sep 17 00:00:00 2001 From: mkouzel-yext Date: Fri, 25 Apr 2025 10:05:15 -0400 Subject: [PATCH 3/3] Deduplicate generative answer sources (#1930) If an entity is present in search results more than once (e.g. returned by two different verticals) and is cited by the GDA, it appeared in the sources more than one. This change dedupes the search results while determining the sources to prevent this. --- package-lock.json | 4 +- package.json | 2 +- src/core/models/generativedirectanswer.js | 10 ++- tests/core/models/generativedirectanswer.js | 81 +++++++++++++++++++++ 4 files changed, 92 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index dbb16e3c9..f1a8b4823 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@yext/answers-search-ui", - "version": "1.18.1", + "version": "1.18.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@yext/answers-search-ui", - "version": "1.18.1", + "version": "1.18.2", "license": "BSD-3-Clause", "dependencies": { "@mapbox/mapbox-gl-language": "^0.10.1", diff --git a/package.json b/package.json index 571775fca..f1a23a810 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@yext/answers-search-ui", - "version": "1.18.1", + "version": "1.18.2", "description": "Javascript Search Programming Interface", "main": "dist/answers-umd.js", "repository": { diff --git a/src/core/models/generativedirectanswer.js b/src/core/models/generativedirectanswer.js index 753549e70..0f79cb298 100644 --- a/src/core/models/generativedirectanswer.js +++ b/src/core/models/generativedirectanswer.js @@ -24,10 +24,16 @@ export default class GenerativeDirectAnswer { const verticalKey = searcher === Searcher.UNIVERSAL ? '' : verticalResults[0].verticalKey; + const citationIds = new Set(gdaResponse.citations); const citationsData = verticalResults .flatMap(vr => vr.results) - .filter(result => result.rawData?.uid && result.id && result.name) - .filter(result => gdaResponse.citations.includes(result.rawData.uid)) + .filter(result => { + if (!result.rawData?.uid || !result.id || !result.name || !citationIds.has(result.rawData.uid)) { + return false; + } + citationIds.delete(result.rawData.uid); + return true; + }) .map(result => { return { id: result.id, diff --git a/tests/core/models/generativedirectanswer.js b/tests/core/models/generativedirectanswer.js index d220a851d..f5e3ebccd 100644 --- a/tests/core/models/generativedirectanswer.js +++ b/tests/core/models/generativedirectanswer.js @@ -189,4 +189,85 @@ describe('Constructs a generative direct answer from an answers-core generative expect(actualGenerativeDirectAnswer).toMatchObject(expectedGenerativeDirectAnswer); }); + + it('GDA citations on universal search are deduplicated', () => { + const coreGenerativeDirectAnswerResponse = { + directAnswer: 'This is some generated text **with bold**.', + resultStatus: 'SUCCESS', + citations: ['uuid-1'] + }; + + const verticalResults = [ + { + results: [ + { + id: 'entityid-1', + rawData: { + uid: 'uuid-1', + someField: 'someValue' + }, + name: 'name-1', + description: 'description-1', + otherData: 'otherData' + } + ], + resultsCount: 1, + verticalKey: 'vertical-key-1' + }, + { + results: [ + { + id: 'entityid-1', + rawData: { + uid: 'uuid-1', + someField: 'someValue' + }, + name: 'name-1', + description: 'description-1', + otherData: 'otherData' + }, + { + id: 'entityid-2', + rawData: { + uid: 'uuid-2', + someField: 'someValue' + }, + name: 'name-2', + description: 'description-2', + otherData: 'otherData' + } + ], + resultsCount: 2, + verticalKey: 'vertical-key-2' + } + ]; + + const directAnswerAsHTML = RichTextFormatter.format(coreGenerativeDirectAnswerResponse.directAnswer, 'gda-snippet'); + const expectedGenerativeDirectAnswer = { + directAnswer: directAnswerAsHTML, + resultStatus: 'SUCCESS', + citations: ['uuid-1'], + searcher: Searcher.UNIVERSAL, + citationsData: [ + { + id: 'entityid-1', + name: 'name-1', + description: 'description-1', + rawData: { + uid: 'uuid-1', + someField: 'someValue' + } + } + ], + verticalKey: '' + }; + + const actualGenerativeDirectAnswer = GenerativeDirectAnswer.fromCore( + coreGenerativeDirectAnswerResponse, + Searcher.UNIVERSAL, + verticalResults + ); + + expect(actualGenerativeDirectAnswer).toMatchObject(expectedGenerativeDirectAnswer); + }); });