From 89812b7869e6ad5edfa2ac05fde72b6fef46a92b Mon Sep 17 00:00:00 2001 From: Hannah Law Date: Wed, 26 Aug 2026 10:41:44 +0100 Subject: [PATCH] Require the submission confirmations. A new extension submission records two agreements from its author: permission to publish under the EULA, and acceptance of the BApp Store submission requirements. The issue form makes both mandatory, but a submission raised through the API bypasses the form and could omit them entirely, so nothing enforced them. The confirmations are checkboxes in the issue body, which this action never sees, so the caller extracts them and passes the keys it found. Which keys exist, what each one records, and which types must have them are declared here alongside the other required fields. Only extension-submission has any. A malformed value is reported once rather than also being counted as absent, as with url and version_number. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/test-sanitize-inputs.yml | 131 +++++++++++++++++++++ sanitize-inputs/action.yml | 51 +++++++- 2 files changed, 181 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-sanitize-inputs.yml b/.github/workflows/test-sanitize-inputs.yml index bb3bb4f..47963ea 100644 --- a/.github/workflows/test-sanitize-inputs.yml +++ b/.github/workflows/test-sanitize-inputs.yml @@ -91,6 +91,7 @@ jobs: url: "https://github.com/ExtensionDev/my-cool-extension" version_number: "1.2.3" product_compatibility: '["Community","DAST"]' + confirmations: '["eula","acceptance-criteria"]' - name: Assert outputs env: TITLE: ${{ steps.sanitize.outputs.title }} @@ -369,6 +370,7 @@ jobs: url: "https://github.com/owner/repo/issues/1" version_number: "1.0.0" product_compatibility: '["Community"]' + confirmations: '["eula","acceptance-criteria"]' - name: Assert the format error is reported once, not twice env: ERROR: ${{ steps.sanitize.outputs.error_message }} @@ -382,6 +384,134 @@ jobs: exit 1 fi + test-confirmations-complete: + name: "extension-submission: both confirmations accepted" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - name: Run sanitizer + id: sanitize + uses: ./sanitize-inputs + with: + type: extension-submission + title: "My Extension" + author: dev + url: "https://github.com/dev/my-extension" + version_number: "1.0.0" + product_compatibility: '["Community"]' + confirmations: '["eula","acceptance-criteria"]' + - name: Assert no error + env: + ERROR: ${{ steps.sanitize.outputs.error_message }} + run: | + test "$ERROR" = "" + + test-confirmations-partial: + name: "extension-submission: one missing confirmation produces error" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - name: Run sanitizer + id: sanitize + uses: ./sanitize-inputs + with: + type: extension-submission + title: "My Extension" + author: dev + url: "https://github.com/dev/my-extension" + version_number: "1.0.0" + product_compatibility: '["Community"]' + confirmations: '["eula"]' + - name: Assert error names only the confirmation that is absent + env: + ERROR: ${{ steps.sanitize.outputs.error_message }} + run: | + if ! echo "$ERROR" | grep -q "submission requirements"; then + echo "FAIL: error should name the absent confirmation: $ERROR" + exit 1 + fi + if echo "$ERROR" | grep -q "EULA"; then + echo "FAIL: error should not name a confirmation that was ticked: $ERROR" + exit 1 + fi + + test-confirmations-absent: + name: "extension-submission: absent confirmations produce error" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - name: Run sanitizer + id: sanitize + uses: ./sanitize-inputs + with: + type: extension-submission + title: "My Extension" + author: dev + url: "https://github.com/dev/my-extension" + version_number: "1.0.0" + product_compatibility: '["Community"]' + - name: Assert error names both confirmations + env: + ERROR: ${{ steps.sanitize.outputs.error_message }} + run: | + if ! echo "$ERROR" | grep -q "EULA"; then + echo "FAIL: error should name the EULA confirmation: $ERROR" + exit 1 + fi + if ! echo "$ERROR" | grep -q "submission requirements"; then + echo "FAIL: error should name the requirements confirmation: $ERROR" + exit 1 + fi + + test-confirmations-invalid-value: + name: "extension-submission: unknown confirmation key is not also reported as absent" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - name: Run sanitizer + id: sanitize + uses: ./sanitize-inputs + with: + type: extension-submission + title: "My Extension" + author: dev + url: "https://github.com/dev/my-extension" + version_number: "1.0.0" + product_compatibility: '["Community"]' + confirmations: '["eula","something-else"]' + - name: Assert the malformed value is reported once + env: + ERROR: ${{ steps.sanitize.outputs.error_message }} + run: | + if ! echo "$ERROR" | grep -q "confirmations contains invalid values"; then + echo "FAIL: error should reject the unknown key: $ERROR" + exit 1 + fi + if echo "$ERROR" | grep -q "did not record"; then + echo "FAIL: a malformed value must not also be reported as absent: $ERROR" + exit 1 + fi + + test-confirmations-not-applicable: + name: "extension-update: confirmations are not required" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - name: Run sanitizer + id: sanitize + uses: ./sanitize-inputs + with: + type: extension-update + title: "My Extension" + author: dev + url: "https://github.com/PortSwigger/my-extension/pull/5" + version_number: "1.2.3" + - name: Assert no error + env: + ERROR: ${{ steps.sanitize.outputs.error_message }} + run: | + test "$ERROR" = "" + test-invalid-version: name: "extension-submission: invalid version format" runs-on: ubuntu-latest @@ -456,6 +586,7 @@ jobs: url: "https://github.com/dev/my-extension/" version_number: "1.0.0" product_compatibility: '["Community"]' + confirmations: '["eula","acceptance-criteria"]' - name: Assert accepted env: URL: ${{ steps.sanitize.outputs.url }} diff --git a/sanitize-inputs/action.yml b/sanitize-inputs/action.yml index a232c2d..3624c56 100644 --- a/sanitize-inputs/action.yml +++ b/sanitize-inputs/action.yml @@ -24,6 +24,10 @@ inputs: description: 'JSON array of product labels (extension-submission only)' required: false default: '' + confirmations: + description: 'JSON array of the confirmation keys the submitter ticked (extension-submission only)' + required: false + default: '' outputs: title: @@ -58,6 +62,7 @@ runs: INPUT_URL: ${{ inputs.url }} INPUT_VERSION_NUMBER: ${{ inputs.version_number }} INPUT_PRODUCT_COMPATIBILITY: ${{ inputs.product_compatibility }} + INPUT_CONFIRMATIONS: ${{ inputs.confirmations }} with: script: | const type = process.env.INPUT_TYPE; @@ -66,6 +71,7 @@ runs: const rawUrl = process.env.INPUT_URL; const rawVersion = process.env.INPUT_VERSION_NUMBER; const rawCompatibility = process.env.INPUT_PRODUCT_COMPATIBILITY; + const rawConfirmations = process.env.INPUT_CONFIRMATIONS; // --- Sanitization functions --- @@ -100,12 +106,23 @@ runs: }, }; + // --- Confirmations --- + + // Boxes the submitter ticks to record an agreement. The caller extracts + // them from the issue body and passes the keys it found; what each one + // records, and which are mandatory, is declared here. + const CONFIRMATION_RECORDS = { + 'eula': "the author's permission to publish under the EULA", + 'acceptance-criteria': "the author's agreement to the BApp Store submission requirements", + }; + // --- Type configuration --- const TYPE_CONFIG = { 'extension-submission': { requiredFields: ['title', 'author', 'url', 'version_number'], - applicableFields: ['title', 'author', 'url', 'version_number', 'product_compatibility'], + applicableFields: ['title', 'author', 'url', 'version_number', 'product_compatibility', 'confirmations'], + requiredConfirmations: ['eula', 'acceptance-criteria'], }, 'extension-update': { requiredFields: ['title', 'url', 'version_number'], @@ -189,6 +206,38 @@ runs: } } + // --- Validate confirmations --- + + if (config.applicableFields.includes('confirmations')) { + let ticked = []; + let confirmationsValidationFailed = false; + if (rawConfirmations) { + try { + const parsed = JSON.parse(rawConfirmations); + const ALLOWED_KEYS = Object.keys(CONFIRMATION_RECORDS); + if (!Array.isArray(parsed)) { + confirmationsValidationFailed = true; + errors.push('confirmations must be a JSON array'); + } else if (parsed.some(v => !ALLOWED_KEYS.includes(v))) { + confirmationsValidationFailed = true; + errors.push(`confirmations contains invalid values. Allowed: ${ALLOWED_KEYS.join(', ')}`); + } else { + ticked = parsed; + } + } catch (e) { + confirmationsValidationFailed = true; + errors.push('confirmations is not valid JSON'); + } + } + + const absent = (config.requiredConfirmations || []).filter(key => !ticked.includes(key)); + if (absent.length > 0 && !confirmationsValidationFailed) { + errors.push( + `This submission did not record: ${absent.map(key => CONFIRMATION_RECORDS[key]).join('; ')}. ` + + 'Tick every box under "I confirm that the following is true", worded exactly as the submission form writes it.'); + } + } + // --- Check required fields --- const fieldValues = { title, author, url, version_number: versionNumber, product_compatibility: productCompatibility };