diff --git a/.github/workflows/checkAndSubmitAddonMetadata.yml b/.github/workflows/checkAndSubmitAddonMetadata.yml index a9d5ab82d49..17e7c8a0b13 100644 --- a/.github/workflows/checkAndSubmitAddonMetadata.yml +++ b/.github/workflows/checkAndSubmitAddonMetadata.yml @@ -234,6 +234,7 @@ jobs: VT_API_LIMIT: ${{ vars.VT_API_LIMIT }} outputs: vtScanUrl: ${{ steps.setVirusTotalAnalysisStatus.outputs.vtScanUrl }} + vtResultsJSON: ${{ steps.setVirusTotalAnalysisStatus.outputs.vtResults }} steps: - name: Checkout repository uses: actions/checkout@v5 @@ -241,8 +242,8 @@ jobs: ref: ${{ env.branchName }} - name: Install Node.js uses: actions/setup-node@v4 - - name: Install glob - run: npm install glob uuid + - name: Install npm dependencies + run: npm install uuid - name: Install virusTotal run: choco install vt-cli - name: Set Virus Total analysis status @@ -252,22 +253,6 @@ jobs: script: | const setVirusTotalAnalysisStatus = require('./.github/workflows/virusTotalAnalysis.js') setVirusTotalAnalysisStatus({core}, ["${{ needs.getAddonId.outputs.addonFileName }}"]) - - name: Upload results - id: uploadResults - if: failure() - uses: actions/upload-artifact@v4 - with: - name: VirusTotal - path: vt.json - overwrite: true - - name: Upload manual approval - id: uploadManualApproval - if: failure() - uses: actions/upload-artifact@v4 - with: - name: manualApproval - path: reviewedAddons.json - overwrite: true - name: Warn if analysis fails if: failure() uses: peter-evans/create-or-update-comment@v4 @@ -286,47 +271,53 @@ jobs: addonFileName: ${{ needs.getAddonId.outputs.addonFileName }} branchName: ${{ inputs.issueAuthorName }}${{ inputs.issueNumber }} - createManualApproval: + commitScanResults: needs: [getAddonId, virusTotal-analysis, codeQL-analysis] - if: ${{ always() && (contains(needs.virusTotal-analysis.result, 'failure') || contains(needs.codeQL-analysis.result, 'failure')) }} - runs-on: windows-latest - strategy: - matrix: - python-version: [ 3.13 ] - permissions: - contents: write - issues: write - pull-requests: write + if: ${{ always() }} + runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v5 - - name: Download artifacts - uses: actions/download-artifact@v4 with: - merge-multiple: true - - name: Create pull request - id: cpr - uses: peter-evans/create-pull-request@v7 - with: - add-paths: reviewedAddons.json - title: Add reviewed add-on (${{ needs.getAddonId.outputs.addonId }}) - branch: reviewedAddon${{ github.event.issue.number }} - commit-message: Add reviewed add-on (${{ needs.getAddonId.outputs.addonId }}) - body: | - This add-on needs to be reviewed by NV Access due to analysis failure. - Review #${{ inputs.issueNumber }} for more information. - author: github-actions - delete-branch: true - - name: Request to keep issue opened - uses: peter-evans/create-or-update-comment@v4 - with: - issue-number: ${{ inputs.issueNumber }} - body: | - Please, don't close this issue. - Wait until #${{ steps.cpr.outputs.pull-request-number }} is merged. + ref: ${{ env.branchName }} + - name: Collate analysis results into add-on metadata + run: | + jq --argjson codeQLWarnings '${{ needs.codeQL-analysis.outputs.warningsJSON }}' \ + --argjson codeQLErrors '${{ needs.codeQL-analysis.outputs.errorsJSON }}' \ + --argjson vtResults '${{ needs.virusTotal-analysis.outputs.vtResultsJSON }}' \ + --arg vtScanUrl '${{ needs.virusTotal-analysis.outputs.vtScanUrl }}' \ + '. + { + vtScanUrl: $vtScanUrl, + scanResults: { + "codeQL-warnings": $codeQLWarnings, + "codeQL-errors": $codeQLErrors, + "virusTotal": $vtResults + } + }' \ + ${{ needs.getAddonId.outputs.addonFileName }} > tmp.json + mv tmp.json ${{ needs.getAddonId.outputs.addonFileName }} + - name: Commit add-on metadata + run: | + git add ${{ needs.getAddonId.outputs.addonFileName }} + git config user.name "github-actions" + git config user.email "github-actions@github.com" + git commit -m "Add code analysis results" + git push --set-upstream origin ${{ env.branchName }} + + requireManualApproval: + needs: [commitScanResults, virusTotal-analysis, codeQL-analysis] + if: ${{ always() && (needs.virusTotal-analysis.result == 'failure' || needs.codeQL-analysis.result == 'failure') }} + runs-on: ubuntu-latest + environment: + # Require a manual deployment approval if security analysis fails + name: securityReview + steps: + - name: Confirm approval + run: echo "Manual approval performed" mergeToMaster: - needs: [getAddonId, createPullRequest, codeQL-analysis, virusTotal-analysis] + needs: [getAddonId, commitScanResults, createPullRequest, requireManualApproval] + if: ${{ always() && needs.commitScanResults.result == 'success' && (needs.requireManualApproval.result == 'success' || needs.requireManualApproval.result == 'skipped') }} permissions: contents: write pull-requests: write @@ -445,7 +436,6 @@ jobs: git add . git commit -m "update discussion URL" git branch -u origin/master - git pull git push call-workflow: diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 0b339a1f03b..57287f06250 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -9,6 +9,13 @@ on: branchName: required: true type: string + outputs: + warningsJSON: + description: "The JSON from CodeQL warnings" + value: ${{ jobs.analyze.outputs.codeQLResults }} + errorsJSON: + description: "The JSON from CodeQL errors" + value: ${{ jobs.analyzeExcludingWarnings.outputs.codeQLResults }} jobs: analyzeExcludingWarnings: @@ -19,6 +26,8 @@ jobs: contents: read security-events: write issues: write + outputs: + codeQLResults: ${{ steps.setSecurityAnalysisStatus.outputs.codeQLResults }} steps: - name: Checkout repository uses: actions/checkout@v5 @@ -51,7 +60,7 @@ jobs: script: | const setSecurityAnalysisStatus = require('./.github/workflows/securityAnalysis.js') const resultsPath = 'results/python.sarif' - setSecurityAnalysisStatus({core}, "${{ inputs.addonFileName }}", resultsPath) + setSecurityAnalysisStatus({core}, resultsPath) - name: Upload results id: uploadResults if: failure() @@ -59,14 +68,6 @@ jobs: with: name: results-excluding-warnings path: results/python.sarif - - name: Upload manual approval - id: uploadManualApproval - if: failure() - uses: actions/upload-artifact@v4 - with: - name: manualApproval - path: reviewedAddons.json - overwrite: true - name: Warn if analysis fails if: failure() uses: peter-evans/create-or-update-comment@v4 @@ -90,6 +91,8 @@ jobs: actions: read security-events: write issues: write + outputs: + codeQLResults: ${{ steps.setSecurityAnalysisStatus.outputs.codeQLResults }} steps: - name: Checkout repository uses: actions/checkout@v5 @@ -122,7 +125,7 @@ jobs: script: | const setSecurityAnalysisStatus = require('./.github/workflows/securityAnalysis.js') const resultsPath = 'results/python.sarif' - setSecurityAnalysisStatus({core}, "${{ inputs.addonFileName }}", resultsPath) + setSecurityAnalysisStatus({core}, resultsPath) - name: Upload results id: uploadResults if: failure() diff --git a/.github/workflows/securityAnalysis.js b/.github/workflows/securityAnalysis.js index 7149e3b6b4c..a94b7f13384 100644 --- a/.github/workflows/securityAnalysis.js +++ b/.github/workflows/securityAnalysis.js @@ -1,28 +1,13 @@ -module.exports = ({core}, addonMetadataPath, resultsPath) => { +module.exports = ({core}, resultsPath) => { const fs = require('fs'); - const addonMetadataContents = fs.readFileSync(addonMetadataPath); - const addonMetadata = JSON.parse(addonMetadataContents); - const addonId = addonMetadata.addonId; - const sha256 = addonMetadata.sha256; - const reviewedAddonsContents = fs.readFileSync('reviewedAddons.json'); - const reviewedAddonsData = JSON.parse(reviewedAddonsContents); - if (reviewedAddonsData[addonId] !== undefined && reviewedAddonsData[addonId].includes(sha256)) { - core.info('Analysis skipped'); - return; - } const contents = fs.readFileSync(resultsPath); const data = JSON.parse(contents); const runs = data.runs[0]; const results = runs.results; + core.setOutput("codeQLResults", results); if (results.length === 0) { core.info("Security analysis succeeded"); - return; - } - if (reviewedAddonsData[addonId] === undefined) { - reviewedAddonsData[addonId] = []; + } else { + core.setFailed("Security analysis failed"); } - reviewedAddonsData[addonId].push(sha256); - const stringified = JSON.stringify(reviewedAddonsData, null, "\t"); - fs.writeFileSync('reviewedAddons.json', stringified); - core.setFailed("Security analysis failed"); }; diff --git a/.github/workflows/virusScanAllAddons.yml b/.github/workflows/virusScanAllAddons.yml index 6ea243004aa..d47769f0ab4 100644 --- a/.github/workflows/virusScanAllAddons.yml +++ b/.github/workflows/virusScanAllAddons.yml @@ -1,4 +1,4 @@ -name: Scan a batch of submitted add-ons with Virus Total +name: Scan a batch of add-ons on: # Every day at 6pm UTC. @@ -11,20 +11,19 @@ on: - cron: '0 18 * * *' workflow_dispatch: +env: + BRANCH_NAME: addScanResults${{ github.run_number }} + jobs: virusTotal-analysis: runs-on: windows-latest - strategy: - matrix: - python-version: [ 3.13 ] permissions: contents: write pull-requests: write env: VT_API_KEY: ${{ secrets.VT_API_KEY }} VT_API_LIMIT: ${{ vars.VT_API_LIMIT }} - BRANCH_NAME: addVTURLs${{ github.run_number }} - BATCH_SIZE: 100 + BATCH_SIZE: 10 steps: - name: Checkout repository uses: actions/checkout@v5 @@ -38,16 +37,16 @@ jobs: - name: Install Node.js uses: actions/setup-node@v4 - name: Install npm dependencies - run: npm install glob uuid - - name: Get add-on filenames without vtScanUrl + run: npm install uuid + - name: Get add-on filenames without scan results shell: bash run: | for file in ./addons/*/*.json; do - if (jq -r '.vtScanUrl' "$file" | grep -q 'null\|""'); then + if (jq -r '.scanResults.virusTotal' "$file" | grep -q '^null\|""$'); then echo "$file" >> addonsWithoutVT.txt fi done - wc -l addonsWithoutVT.txt | awk '{print "Total add-ons without VT URLs: " $1}' + wc -l addonsWithoutVT.txt | awk '{print "Total add-ons without scan results: " $1}' - name: Set Virus Total analysis status id: setVirusTotalAnalysisStatus uses: actions/github-script@v7 @@ -57,35 +56,36 @@ jobs: const fs = require('fs') const addonsWithoutVT = fs.readFileSync('addonsWithoutVT.txt', 'utf-8').split('\n').filter(Boolean) setVirusTotalAnalysisStatus({core}, addonsWithoutVT.slice(0, ${{ env.BATCH_SIZE }})) - - name: Create PR for updated VT urls + - name: Push updated VT urls shell: bash run: | git add addons git config user.name "github-actions" git config user.email "github-actions@github.com" - git commit -m "Add VirusTotal review URLs" + git commit -m "Add VirusTotal results" git push --set-upstream origin ${{ env.BRANCH_NAME }} - gh pr create \ - --title "Add VirusTotal review URLs" \ - --base ${{ github.ref }} \ - --head ${{ env.BRANCH_NAME }} \ - --body "Add VirusTotal review URLs to add-ons" - gh pr merge --merge "${{ env.BRANCH_NAME }}" env: GH_TOKEN: ${{ github.token }} - - name: Upload results - id: uploadResults - if: failure() - uses: actions/upload-artifact@v4 - with: - name: VirusTotal - path: vt.json - overwrite: true - - name: Upload manual approval - id: uploadManualApproval - if: failure() - uses: actions/upload-artifact@v4 + + pull-request: + needs: virusTotal-analysis + runs-on: windows-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout repository + uses: actions/checkout@v5 with: - name: manualApproval - path: reviewedAddons.json - overwrite: true + ref: ${{ env.BRANCH_NAME }} + - name: Open pr and merge + shell: bash + run: | + gh pr create \ + --title "Add scanning results" \ + --base ${{ github.ref }} \ + --head ${{ env.BRANCH_NAME }} \ + --body "Add scanning results to add-ons" + gh pr merge --merge "${{ env.BRANCH_NAME }}" + env: + GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/virusTotalAnalysis.js b/.github/workflows/virusTotalAnalysis.js index 32d5725e817..cc953e6bdbc 100644 --- a/.github/workflows/virusTotalAnalysis.js +++ b/.github/workflows/virusTotalAnalysis.js @@ -1,4 +1,3 @@ -const glob = require("glob"); const fs = require("fs"); const { exec } = require("child_process"); const countAPIUsageAndWait = require("./virusTotalAPISleepAndCount"); @@ -7,20 +6,20 @@ const virusTotalSubmit = require("./virusTotalSubmit"); function writeVTScanUrl({core}, metadataFile, addonMetadata) { const vtScanUrl = `https://www.virustotal.com/gui/file/${addonMetadata.sha256}`; - addonMetadata.vtScanUrl = vtScanUrl; - stringified = JSON.stringify(addonMetadata, null, "\t"); - // Write vtScanUrl to add-on metadata file - fs.writeFileSync(metadataFile, stringified); - // Store the latest vtScanUrl for single file analysis - core.setOutput("vtScanUrl", vtScanUrl); + if (core._isSingleFileAnalysis) { + core.setOutput("vtScanUrl", vtScanUrl); + } else { + addonMetadata.vtScanUrl = vtScanUrl; + stringified = JSON.stringify(addonMetadata, null, "\t"); + fs.writeFileSync(metadataFile, stringified + "\n"); + } } -function getVirusTotalAnalysis({core}, addonMetadata, metadataFile, reviewedAddonsData) { +function getVirusTotalAnalysis({core}, addonMetadata, metadataFile) { /* Get the VirusTotal analysis for the add-on file. - If the add-on is flagged as malicious, store the sha256 hash in reviewedAddons.json. - Always store the scan URL in the add-on metadata file. + Store the results in the metadata file and the scan URL in the add-on metadata file. If Virus total fails to scan the add-on, fail the job. */ countAPIUsageAndWait({core}); @@ -33,27 +32,29 @@ function getVirusTotalAnalysis({core}, addonMetadata, metadataFile, reviewedAddo if (core._isSingleFileAnalysis) { core.setFailed(`Failed to get VirusTotal analysis for ${metadataFile}`); } + // Resubmit and try again virusTotalSubmit({core}, [metadataFile]); - getVirusTotalAnalysis({core}, addonMetadata, metadataFile, reviewedAddonsData); + getVirusTotalAnalysis({core}, addonMetadata, metadataFile); return; } writeVTScanUrl({core}, metadataFile, addonMetadata); // Append the VirusTotal analysis to the file for an artifact const vtData = JSON.parse(stdout); - fs.appendFileSync("vt.json", stdout); const stats = vtData[0]["last_analysis_stats"]; const malicious = stats.malicious; + if (core._isSingleFileAnalysis) { + core.setOutput("vtResults", vtData); + } else { + if (addonMetadata.scanResults === undefined) { + addonMetadata.scanResults = {}; + } + addonMetadata.scanResults.virusTotal = vtData; + stringified = JSON.stringify(addonMetadata, null, "\t"); + fs.writeFileSync(metadataFile, stringified + "\n"); + } if (malicious === 0) { core.info(`VirusTotal analysis succeeded for ${metadataFile}`); - return; - } - if (reviewedAddonsData[addonMetadata.addonId] === undefined) { - reviewedAddonsData[addonMetadata.addonId] = []; - } - reviewedAddonsData[addonMetadata.addonId].push(addonMetadata.sha256); - stringified = JSON.stringify(reviewedAddonsData, null, "\t"); - fs.writeFileSync("reviewedAddons.json", stringified); - if (core._isSingleFileAnalysis) { + } else if (core._isSingleFileAnalysis) { core.setFailed(`VirusTotal analysis failed for ${metadataFile}`); } }); @@ -68,20 +69,17 @@ function getVirusTotalAnalysisIfRequired({core}, metadataFile) { */ const addonMetadataContents = fs.readFileSync(metadataFile); const addonMetadata = JSON.parse(addonMetadataContents); - const addonId = addonMetadata.addonId; - const reviewedAddonsContents = fs.readFileSync("reviewedAddons.json"); - const reviewedAddonsData = JSON.parse(reviewedAddonsContents); - // Check if add-on has been flagged before through VirusTotal. - if (reviewedAddonsData[addonId] !== undefined && reviewedAddonsData[addonId].includes(addonMetadata.sha256)) { - core.info(`VirusTotal analysis skipped, already performed for ${metadataFile}`); - return; + // Check if add-on has been submitted before to VirusTotal. + if (addonMetadata.vtScanUrl === undefined) { + core.info(`VirusTotal scanning has not been performed for ${metadataFile}`); + virusTotalSubmit({core}, [metadataFile]); } - // Check if add-on has been scanned before through VirusTotal. - if (addonMetadata.vtScanUrl !== undefined) { - core.info(`VirusTotal analysis skipped, already performed for ${metadataFile}`); + // Check if add-on has had results saved before through VirusTotal. + if (addonMetadata.scanResults !== undefined && addonMetadata.scanResults.virusTotal !== undefined) { + core.info(`VirusTotal results fetch skipped, already performed for ${metadataFile}`); return; } - getVirusTotalAnalysis({core}, addonMetadata, metadataFile, reviewedAddonsData); + getVirusTotalAnalysis({core}, addonMetadata, metadataFile); } module.exports = ({core}, metadataFiles) => { diff --git a/.github/workflows/virusTotalSubmit.js b/.github/workflows/virusTotalSubmit.js index b44e7ecf9f6..0f0b61ab888 100644 --- a/.github/workflows/virusTotalSubmit.js +++ b/.github/workflows/virusTotalSubmit.js @@ -1,4 +1,3 @@ -const glob = require("glob"); const { v4: uuidv4 } = require("uuid"); const fs = require("fs"); const { exec } = require("child_process"); diff --git a/docs/dev/submissionReview.md b/docs/dev/submissionReview.md index d83e5831bf5..fc025033815 100644 --- a/docs/dev/submissionReview.md +++ b/docs/dev/submissionReview.md @@ -1,5 +1,7 @@ +# Submission review processes + +## Approving an author to submit to a particular add-on ID for the first time -### Approving an author to submit to a particular add-on ID for the first time When a GitHub user submits a particular add-on for the first time, the submission is blocked pending initial review from NV Access. This is to confirm that the GitHub user has authorisation to submit the add-on from the add-on maintainers. In some cases, 2 separate add-on maintainers may want to lay claim to a single "add-on ID". @@ -8,33 +10,35 @@ It would be misleading and potentially risky to be encouraged to update to a for See [the submission guide](../submitters/submissionGuide.md#approval-process) for further reasoning behind this. This registration process ensures a specific add-on ID is only submitted by the same group of authorised maintainers. -#### Process +### Process for first time submissions + The process for reviewing pending first submissions is as follows: 1. A Pull Request named "Add `GitHubName` as an approved submitter for `addonId`" will be waiting on review. - - Example: https://github.com/nvaccess/addon-datastore/pull/2674 + * Example: 1. Open the referenced issue 1. Check the source code link in the referenced issue. - - Ensure the submitter matches the repository ownership, or the submitter is a core maintainer for the project. + * Ensure the submitter matches the repository ownership, or the submitter is a core maintainer for the project. If this is not the case, tag the core maintainer in the submission to confirm they give permission for the submission. - - Check for any obvious red flags with the repository i.e. it doesn't look structured as an add-on, inappropriate content in the readme, author and code only been around for a few days + * Check for any obvious red flags with the repository i.e. it doesn't look structured as an add-on, inappropriate content in the readme, author and code only been around for a few days 1. If it is clear that the submitter has permission to submit the add-on, merge the approval PR. 1. Relabel the original issue with `autoSubmissionFromIssue` - - i.e. remove the label, save, add the label back, save. - - This will resubmit the issue. - -#### Handling known issues - -##### "Create branch" fails -If an issue is mistakenly relabelled twice, the subsequent action may fail at the "Create branch" step. -To fix this: -1. Go to the GitHub action failure and identify the name of the branch where "Create branch" is failing. -1. Go to the [branches for the repository](https://github.com/nvaccess/addon-datastore/branches) and delete the branch. -1. Check if the initial issue has been successfully submitted already. - - The summary of the GitHub action failure should have a message like "Triggered via issue `x days ago` `@gitHubUserWhoSubmittedOrLabelledTheIssue` labeled `#issueNum` `commitRef`" - - Confirm that the issue was closed successfully with a matching merged PR - - Confirm that the add-on version (add-on id and version number) [exists in the repository](https://github.com/nvaccess/addon-datastore/tree/master/addons) -1. If the issue has been submitted correctly, no further action is required. -1. If the issue has not been submitted correctly, relabel the original issue with `autoSubmissionFromIssue` - - i.e. remove the label, save, add the label back, save. - - This will resubmit the issue. + * i.e. remove the label, save, add the label back, save. + * This will resubmit the issue. + +## Approving an add-on which was flagged as malicious + +An add-on may be flagged as malicious by CodeQL or VirusTotal. +If this happens, the submission process will be held, pending approval from NV Access. +This is done using a [deployment environment](https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/review-deployments). + +### Process for flagged add-ons + +1. A comment should appear on the GitHub issue with information on why the add-on was flagged. + * For CodeQL results, review the SARIF file. + * For VirusTotal results, review the VirusTotal link. +1. Consider if the flagged content is a false positive. +This may require discussion within NV Access or with the add-on contributor. +1. Go to the failed submission in GitHub Actions. +Approve or deny the deployment. +If the deployment review request has expired, re-run the job. diff --git a/docs/submitters/submissionGuide.md b/docs/submitters/submissionGuide.md index eda943cce12..c54e07bf79d 100644 --- a/docs/submitters/submissionGuide.md +++ b/docs/submitters/submissionGuide.md @@ -22,6 +22,7 @@ If there are validation errors, they will be commented on the pull request. Otherwise, the pull request will be merged, the issue will be closed and the add-on will become available in the Add-on Store. ## Approval process + Publishers must be approved to submit add-ons, on a per add-on basis. If you do not maintain the submitted add-on's repository, it is expected that you have authorisation to publish the add-on from the authors. @@ -33,21 +34,27 @@ Submitters which abuse the submission process will have their submitter approval Please report any issues with submitted add-ons to . ## Steps to submit an add-on + 1. Select ["Add-on registration" from the new issue options](https://github.com/nvaccess/addon-datastore/issues/new/choose). 1. Fill out and submit the issue form. This will create an issue with a summary of your submission, and generate a pull request to submit your add-on to the store. 1. If this is your first submission of this add-on, [manual approval](#approval-process) will be required to be added to the approved submitters list for the add-on. You do not need to do anything if you are the maintainer of the add-on you submitted. +Please wait for an NV Access staff member to review the submission. 1. Automated checks are ran on the pull request to validate the submission. Refer to [addon-datastore-validation](https://github.com/nvaccess/addon-datastore-validation) for more information on automated checks. 1. If the checks fail, a comment should be added to the issue outlining the failure. To address the issues, resubmit the issue form. You may need to also update your add-on manifest. Descriptions for the fields of the JSON schema can be found in [jsonMetadata.md](./jsonMetadata.md). +1. The checks may fail due to code being flagged as malicious. +Please review the flagged content and consider fixing them or commenting why they are a false positive. +An NV Access staff will review the submission and consider accepting if they believe it is a false positive. 1. If the checks pass, the pull request should be merged automatically. The add-on should soon become available in the store. ## Registering an add-on in the translation system + Optional. Some authors may wish to include their add-on in the translations system. To do this, follow the steps to register the add-on in [mrconfig](https://github.com/nvaccess/mrconfig/blob/master/readme.md#steps-for-addon-authors). diff --git a/reviewedAddons.json b/reviewedAddons.json deleted file mode 100644 index 5c83299f7f6..00000000000 --- a/reviewedAddons.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "deltaTalk": [ - "f1aae0c1a1fb8a3fbbf72d3050629ffb9b1cf8e9daf105089a317d744f1e0790" - ] -} \ No newline at end of file