├── .editorconfig ├── .github └── workflows │ ├── deploy.sh │ ├── main.yaml │ └── pr.yaml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── environment.json ├── lib ├── actions.js ├── ag-handler.js ├── client.js ├── config.js ├── s3-handler.js └── utils.js ├── package-lock.json ├── package.json ├── template.yml └── tests ├── actions.js ├── ag-handler.js ├── client.js ├── config.js ├── s3-handler.js └── utils.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | insert_final_newline = true 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # Don't trim Markdown files 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | 18 | [{Makefile,**.mk}] 19 | # Use tabs for indentation (Makefiles require tabs) 20 | indent_style = tab 21 | -------------------------------------------------------------------------------- /.github/workflows/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | 3 | export LC_ALL=C.UTF-8 # needed by SAM 4 | export LANG=C.UTF-8 # needed by SAM 5 | export AWS_DEFAULT_REGION=us-east-1 # needed by SAM 6 | export APPLICATION="UvaSoftware-Scanii-Lambda" 7 | export ACCOUNT_ID=${AWS_ACCOUNT_ID} 8 | export SAM_CLI_TELEMETRY=0 9 | 10 | VERSION=$( node -pe "require('./package.json').version") 11 | 12 | # building 13 | make build package || exit 100 14 | 15 | aws serverlessrepo create-application-version \ 16 | --application-id arn:aws:serverlessrepo:us-east-1:"${ACCOUNT_ID}":applications/${APPLICATION} \ 17 | --semantic-version "${VERSION}" \ 18 | --source-code-url https://github.com/uvasoftware/scanii-lambda/releases/tag/v"${VERSION}" \ 19 | --template-body file://scanii-lambda.yaml >/dev/null || exit 99 20 | 21 | echo "SAM application ${APPLICATION} version ${VERSION} published!" 22 | 23 | # tag repo 24 | git config --global user.email "ci@uvasoftware.com" 25 | git config --global user.name "Github Actions" 26 | git tag -a v"${VERSION}" -m "Release by Github Actions v${VERSION}" 27 | git push origin v"${VERSION}" 28 | 29 | # bumping version 30 | npm --no-git-tag-version version minor 31 | VERSION=$( node -pe "require('./package.json').version") 32 | echo "next version is: $VERSION" 33 | 34 | #commit version change 35 | git commit -a -m "bump to $VERSION [ci skip]" 36 | git push origin main 37 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | jobs: 6 | deploy: 7 | permissions: 8 | contents: write 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Setup node 13 | uses: actions/setup-node@v4 14 | with: 15 | node-version: 20.x 16 | - name: Configure AWS credentials 17 | uses: aws-actions/configure-aws-credentials@v4 18 | with: 19 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 20 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 21 | aws-region: us-east-1 22 | 23 | - name: Deploy 24 | env: 25 | AWS_ACCOUNT_ID: ${{ secrets.AWS_PROD_ACCOUNT_ID }} 26 | run: bash ./.github/workflows/deploy.sh 27 | -------------------------------------------------------------------------------- /.github/workflows/pr.yaml: -------------------------------------------------------------------------------- 1 | name: PR Builds 2 | on: pull_request 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | node-version: [ 20.x, ] 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Use Node.js ${{ matrix.node-version }} 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: ${{ matrix.node-version }} 17 | - run: npm install 18 | - run: npm run build --if-present 19 | - run: npm test 20 | env: 21 | CI: true 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.idea 2 | node_modules 3 | scanii-lambda.yaml 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: clean npm-install 2 | zip -9vr /tmp/scanii-lambda.zip lib/ node_modules/ package* README.md LICENSE 3 | @echo "#### BUILD COMPLETED ####" 4 | @echo "bundle => /tmp/scanii-lambda.zip" 5 | 6 | package: 7 | sam validate 8 | sam package --s3-bucket scanii-assets --s3-prefix sam/scanii-lambda \ 9 | --template-file template.yml --output-template-file scanii-lambda.yaml 10 | 11 | npm-install: 12 | npm install . 13 | 14 | test: 15 | npm test 16 | 17 | clean: 18 | rm -rf /tmp/scanii-lambda.zip 19 | rm -rf node_modules 20 | 21 | run: 22 | sam local start-api --env-vars environment.json 23 | 24 | run-submit-event: 25 | sam local generate-event s3 put | sam local invoke ScaniiSubmitFn 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Uva Software’s scanii-lambda 2 | A Sam-Packaged AWS Lambda client to the [scanii.com](https://scanii.com) content processing engine. For a detailed walk-through of deploying this application see: https://docs.scanii.com/article/151-how-do-i-analyze-content-stored-on-amazon-s3. 3 | 4 | ## How it works 5 | This is, essentially, a series of lambda functions packaged in a one-click deployable application that configures everything needed so your S3 objects are submitted automatically to scanii’s content analysis [API](https://docs.scanii.com/v2.1/overview.html). Once the content is processed, you can choose from a couple of different actions: 6 | 7 | 1. Tag the content - this is defaulted to on and adds the following tag to objects processed: 8 | 1. `ScaniiId` -> the resource id of the processed content 9 | 2. `ScaniiFindings` -> list of identified findings (content engine dependent) 10 | 3. `ScaniiContentType` -> the identified content type of the file processed 11 | 2. Delete the object with findings - this is defaulted to **off** and will delete S3 objects with findings (such as malware or NSFW content) - for a full list of available content identification see https://docs.scanii.com/article/149-how-do-the-different-detection-engines-work 12 | 13 | ## Working with the source code 14 | The source code for this application is written using Javascript and requires, at least, nodejs 8 to run. Before getting started we strongly advise you to become familiar with the following technologies: 15 | 16 | 1. [Amazon S3](https://aws.amazon.com/s3/) 17 | 2. [Amazon Lambda](https://aws.amazon.com/lambda/) 18 | 3. [AWS Serverless Application Model (SAM) specification](https://github.com/awslabs/serverless-application-model) 19 | 20 | ### Building and running tests 21 | Tests utilize Mocha and are triggered into NPM, we provide a makefile to tie everything together: 22 | ``` 23 | $ make test 24 | ``` 25 | 26 | ### Running the application locally 27 | If you have the SAM CLI (https://github.com/awslabs/aws-sam-cli) installed locally you can run scanii-lambda locally for testing: 28 | 29 | ``` 30 | $ make run 31 | ``` 32 | 33 | ## Deploying it 34 | You can deploy this application by clicking [here.](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:484983087487:applications~UvaSoftware-Scanii-Lambda) - please note that after deployment you must manually create a trigger event for the `uvasoftware-scanii-lambda-submit` function for your S3 bucket, under “Add Triggers/S3” and event type `Object Created (All)` 35 | -------------------------------------------------------------------------------- /environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "ScaniiS3Fn": { 3 | "API_KEY": "key", 4 | "API_SECRET": "secret" 5 | }, 6 | "ScaniiApiGatewayFn": { 7 | "API_KEY": "key", 8 | "API_SECRET": "secret", 9 | "ACTION_TAG_OBJECT": "true", 10 | "ACTION_DELETE_OBJECT": "false" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/actions.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | const utils = require('./utils'); 3 | const CONFIG = require('./config').CONFIG; 4 | const assert = require('assert'); 5 | 6 | /** 7 | * Callback function that fires whenever a result has findings 8 | * @param bucket the bucket the result relates to 9 | * @param key the key the result relates to 10 | * @param result the processing result 11 | * @returns {Promise} 12 | */ 13 | const onFindings = async function (bucket, key, result) { 14 | assert(bucket !== undefined); 15 | assert(key !== undefined); 16 | assert(result !== undefined); 17 | 18 | const objectPath = utils.internalId(bucket, key); 19 | console.log(`RESULT: ${objectPath} has findings`, result.findings); 20 | console.log(`actions delete: ${CONFIG.ACTION_DELETE_OBJECT} tag: ${CONFIG.ACTION_TAG_OBJECT}`); 21 | 22 | if (CONFIG.ACTION_TAG_OBJECT === true) { 23 | await tagObject(bucket, key, result); 24 | console.log(`${objectPath} tagging completed`); 25 | } 26 | 27 | if (CONFIG.ACTION_DELETE_OBJECT === true) { 28 | await deleteObject(bucket, key); 29 | console.log(`${objectPath} deletion completed`); 30 | } 31 | 32 | return true; 33 | }; 34 | 35 | 36 | /** 37 | * Callback function that fires whenever a result DOES NOT have findings 38 | * @param bucket the bucket the result relates to 39 | * @param key the key the result relates to 40 | * @param result the processing result 41 | * @returns Promise 42 | */ 43 | const onNoFindings = async function (bucket, key, result) { 44 | assert(bucket !== undefined); 45 | assert(key !== undefined); 46 | assert(result !== undefined); 47 | 48 | console.log('RESULT:', utils.internalId(bucket, key), 'has no findings'); 49 | 50 | if (CONFIG.ACTION_TAG_OBJECT === true) { 51 | await tagObject(bucket, key, result); 52 | } 53 | 54 | console.log(`callback for file ${result.id} completed successful`); 55 | 56 | }; 57 | 58 | /** 59 | * Function that adds custom tags to an S3 object by a server side copy command 60 | * @param bucket 61 | * @param key 62 | * @param result 63 | * @returns {Promise} 64 | */ 65 | const tagObject = async function (bucket, key, result) { 66 | // https://docs.aws.amazon.com/AmazonS3/latest/dev/object-tagging.html 67 | 68 | const S3 = new AWS.S3({apiVersion: '2006-03-01'}); 69 | 70 | let params = { 71 | Bucket: bucket, 72 | Key: key, 73 | Tagging: { 74 | TagSet: [] 75 | } 76 | }; 77 | 78 | // retrieve tagging since we want to append and NOT replace them: 79 | const existingTags = await S3.getObjectTagging({Bucket: bucket, Key: key}).promise(); 80 | 81 | if (existingTags !== undefined) { 82 | params.Tagging.TagSet = existingTags.TagSet; 83 | } 84 | 85 | params.Tagging.TagSet.push({ 86 | Key: "ScaniiFindings", 87 | Value: result.findings.length !== 0 ? utils.formatTagValue(result.findings) : "None" 88 | }); 89 | 90 | params.Tagging.TagSet.push({ 91 | Key: "ScaniiId", 92 | Value: utils.formatTagValue(result.id) 93 | }); 94 | 95 | params.Tagging.TagSet.push({ 96 | Key: "ScaniiContentType", 97 | Value: utils.formatTagValue(result.content_type) 98 | }); 99 | 100 | await S3.putObjectTagging(params).promise(); 101 | console.log('file', utils.internalId(bucket, key), 'tagged'); 102 | 103 | }; 104 | 105 | /** 106 | * Handler that deletes objects with findings from S3 107 | */ 108 | const deleteObject = async function (bucket, key) { 109 | const S3 = new AWS.S3({apiVersion: '2006-03-01'}); 110 | await S3.deleteObject({Bucket: bucket, Key: key}).promise(); 111 | }; 112 | 113 | 114 | exports.onFindings = onFindings; 115 | exports.onNoFindings = onNoFindings; 116 | exports.tagObject = tagObject; // exported for testing 117 | -------------------------------------------------------------------------------- /lib/ag-handler.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const actions = require('./actions'); 3 | const utils = require('./utils'); 4 | const pkg = require('../package.json'); 5 | 6 | /** 7 | * Handles HTTP result callback 8 | * @param {Object} event the API gateway event to be processed 9 | * @param context 10 | * @param callback 11 | */ 12 | exports.handler = async (event, context, callback) => { 13 | console.log(`handling callback event using ${pkg.name}/v${pkg.version}`); 14 | 15 | try { 16 | assert.ok(event.body !== undefined, "event had no body"); 17 | 18 | const result = JSON.parse(event.body); 19 | 20 | // callback sanity checks 21 | assert.ok(result.id !== undefined, "no id provided"); 22 | assert.ok(result.metadata !== undefined, "no metadata supplied"); 23 | assert.ok(result.metadata.bucket !== undefined, "no bucket supplied in metadata"); 24 | assert.ok(result.metadata.key !== undefined, "no key supplied in metadata"); 25 | assert.ok(result.metadata.signature !== undefined, "no signature supplied in metadata"); 26 | 27 | console.log("metadata:", result.metadata); 28 | 29 | // now asserting bucket/keys were not tampered with: 30 | assert.ok(result.metadata.signature === utils.generateSignature(result.metadata.bucket.toString(), 31 | result.metadata.key.toString()), "invalid signature"); 32 | console.log('signature check passed for signature', result.metadata.signature); 33 | 34 | if (result.error === undefined) { 35 | if (result.findings.length > 0) { 36 | await actions.onFindings(result.metadata.bucket, result.metadata.key, result); 37 | } else { 38 | // we need to error something out here 39 | await actions.onNoFindings(result.metadata.bucket, result.metadata.key, result); 40 | } 41 | } 42 | 43 | // returning callback 44 | callback(null, { 45 | "statusCode": 200, 46 | "headers": { 47 | "Content-Type": "application/json" 48 | }, 49 | "body": JSON.stringify({status: "OK"}, null, 2) 50 | }); 51 | 52 | console.log("handling completed"); 53 | 54 | } catch (error) { 55 | console.log("something went wrong..."); 56 | console.error(error); // an error occurred 57 | callback(null, { 58 | "statusCode": 500, 59 | "headers": { 60 | "Content-Type": "application/json" 61 | }, 62 | "body": JSON.stringify({status: error.message}, null, 2) 63 | }); 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | const VERSION = require('../package.json').version; 2 | const assert = require('assert'); 3 | const axios = require('axios'); 4 | const querystring = require('querystring') 5 | 6 | /** 7 | * Minimal Scanii API client in javascript (@see https://docs.scanii.com) 8 | */ 9 | class ScaniiClient { 10 | constructor(key, secret, endpoint = "api.scanii.com", maxAttempts = 3, maxAttemptDelay = 1000) { 11 | this.key = key; 12 | this.secret = secret; 13 | this.maxAttempts = maxAttempts; 14 | this.maxAttemptDelay = maxAttemptDelay; 15 | this.userAgent = `scanii-lambda/v${VERSION}`; 16 | 17 | this.client = axios.create({ 18 | auth: { 19 | username: key, password: secret 20 | }, 21 | headers: { 22 | 'User-Agent': this.userAgent 23 | }, 24 | baseURL: `https://${endpoint}` 25 | }); 26 | console.log(`scanii client created using endpoint ${endpoint} and version ${VERSION}`) 27 | } 28 | 29 | /** 30 | * Makes a fetch call to scanii, see https://uvasoftware.github.io/openapi/v22/#/Files/processFileFetch 31 | * @param location (URL) of the content to be processed 32 | * @param callback callback location (URL) to be notified and receive the result 33 | * @param metadata optional metadata to be added to this file 34 | * @returns {Promise<*>} 35 | */ 36 | 37 | async fetch(location, callback, metadata) { 38 | let data = { 39 | location: location 40 | } 41 | 42 | if (callback !== null) { 43 | data.callback = callback; 44 | } 45 | if (metadata !== null) { 46 | for (const k in metadata) { 47 | if (metadata.hasOwnProperty(k)) { 48 | data[`metadata[${k}]`] = metadata[k]; 49 | } 50 | } 51 | } 52 | 53 | return await this._retry(async () => { 54 | const response = await this.client.post('/v2.2/files/fetch', querystring.stringify(data), {headers: {'content-type': 'application/x-www-form-urlencoded'}}); 55 | assert.ok(response.status === 202, `Invalid response from server, with HTTP code: ${response.status}`); 56 | console.log(`submit successful with id: ${response.data.id}`); 57 | return ({id: response.data.id, location: response.headers.location}); 58 | }); 59 | } 60 | 61 | // noinspection JSUnusedGlobalSymbols 62 | /** 63 | * Fetches the results of a previously processed file @see https://uvasoftware.github.io/openapi/v22/#/Files/retrieveFile 64 | * @param id of the content/file to be retrieved 65 | * @returns {Promise<*>} 66 | */ 67 | async retrieve(id) { 68 | return await this._retry(async () => { 69 | const response = await this.client.get(`/v2.2/files/fetch/${id}`); 70 | assert.ok(response.status === 200, `Invalid response from server, with HTTP code: ${response.status}`); 71 | let result = JSON.parse(response.data); 72 | console.log(`retrieve successful with id: ${result.id}`); 73 | return result; 74 | }); 75 | } 76 | 77 | /** 78 | * Wraps an async function call around a basic retry logic 79 | * @param func 80 | * @returns {Promise} 81 | * @private 82 | */ 83 | async _retry(func) { 84 | let attempt = 1; 85 | while (attempt <= this.maxAttempts) { 86 | if (attempt > 1) { 87 | const wait = Math.round(Math.random() * this.maxAttemptDelay); 88 | console.log(`retrying is enabled, going to wait ${wait}ms and try again`); 89 | await new Promise(resolve => setTimeout(resolve, wait)); 90 | } 91 | 92 | try { 93 | return await func() 94 | } catch (e) { 95 | console.error(e.message); 96 | } finally { 97 | attempt++ 98 | } 99 | } 100 | 101 | throw new ScaniiError(attempt - 1); 102 | } 103 | } 104 | 105 | class ScaniiError extends Error { 106 | constructor(attempts) { 107 | super(`Scanii ERROR, could not get a successful response from service after ${attempts} attempts`); 108 | this.attempts = attempts; 109 | } 110 | } 111 | 112 | exports.ScaniiClient = ScaniiClient; 113 | exports.ScaniiError = ScaniiError; 114 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | const CONFIG = {} 2 | 3 | if (process.env.AWS_SAM_LOCAL !== undefined) { 4 | console.log("starting..."); 5 | console.log(process.env); 6 | } 7 | 8 | function defaults() { 9 | CONFIG.KEY = null; 10 | CONFIG.SECRET = null; 11 | CONFIG.API_ENDPOINT = "api-us1.scanii.com"; 12 | CONFIG.CALLBACK_URL = null; 13 | CONFIG.ACTION_TAG_OBJECT = false; 14 | CONFIG.ACTION_DELETE_OBJECT = false; 15 | CONFIG.MAX_ATTEMPTS = 10; 16 | CONFIG.MAX_ATTEMPT_DELAY_MSEC = 30_000; 17 | CONFIG.SIGNED_URL_DURATION = 3600 18 | 19 | // extracting config overwrites from the environment: 20 | if (process.env.API_KEY) { 21 | CONFIG.KEY = process.env.API_KEY; 22 | } 23 | if (process.env.API_SECRET) { 24 | CONFIG.SECRET = process.env.API_SECRET; 25 | } 26 | 27 | if (process.env.API_ENDPOINT) { 28 | CONFIG.API_ENDPOINT = process.env.API_ENDPOINT; 29 | } 30 | 31 | if (process.env.ACTION_TAG_OBJECT === "true") { 32 | CONFIG.ACTION_TAG_OBJECT = true; 33 | } 34 | 35 | if (process.env.ACTION_DELETE_OBJECT === "true") { 36 | CONFIG.ACTION_DELETE_OBJECT = true; 37 | } 38 | 39 | if (process.env.CALLBACK_URL) { 40 | CONFIG.CALLBACK_URL = process.env.CALLBACK_URL; 41 | } 42 | 43 | if (process.env.MAX_ATTEMPTS) { 44 | CONFIG.MAX_ATTEMPTS = process.env.MAX_ATTEMPTS; 45 | } 46 | 47 | if (process.env.MAX_ATTEMPT_DELAY_MSEC) { 48 | CONFIG.MAX_ATTEMPT_DELAY_MSEC = process.env.MAX_ATTEMPT_DELAY_MSEC; 49 | } 50 | 51 | if (process.env.SIGNED_URL_DURATION) { 52 | CONFIG.SIGNED_URL_DURATION = process.env.SIGNED_URL_DURATION; 53 | } 54 | 55 | } 56 | 57 | defaults(); 58 | exports.defaults = defaults; 59 | exports.CONFIG = CONFIG; 60 | -------------------------------------------------------------------------------- /lib/s3-handler.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | const assert = require('assert'); 3 | const qs = require('querystring'); 4 | const CONFIG = require('./config').CONFIG; 5 | const utils = require('./utils'); 6 | const scanii = require('./client'); 7 | const pkg = require('../package.json'); 8 | 9 | 10 | /** 11 | * Handles events from S3 and submits object for processing 12 | * @param event {Object} S3 event to be processed 13 | * @param context the AWS lambda context 14 | * @param callback AWS lambda callback 15 | */ 16 | exports.handler = async (event, context, callback) => { 17 | 18 | try { 19 | console.log(`handling s3 event using ${pkg.name}/v${pkg.version}`); 20 | 21 | // pre-flight checks: 22 | assert.ok(CONFIG.CALLBACK_URL !== null, "api callback url cannot be null"); 23 | assert.ok(CONFIG.KEY !== null, "api key cannot be null"); 24 | assert.ok(CONFIG.SECRET !== null, "api secret cannot be null"); 25 | 26 | const scaniiClient = new scanii.ScaniiClient(CONFIG.KEY, CONFIG.SECRET, CONFIG.API_ENDPOINT, 27 | CONFIG.MAX_ATTEMPTS, CONFIG.MAX_ATTEMPT_DELAY_MSEC); 28 | 29 | const S3 = new AWS.S3({apiVersion: '2006-03-01'}); 30 | 31 | // Get the object from the event and show its content type 32 | const bucket = event.Records[0].s3.bucket.name; 33 | 34 | // see https://forums.aws.amazon.com/thread.jspa?threadID=215813 35 | const key = Object.keys(qs.decode(event.Records[0].s3.object.key))[0]; 36 | 37 | // sanity checks 38 | assert(bucket !== undefined, "bucket not present in s3 event"); 39 | assert(key !== undefined, "key not present in s3 event"); 40 | assert(key.endsWith('/') !== true, "cannot process directory"); 41 | 42 | console.log('processing ' + utils.internalId(bucket, key)); 43 | 44 | // creating signed url for processing - permissions are only checked at execution time 45 | const url = S3.getSignedUrl('getObject', { 46 | Bucket: bucket, 47 | Key: key, 48 | Expires: CONFIG.SIGNED_URL_DURATION 49 | }); 50 | console.log('created signed url', url); 51 | 52 | console.log('submitting content for processing'); 53 | // signing request 54 | const signature = utils.generateSignature(bucket, key); 55 | console.log('using signature ' + signature); 56 | 57 | const metadata = { 58 | "signature": signature, 59 | "bucket": bucket, 60 | "key": key 61 | }; 62 | 63 | const submitResult = await scaniiClient.fetch(url, CONFIG.CALLBACK_URL, metadata); 64 | assert.ok(submitResult.id !== undefined, "invalid response from server"); 65 | assert.ok(submitResult.location !== undefined, "invalid response from server, no response received"); 66 | console.log(`contents submitted for processing with id: ${submitResult.id} and location: ${submitResult.location}`); 67 | 68 | 69 | // returning back to 70 | callback(null, { 71 | "statusCode": 200, 72 | "headers": { 73 | "Content-Type": "application/json" 74 | }, 75 | "body": JSON.stringify({status: "OK"}, null, 2) 76 | }); 77 | } catch (error) { 78 | console.error(error, error.stack); // an error occurred 79 | callback(null, { 80 | "statusCode": 500, 81 | "headers": { 82 | "Content-Type": "application/json" 83 | }, 84 | "body": JSON.stringify({status: error.message}, null, 2) 85 | }); 86 | } 87 | }; 88 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const CONFIG = require('./config').CONFIG; 3 | const assert = require('assert'); 4 | 5 | const SIGNATURE_ALGORITHM = 'sha256'; 6 | 7 | /** 8 | * Generates a HMAC-SHA1 digital signature for a bucket/key combination using the SECRET as the key 9 | * @param bucket the bucket name 10 | * @param key the key name 11 | * @returns {string} the digitally signed bucket+key combination 12 | */ 13 | const generateSignature = function (bucket, key) { 14 | assert(bucket !== undefined); 15 | assert(key !== undefined); 16 | assert(typeof CONFIG.SECRET === 'string', 'API key secret must be set for signatures to work'); 17 | 18 | return crypto.createHmac(SIGNATURE_ALGORITHM, CONFIG.SECRET).update(internalId(bucket, key)).digest('hex'); 19 | }; 20 | 21 | /** 22 | * Given a bucket name and key, we'll return a S3 URL for it 23 | * @param bucket 24 | * @param key 25 | * @returns {string} 26 | */ 27 | const internalId = (bucket, key) => `s3://${bucket}/${key}`; 28 | 29 | exports.formatTagValue = (value) => { 30 | if (value instanceof Array) { 31 | value = value.join(' '); 32 | } 33 | //we need to make sure we stay under the tag max length: 34 | let formattedValue = value.substr(0, 255); 35 | 36 | console.log('formatted tag value: ', formattedValue); 37 | return formattedValue; 38 | }; 39 | 40 | exports.internalId = internalId; 41 | exports.generateSignature = generateSignature; 42 | 43 | 44 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scanii-lambda", 3 | "version": "2.11.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "scanii-lambda", 9 | "version": "2.11.0", 10 | "license": "Apache-2.0", 11 | "dependencies": { 12 | "aws-sdk": "^2.1583.0", 13 | "axios": "^1.8.2" 14 | }, 15 | "devDependencies": { 16 | "aws-sdk-mock": "^5.9.0", 17 | "eslint": "^8.57.0", 18 | "mocha": "^10.8.2", 19 | "nock": "^13.5.4", 20 | "sinon": "^17.0.1" 21 | } 22 | }, 23 | "node_modules/@aashutoshrathi/word-wrap": { 24 | "version": "1.2.6", 25 | "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", 26 | "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", 27 | "dev": true, 28 | "engines": { 29 | "node": ">=0.10.0" 30 | } 31 | }, 32 | "node_modules/@eslint-community/eslint-utils": { 33 | "version": "4.4.0", 34 | "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", 35 | "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", 36 | "dev": true, 37 | "dependencies": { 38 | "eslint-visitor-keys": "^3.3.0" 39 | }, 40 | "engines": { 41 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 42 | }, 43 | "peerDependencies": { 44 | "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" 45 | } 46 | }, 47 | "node_modules/@eslint-community/regexpp": { 48 | "version": "4.10.0", 49 | "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", 50 | "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", 51 | "dev": true, 52 | "engines": { 53 | "node": "^12.0.0 || ^14.0.0 || >=16.0.0" 54 | } 55 | }, 56 | "node_modules/@eslint/eslintrc": { 57 | "version": "2.1.4", 58 | "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", 59 | "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", 60 | "dev": true, 61 | "dependencies": { 62 | "ajv": "^6.12.4", 63 | "debug": "^4.3.2", 64 | "espree": "^9.6.0", 65 | "globals": "^13.19.0", 66 | "ignore": "^5.2.0", 67 | "import-fresh": "^3.2.1", 68 | "js-yaml": "^4.1.0", 69 | "minimatch": "^3.1.2", 70 | "strip-json-comments": "^3.1.1" 71 | }, 72 | "engines": { 73 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 74 | }, 75 | "funding": { 76 | "url": "https://opencollective.com/eslint" 77 | } 78 | }, 79 | "node_modules/@eslint/js": { 80 | "version": "8.57.0", 81 | "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", 82 | "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", 83 | "dev": true, 84 | "engines": { 85 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 86 | } 87 | }, 88 | "node_modules/@humanwhocodes/config-array": { 89 | "version": "0.11.14", 90 | "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", 91 | "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", 92 | "dev": true, 93 | "dependencies": { 94 | "@humanwhocodes/object-schema": "^2.0.2", 95 | "debug": "^4.3.1", 96 | "minimatch": "^3.0.5" 97 | }, 98 | "engines": { 99 | "node": ">=10.10.0" 100 | } 101 | }, 102 | "node_modules/@humanwhocodes/module-importer": { 103 | "version": "1.0.1", 104 | "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", 105 | "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", 106 | "dev": true, 107 | "engines": { 108 | "node": ">=12.22" 109 | }, 110 | "funding": { 111 | "type": "github", 112 | "url": "https://github.com/sponsors/nzakas" 113 | } 114 | }, 115 | "node_modules/@humanwhocodes/object-schema": { 116 | "version": "2.0.2", 117 | "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", 118 | "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", 119 | "dev": true 120 | }, 121 | "node_modules/@nodelib/fs.scandir": { 122 | "version": "2.1.5", 123 | "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", 124 | "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", 125 | "dev": true, 126 | "dependencies": { 127 | "@nodelib/fs.stat": "2.0.5", 128 | "run-parallel": "^1.1.9" 129 | }, 130 | "engines": { 131 | "node": ">= 8" 132 | } 133 | }, 134 | "node_modules/@nodelib/fs.stat": { 135 | "version": "2.0.5", 136 | "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", 137 | "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", 138 | "dev": true, 139 | "engines": { 140 | "node": ">= 8" 141 | } 142 | }, 143 | "node_modules/@nodelib/fs.walk": { 144 | "version": "1.2.8", 145 | "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", 146 | "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", 147 | "dev": true, 148 | "dependencies": { 149 | "@nodelib/fs.scandir": "2.1.5", 150 | "fastq": "^1.6.0" 151 | }, 152 | "engines": { 153 | "node": ">= 8" 154 | } 155 | }, 156 | "node_modules/@sinonjs/commons": { 157 | "version": "3.0.1", 158 | "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", 159 | "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", 160 | "dev": true, 161 | "dependencies": { 162 | "type-detect": "4.0.8" 163 | } 164 | }, 165 | "node_modules/@sinonjs/fake-timers": { 166 | "version": "11.2.2", 167 | "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", 168 | "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", 169 | "dev": true, 170 | "dependencies": { 171 | "@sinonjs/commons": "^3.0.0" 172 | } 173 | }, 174 | "node_modules/@sinonjs/samsam": { 175 | "version": "8.0.0", 176 | "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", 177 | "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", 178 | "dev": true, 179 | "dependencies": { 180 | "@sinonjs/commons": "^2.0.0", 181 | "lodash.get": "^4.4.2", 182 | "type-detect": "^4.0.8" 183 | } 184 | }, 185 | "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { 186 | "version": "2.0.0", 187 | "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", 188 | "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", 189 | "dev": true, 190 | "dependencies": { 191 | "type-detect": "4.0.8" 192 | } 193 | }, 194 | "node_modules/@sinonjs/text-encoding": { 195 | "version": "0.7.2", 196 | "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", 197 | "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", 198 | "dev": true 199 | }, 200 | "node_modules/@ungap/structured-clone": { 201 | "version": "1.2.0", 202 | "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", 203 | "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", 204 | "dev": true 205 | }, 206 | "node_modules/acorn": { 207 | "version": "8.11.3", 208 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", 209 | "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", 210 | "dev": true, 211 | "bin": { 212 | "acorn": "bin/acorn" 213 | }, 214 | "engines": { 215 | "node": ">=0.4.0" 216 | } 217 | }, 218 | "node_modules/acorn-jsx": { 219 | "version": "5.3.2", 220 | "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", 221 | "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", 222 | "dev": true, 223 | "peerDependencies": { 224 | "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" 225 | } 226 | }, 227 | "node_modules/ajv": { 228 | "version": "6.12.6", 229 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 230 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 231 | "dev": true, 232 | "dependencies": { 233 | "fast-deep-equal": "^3.1.1", 234 | "fast-json-stable-stringify": "^2.0.0", 235 | "json-schema-traverse": "^0.4.1", 236 | "uri-js": "^4.2.2" 237 | }, 238 | "funding": { 239 | "type": "github", 240 | "url": "https://github.com/sponsors/epoberezkin" 241 | } 242 | }, 243 | "node_modules/ansi-colors": { 244 | "version": "4.1.3", 245 | "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", 246 | "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", 247 | "dev": true, 248 | "license": "MIT", 249 | "engines": { 250 | "node": ">=6" 251 | } 252 | }, 253 | "node_modules/ansi-regex": { 254 | "version": "5.0.1", 255 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 256 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 257 | "dev": true, 258 | "engines": { 259 | "node": ">=8" 260 | } 261 | }, 262 | "node_modules/ansi-styles": { 263 | "version": "4.3.0", 264 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 265 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 266 | "dev": true, 267 | "dependencies": { 268 | "color-convert": "^2.0.1" 269 | }, 270 | "engines": { 271 | "node": ">=8" 272 | }, 273 | "funding": { 274 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 275 | } 276 | }, 277 | "node_modules/anymatch": { 278 | "version": "3.1.3", 279 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", 280 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", 281 | "dev": true, 282 | "dependencies": { 283 | "normalize-path": "^3.0.0", 284 | "picomatch": "^2.0.4" 285 | }, 286 | "engines": { 287 | "node": ">= 8" 288 | } 289 | }, 290 | "node_modules/argparse": { 291 | "version": "2.0.1", 292 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 293 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 294 | "dev": true 295 | }, 296 | "node_modules/asynckit": { 297 | "version": "0.4.0", 298 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 299 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" 300 | }, 301 | "node_modules/available-typed-arrays": { 302 | "version": "1.0.5", 303 | "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", 304 | "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", 305 | "engines": { 306 | "node": ">= 0.4" 307 | }, 308 | "funding": { 309 | "url": "https://github.com/sponsors/ljharb" 310 | } 311 | }, 312 | "node_modules/aws-sdk": { 313 | "version": "2.1583.0", 314 | "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1583.0.tgz", 315 | "integrity": "sha512-gpJFO1kWC4qHPFMRgtjp15X48W8UrU1BvhYNUE7vv4O2YFHWbfulDp1hAdv6NN1sFkq/gvK2gdo5+qjj+4wdOQ==", 316 | "hasInstallScript": true, 317 | "dependencies": { 318 | "buffer": "4.9.2", 319 | "events": "1.1.1", 320 | "ieee754": "1.1.13", 321 | "jmespath": "0.16.0", 322 | "querystring": "0.2.0", 323 | "sax": "1.2.1", 324 | "url": "0.10.3", 325 | "util": "^0.12.4", 326 | "uuid": "8.0.0", 327 | "xml2js": "0.6.2" 328 | }, 329 | "engines": { 330 | "node": ">= 10.0.0" 331 | } 332 | }, 333 | "node_modules/aws-sdk-mock": { 334 | "version": "5.9.0", 335 | "resolved": "https://registry.npmjs.org/aws-sdk-mock/-/aws-sdk-mock-5.9.0.tgz", 336 | "integrity": "sha512-kTUXaQQ1CTn3Cwxa2g1XqtCDq+FTEbPl/zgaYCok357f7gbWkeYEegqa5RziTRb11oNIUHrLp9DSHwZT3XdBkA==", 337 | "dev": true, 338 | "license": "Apache-2.0", 339 | "dependencies": { 340 | "aws-sdk": "^2.1231.0", 341 | "sinon": "^17.0.0", 342 | "traverse": "^0.6.6" 343 | }, 344 | "engines": { 345 | "node": ">=18.0.0" 346 | } 347 | }, 348 | "node_modules/axios": { 349 | "version": "1.8.2", 350 | "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz", 351 | "integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==", 352 | "license": "MIT", 353 | "dependencies": { 354 | "follow-redirects": "^1.15.6", 355 | "form-data": "^4.0.0", 356 | "proxy-from-env": "^1.1.0" 357 | } 358 | }, 359 | "node_modules/balanced-match": { 360 | "version": "1.0.2", 361 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 362 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 363 | "dev": true 364 | }, 365 | "node_modules/base64-js": { 366 | "version": "1.5.1", 367 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 368 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 369 | "funding": [ 370 | { 371 | "type": "github", 372 | "url": "https://github.com/sponsors/feross" 373 | }, 374 | { 375 | "type": "patreon", 376 | "url": "https://www.patreon.com/feross" 377 | }, 378 | { 379 | "type": "consulting", 380 | "url": "https://feross.org/support" 381 | } 382 | ] 383 | }, 384 | "node_modules/binary-extensions": { 385 | "version": "2.2.0", 386 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 387 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", 388 | "dev": true, 389 | "engines": { 390 | "node": ">=8" 391 | } 392 | }, 393 | "node_modules/brace-expansion": { 394 | "version": "1.1.11", 395 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 396 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 397 | "dev": true, 398 | "dependencies": { 399 | "balanced-match": "^1.0.0", 400 | "concat-map": "0.0.1" 401 | } 402 | }, 403 | "node_modules/braces": { 404 | "version": "3.0.3", 405 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", 406 | "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", 407 | "dev": true, 408 | "dependencies": { 409 | "fill-range": "^7.1.1" 410 | }, 411 | "engines": { 412 | "node": ">=8" 413 | } 414 | }, 415 | "node_modules/browser-stdout": { 416 | "version": "1.3.1", 417 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 418 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 419 | "dev": true 420 | }, 421 | "node_modules/buffer": { 422 | "version": "4.9.2", 423 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", 424 | "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", 425 | "dependencies": { 426 | "base64-js": "^1.0.2", 427 | "ieee754": "^1.1.4", 428 | "isarray": "^1.0.0" 429 | } 430 | }, 431 | "node_modules/call-bind": { 432 | "version": "1.0.2", 433 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", 434 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", 435 | "dependencies": { 436 | "function-bind": "^1.1.1", 437 | "get-intrinsic": "^1.0.2" 438 | }, 439 | "funding": { 440 | "url": "https://github.com/sponsors/ljharb" 441 | } 442 | }, 443 | "node_modules/callsites": { 444 | "version": "3.1.0", 445 | "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", 446 | "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", 447 | "dev": true, 448 | "engines": { 449 | "node": ">=6" 450 | } 451 | }, 452 | "node_modules/camelcase": { 453 | "version": "6.3.0", 454 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", 455 | "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", 456 | "dev": true, 457 | "engines": { 458 | "node": ">=10" 459 | }, 460 | "funding": { 461 | "url": "https://github.com/sponsors/sindresorhus" 462 | } 463 | }, 464 | "node_modules/chalk": { 465 | "version": "4.1.2", 466 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 467 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 468 | "dev": true, 469 | "dependencies": { 470 | "ansi-styles": "^4.1.0", 471 | "supports-color": "^7.1.0" 472 | }, 473 | "engines": { 474 | "node": ">=10" 475 | }, 476 | "funding": { 477 | "url": "https://github.com/chalk/chalk?sponsor=1" 478 | } 479 | }, 480 | "node_modules/chokidar": { 481 | "version": "3.5.3", 482 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", 483 | "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", 484 | "dev": true, 485 | "funding": [ 486 | { 487 | "type": "individual", 488 | "url": "https://paulmillr.com/funding/" 489 | } 490 | ], 491 | "dependencies": { 492 | "anymatch": "~3.1.2", 493 | "braces": "~3.0.2", 494 | "glob-parent": "~5.1.2", 495 | "is-binary-path": "~2.1.0", 496 | "is-glob": "~4.0.1", 497 | "normalize-path": "~3.0.0", 498 | "readdirp": "~3.6.0" 499 | }, 500 | "engines": { 501 | "node": ">= 8.10.0" 502 | }, 503 | "optionalDependencies": { 504 | "fsevents": "~2.3.2" 505 | } 506 | }, 507 | "node_modules/cliui": { 508 | "version": "7.0.4", 509 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", 510 | "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", 511 | "dev": true, 512 | "dependencies": { 513 | "string-width": "^4.2.0", 514 | "strip-ansi": "^6.0.0", 515 | "wrap-ansi": "^7.0.0" 516 | } 517 | }, 518 | "node_modules/color-convert": { 519 | "version": "2.0.1", 520 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 521 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 522 | "dev": true, 523 | "dependencies": { 524 | "color-name": "~1.1.4" 525 | }, 526 | "engines": { 527 | "node": ">=7.0.0" 528 | } 529 | }, 530 | "node_modules/color-name": { 531 | "version": "1.1.4", 532 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 533 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 534 | "dev": true 535 | }, 536 | "node_modules/combined-stream": { 537 | "version": "1.0.8", 538 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 539 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 540 | "dependencies": { 541 | "delayed-stream": "~1.0.0" 542 | }, 543 | "engines": { 544 | "node": ">= 0.8" 545 | } 546 | }, 547 | "node_modules/concat-map": { 548 | "version": "0.0.1", 549 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 550 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 551 | "dev": true 552 | }, 553 | "node_modules/cross-spawn": { 554 | "version": "7.0.3", 555 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", 556 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", 557 | "dev": true, 558 | "dependencies": { 559 | "path-key": "^3.1.0", 560 | "shebang-command": "^2.0.0", 561 | "which": "^2.0.1" 562 | }, 563 | "engines": { 564 | "node": ">= 8" 565 | } 566 | }, 567 | "node_modules/debug": { 568 | "version": "4.4.0", 569 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", 570 | "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", 571 | "dev": true, 572 | "license": "MIT", 573 | "dependencies": { 574 | "ms": "^2.1.3" 575 | }, 576 | "engines": { 577 | "node": ">=6.0" 578 | }, 579 | "peerDependenciesMeta": { 580 | "supports-color": { 581 | "optional": true 582 | } 583 | } 584 | }, 585 | "node_modules/decamelize": { 586 | "version": "4.0.0", 587 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", 588 | "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", 589 | "dev": true, 590 | "engines": { 591 | "node": ">=10" 592 | }, 593 | "funding": { 594 | "url": "https://github.com/sponsors/sindresorhus" 595 | } 596 | }, 597 | "node_modules/deep-is": { 598 | "version": "0.1.4", 599 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", 600 | "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", 601 | "dev": true 602 | }, 603 | "node_modules/delayed-stream": { 604 | "version": "1.0.0", 605 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 606 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 607 | "engines": { 608 | "node": ">=0.4.0" 609 | } 610 | }, 611 | "node_modules/diff": { 612 | "version": "5.2.0", 613 | "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", 614 | "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", 615 | "dev": true, 616 | "license": "BSD-3-Clause", 617 | "engines": { 618 | "node": ">=0.3.1" 619 | } 620 | }, 621 | "node_modules/doctrine": { 622 | "version": "3.0.0", 623 | "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", 624 | "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", 625 | "dev": true, 626 | "dependencies": { 627 | "esutils": "^2.0.2" 628 | }, 629 | "engines": { 630 | "node": ">=6.0.0" 631 | } 632 | }, 633 | "node_modules/emoji-regex": { 634 | "version": "8.0.0", 635 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 636 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 637 | "dev": true 638 | }, 639 | "node_modules/escalade": { 640 | "version": "3.1.1", 641 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", 642 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", 643 | "dev": true, 644 | "engines": { 645 | "node": ">=6" 646 | } 647 | }, 648 | "node_modules/escape-string-regexp": { 649 | "version": "4.0.0", 650 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 651 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 652 | "dev": true, 653 | "engines": { 654 | "node": ">=10" 655 | }, 656 | "funding": { 657 | "url": "https://github.com/sponsors/sindresorhus" 658 | } 659 | }, 660 | "node_modules/eslint": { 661 | "version": "8.57.0", 662 | "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", 663 | "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", 664 | "dev": true, 665 | "dependencies": { 666 | "@eslint-community/eslint-utils": "^4.2.0", 667 | "@eslint-community/regexpp": "^4.6.1", 668 | "@eslint/eslintrc": "^2.1.4", 669 | "@eslint/js": "8.57.0", 670 | "@humanwhocodes/config-array": "^0.11.14", 671 | "@humanwhocodes/module-importer": "^1.0.1", 672 | "@nodelib/fs.walk": "^1.2.8", 673 | "@ungap/structured-clone": "^1.2.0", 674 | "ajv": "^6.12.4", 675 | "chalk": "^4.0.0", 676 | "cross-spawn": "^7.0.2", 677 | "debug": "^4.3.2", 678 | "doctrine": "^3.0.0", 679 | "escape-string-regexp": "^4.0.0", 680 | "eslint-scope": "^7.2.2", 681 | "eslint-visitor-keys": "^3.4.3", 682 | "espree": "^9.6.1", 683 | "esquery": "^1.4.2", 684 | "esutils": "^2.0.2", 685 | "fast-deep-equal": "^3.1.3", 686 | "file-entry-cache": "^6.0.1", 687 | "find-up": "^5.0.0", 688 | "glob-parent": "^6.0.2", 689 | "globals": "^13.19.0", 690 | "graphemer": "^1.4.0", 691 | "ignore": "^5.2.0", 692 | "imurmurhash": "^0.1.4", 693 | "is-glob": "^4.0.0", 694 | "is-path-inside": "^3.0.3", 695 | "js-yaml": "^4.1.0", 696 | "json-stable-stringify-without-jsonify": "^1.0.1", 697 | "levn": "^0.4.1", 698 | "lodash.merge": "^4.6.2", 699 | "minimatch": "^3.1.2", 700 | "natural-compare": "^1.4.0", 701 | "optionator": "^0.9.3", 702 | "strip-ansi": "^6.0.1", 703 | "text-table": "^0.2.0" 704 | }, 705 | "bin": { 706 | "eslint": "bin/eslint.js" 707 | }, 708 | "engines": { 709 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 710 | }, 711 | "funding": { 712 | "url": "https://opencollective.com/eslint" 713 | } 714 | }, 715 | "node_modules/eslint-scope": { 716 | "version": "7.2.2", 717 | "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", 718 | "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", 719 | "dev": true, 720 | "dependencies": { 721 | "esrecurse": "^4.3.0", 722 | "estraverse": "^5.2.0" 723 | }, 724 | "engines": { 725 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 726 | }, 727 | "funding": { 728 | "url": "https://opencollective.com/eslint" 729 | } 730 | }, 731 | "node_modules/eslint-visitor-keys": { 732 | "version": "3.4.3", 733 | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", 734 | "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", 735 | "dev": true, 736 | "engines": { 737 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 738 | }, 739 | "funding": { 740 | "url": "https://opencollective.com/eslint" 741 | } 742 | }, 743 | "node_modules/eslint/node_modules/glob-parent": { 744 | "version": "6.0.2", 745 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", 746 | "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", 747 | "dev": true, 748 | "dependencies": { 749 | "is-glob": "^4.0.3" 750 | }, 751 | "engines": { 752 | "node": ">=10.13.0" 753 | } 754 | }, 755 | "node_modules/espree": { 756 | "version": "9.6.1", 757 | "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", 758 | "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", 759 | "dev": true, 760 | "dependencies": { 761 | "acorn": "^8.9.0", 762 | "acorn-jsx": "^5.3.2", 763 | "eslint-visitor-keys": "^3.4.1" 764 | }, 765 | "engines": { 766 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 767 | }, 768 | "funding": { 769 | "url": "https://opencollective.com/eslint" 770 | } 771 | }, 772 | "node_modules/esquery": { 773 | "version": "1.5.0", 774 | "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", 775 | "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", 776 | "dev": true, 777 | "dependencies": { 778 | "estraverse": "^5.1.0" 779 | }, 780 | "engines": { 781 | "node": ">=0.10" 782 | } 783 | }, 784 | "node_modules/esrecurse": { 785 | "version": "4.3.0", 786 | "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", 787 | "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", 788 | "dev": true, 789 | "dependencies": { 790 | "estraverse": "^5.2.0" 791 | }, 792 | "engines": { 793 | "node": ">=4.0" 794 | } 795 | }, 796 | "node_modules/estraverse": { 797 | "version": "5.3.0", 798 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", 799 | "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", 800 | "dev": true, 801 | "engines": { 802 | "node": ">=4.0" 803 | } 804 | }, 805 | "node_modules/esutils": { 806 | "version": "2.0.3", 807 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 808 | "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", 809 | "dev": true, 810 | "engines": { 811 | "node": ">=0.10.0" 812 | } 813 | }, 814 | "node_modules/events": { 815 | "version": "1.1.1", 816 | "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", 817 | "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", 818 | "engines": { 819 | "node": ">=0.4.x" 820 | } 821 | }, 822 | "node_modules/fast-deep-equal": { 823 | "version": "3.1.3", 824 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 825 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 826 | "dev": true 827 | }, 828 | "node_modules/fast-json-stable-stringify": { 829 | "version": "2.1.0", 830 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 831 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", 832 | "dev": true 833 | }, 834 | "node_modules/fast-levenshtein": { 835 | "version": "2.0.6", 836 | "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 837 | "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", 838 | "dev": true 839 | }, 840 | "node_modules/fastq": { 841 | "version": "1.17.1", 842 | "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", 843 | "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", 844 | "dev": true, 845 | "dependencies": { 846 | "reusify": "^1.0.4" 847 | } 848 | }, 849 | "node_modules/file-entry-cache": { 850 | "version": "6.0.1", 851 | "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", 852 | "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", 853 | "dev": true, 854 | "dependencies": { 855 | "flat-cache": "^3.0.4" 856 | }, 857 | "engines": { 858 | "node": "^10.12.0 || >=12.0.0" 859 | } 860 | }, 861 | "node_modules/fill-range": { 862 | "version": "7.1.1", 863 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", 864 | "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", 865 | "dev": true, 866 | "dependencies": { 867 | "to-regex-range": "^5.0.1" 868 | }, 869 | "engines": { 870 | "node": ">=8" 871 | } 872 | }, 873 | "node_modules/find-up": { 874 | "version": "5.0.0", 875 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 876 | "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 877 | "dev": true, 878 | "dependencies": { 879 | "locate-path": "^6.0.0", 880 | "path-exists": "^4.0.0" 881 | }, 882 | "engines": { 883 | "node": ">=10" 884 | }, 885 | "funding": { 886 | "url": "https://github.com/sponsors/sindresorhus" 887 | } 888 | }, 889 | "node_modules/flat": { 890 | "version": "5.0.2", 891 | "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", 892 | "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", 893 | "dev": true, 894 | "bin": { 895 | "flat": "cli.js" 896 | } 897 | }, 898 | "node_modules/flat-cache": { 899 | "version": "3.0.4", 900 | "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", 901 | "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", 902 | "dev": true, 903 | "dependencies": { 904 | "flatted": "^3.1.0", 905 | "rimraf": "^3.0.2" 906 | }, 907 | "engines": { 908 | "node": "^10.12.0 || >=12.0.0" 909 | } 910 | }, 911 | "node_modules/flatted": { 912 | "version": "3.2.7", 913 | "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", 914 | "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", 915 | "dev": true 916 | }, 917 | "node_modules/follow-redirects": { 918 | "version": "1.15.6", 919 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", 920 | "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", 921 | "funding": [ 922 | { 923 | "type": "individual", 924 | "url": "https://github.com/sponsors/RubenVerborgh" 925 | } 926 | ], 927 | "engines": { 928 | "node": ">=4.0" 929 | }, 930 | "peerDependenciesMeta": { 931 | "debug": { 932 | "optional": true 933 | } 934 | } 935 | }, 936 | "node_modules/for-each": { 937 | "version": "0.3.3", 938 | "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", 939 | "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", 940 | "dependencies": { 941 | "is-callable": "^1.1.3" 942 | } 943 | }, 944 | "node_modules/form-data": { 945 | "version": "4.0.0", 946 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", 947 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", 948 | "dependencies": { 949 | "asynckit": "^0.4.0", 950 | "combined-stream": "^1.0.8", 951 | "mime-types": "^2.1.12" 952 | }, 953 | "engines": { 954 | "node": ">= 6" 955 | } 956 | }, 957 | "node_modules/fs.realpath": { 958 | "version": "1.0.0", 959 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 960 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", 961 | "dev": true 962 | }, 963 | "node_modules/fsevents": { 964 | "version": "2.3.2", 965 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 966 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 967 | "dev": true, 968 | "hasInstallScript": true, 969 | "optional": true, 970 | "os": [ 971 | "darwin" 972 | ], 973 | "engines": { 974 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 975 | } 976 | }, 977 | "node_modules/function-bind": { 978 | "version": "1.1.1", 979 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 980 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" 981 | }, 982 | "node_modules/get-caller-file": { 983 | "version": "2.0.5", 984 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 985 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 986 | "dev": true, 987 | "engines": { 988 | "node": "6.* || 8.* || >= 10.*" 989 | } 990 | }, 991 | "node_modules/get-intrinsic": { 992 | "version": "1.2.1", 993 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", 994 | "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", 995 | "dependencies": { 996 | "function-bind": "^1.1.1", 997 | "has": "^1.0.3", 998 | "has-proto": "^1.0.1", 999 | "has-symbols": "^1.0.3" 1000 | }, 1001 | "funding": { 1002 | "url": "https://github.com/sponsors/ljharb" 1003 | } 1004 | }, 1005 | "node_modules/glob": { 1006 | "version": "7.2.0", 1007 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", 1008 | "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", 1009 | "dev": true, 1010 | "dependencies": { 1011 | "fs.realpath": "^1.0.0", 1012 | "inflight": "^1.0.4", 1013 | "inherits": "2", 1014 | "minimatch": "^3.0.4", 1015 | "once": "^1.3.0", 1016 | "path-is-absolute": "^1.0.0" 1017 | }, 1018 | "engines": { 1019 | "node": "*" 1020 | }, 1021 | "funding": { 1022 | "url": "https://github.com/sponsors/isaacs" 1023 | } 1024 | }, 1025 | "node_modules/glob-parent": { 1026 | "version": "5.1.2", 1027 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 1028 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 1029 | "dev": true, 1030 | "dependencies": { 1031 | "is-glob": "^4.0.1" 1032 | }, 1033 | "engines": { 1034 | "node": ">= 6" 1035 | } 1036 | }, 1037 | "node_modules/globals": { 1038 | "version": "13.24.0", 1039 | "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", 1040 | "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", 1041 | "dev": true, 1042 | "dependencies": { 1043 | "type-fest": "^0.20.2" 1044 | }, 1045 | "engines": { 1046 | "node": ">=8" 1047 | }, 1048 | "funding": { 1049 | "url": "https://github.com/sponsors/sindresorhus" 1050 | } 1051 | }, 1052 | "node_modules/gopd": { 1053 | "version": "1.0.1", 1054 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", 1055 | "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", 1056 | "dependencies": { 1057 | "get-intrinsic": "^1.1.3" 1058 | }, 1059 | "funding": { 1060 | "url": "https://github.com/sponsors/ljharb" 1061 | } 1062 | }, 1063 | "node_modules/graphemer": { 1064 | "version": "1.4.0", 1065 | "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", 1066 | "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", 1067 | "dev": true 1068 | }, 1069 | "node_modules/has": { 1070 | "version": "1.0.3", 1071 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 1072 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 1073 | "dependencies": { 1074 | "function-bind": "^1.1.1" 1075 | }, 1076 | "engines": { 1077 | "node": ">= 0.4.0" 1078 | } 1079 | }, 1080 | "node_modules/has-flag": { 1081 | "version": "4.0.0", 1082 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 1083 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 1084 | "dev": true, 1085 | "engines": { 1086 | "node": ">=8" 1087 | } 1088 | }, 1089 | "node_modules/has-proto": { 1090 | "version": "1.0.1", 1091 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", 1092 | "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", 1093 | "engines": { 1094 | "node": ">= 0.4" 1095 | }, 1096 | "funding": { 1097 | "url": "https://github.com/sponsors/ljharb" 1098 | } 1099 | }, 1100 | "node_modules/has-symbols": { 1101 | "version": "1.0.3", 1102 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 1103 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 1104 | "engines": { 1105 | "node": ">= 0.4" 1106 | }, 1107 | "funding": { 1108 | "url": "https://github.com/sponsors/ljharb" 1109 | } 1110 | }, 1111 | "node_modules/has-tostringtag": { 1112 | "version": "1.0.0", 1113 | "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", 1114 | "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", 1115 | "dependencies": { 1116 | "has-symbols": "^1.0.2" 1117 | }, 1118 | "engines": { 1119 | "node": ">= 0.4" 1120 | }, 1121 | "funding": { 1122 | "url": "https://github.com/sponsors/ljharb" 1123 | } 1124 | }, 1125 | "node_modules/he": { 1126 | "version": "1.2.0", 1127 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", 1128 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", 1129 | "dev": true, 1130 | "bin": { 1131 | "he": "bin/he" 1132 | } 1133 | }, 1134 | "node_modules/ieee754": { 1135 | "version": "1.1.13", 1136 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", 1137 | "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" 1138 | }, 1139 | "node_modules/ignore": { 1140 | "version": "5.3.1", 1141 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", 1142 | "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", 1143 | "dev": true, 1144 | "engines": { 1145 | "node": ">= 4" 1146 | } 1147 | }, 1148 | "node_modules/import-fresh": { 1149 | "version": "3.3.0", 1150 | "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", 1151 | "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", 1152 | "dev": true, 1153 | "dependencies": { 1154 | "parent-module": "^1.0.0", 1155 | "resolve-from": "^4.0.0" 1156 | }, 1157 | "engines": { 1158 | "node": ">=6" 1159 | }, 1160 | "funding": { 1161 | "url": "https://github.com/sponsors/sindresorhus" 1162 | } 1163 | }, 1164 | "node_modules/imurmurhash": { 1165 | "version": "0.1.4", 1166 | "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 1167 | "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", 1168 | "dev": true, 1169 | "engines": { 1170 | "node": ">=0.8.19" 1171 | } 1172 | }, 1173 | "node_modules/inflight": { 1174 | "version": "1.0.6", 1175 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 1176 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 1177 | "dev": true, 1178 | "dependencies": { 1179 | "once": "^1.3.0", 1180 | "wrappy": "1" 1181 | } 1182 | }, 1183 | "node_modules/inherits": { 1184 | "version": "2.0.4", 1185 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 1186 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 1187 | }, 1188 | "node_modules/is-arguments": { 1189 | "version": "1.1.1", 1190 | "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", 1191 | "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", 1192 | "dependencies": { 1193 | "call-bind": "^1.0.2", 1194 | "has-tostringtag": "^1.0.0" 1195 | }, 1196 | "engines": { 1197 | "node": ">= 0.4" 1198 | }, 1199 | "funding": { 1200 | "url": "https://github.com/sponsors/ljharb" 1201 | } 1202 | }, 1203 | "node_modules/is-binary-path": { 1204 | "version": "2.1.0", 1205 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 1206 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 1207 | "dev": true, 1208 | "dependencies": { 1209 | "binary-extensions": "^2.0.0" 1210 | }, 1211 | "engines": { 1212 | "node": ">=8" 1213 | } 1214 | }, 1215 | "node_modules/is-callable": { 1216 | "version": "1.2.7", 1217 | "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", 1218 | "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", 1219 | "engines": { 1220 | "node": ">= 0.4" 1221 | }, 1222 | "funding": { 1223 | "url": "https://github.com/sponsors/ljharb" 1224 | } 1225 | }, 1226 | "node_modules/is-extglob": { 1227 | "version": "2.1.1", 1228 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 1229 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 1230 | "dev": true, 1231 | "engines": { 1232 | "node": ">=0.10.0" 1233 | } 1234 | }, 1235 | "node_modules/is-fullwidth-code-point": { 1236 | "version": "3.0.0", 1237 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 1238 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 1239 | "dev": true, 1240 | "engines": { 1241 | "node": ">=8" 1242 | } 1243 | }, 1244 | "node_modules/is-generator-function": { 1245 | "version": "1.0.10", 1246 | "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", 1247 | "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", 1248 | "dependencies": { 1249 | "has-tostringtag": "^1.0.0" 1250 | }, 1251 | "engines": { 1252 | "node": ">= 0.4" 1253 | }, 1254 | "funding": { 1255 | "url": "https://github.com/sponsors/ljharb" 1256 | } 1257 | }, 1258 | "node_modules/is-glob": { 1259 | "version": "4.0.3", 1260 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 1261 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 1262 | "dev": true, 1263 | "dependencies": { 1264 | "is-extglob": "^2.1.1" 1265 | }, 1266 | "engines": { 1267 | "node": ">=0.10.0" 1268 | } 1269 | }, 1270 | "node_modules/is-number": { 1271 | "version": "7.0.0", 1272 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 1273 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 1274 | "dev": true, 1275 | "engines": { 1276 | "node": ">=0.12.0" 1277 | } 1278 | }, 1279 | "node_modules/is-path-inside": { 1280 | "version": "3.0.3", 1281 | "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", 1282 | "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", 1283 | "dev": true, 1284 | "engines": { 1285 | "node": ">=8" 1286 | } 1287 | }, 1288 | "node_modules/is-plain-obj": { 1289 | "version": "2.1.0", 1290 | "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", 1291 | "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", 1292 | "dev": true, 1293 | "engines": { 1294 | "node": ">=8" 1295 | } 1296 | }, 1297 | "node_modules/is-typed-array": { 1298 | "version": "1.1.10", 1299 | "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", 1300 | "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", 1301 | "dependencies": { 1302 | "available-typed-arrays": "^1.0.5", 1303 | "call-bind": "^1.0.2", 1304 | "for-each": "^0.3.3", 1305 | "gopd": "^1.0.1", 1306 | "has-tostringtag": "^1.0.0" 1307 | }, 1308 | "engines": { 1309 | "node": ">= 0.4" 1310 | }, 1311 | "funding": { 1312 | "url": "https://github.com/sponsors/ljharb" 1313 | } 1314 | }, 1315 | "node_modules/is-unicode-supported": { 1316 | "version": "0.1.0", 1317 | "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", 1318 | "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", 1319 | "dev": true, 1320 | "engines": { 1321 | "node": ">=10" 1322 | }, 1323 | "funding": { 1324 | "url": "https://github.com/sponsors/sindresorhus" 1325 | } 1326 | }, 1327 | "node_modules/isarray": { 1328 | "version": "1.0.0", 1329 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 1330 | "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" 1331 | }, 1332 | "node_modules/isexe": { 1333 | "version": "2.0.0", 1334 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 1335 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 1336 | "dev": true 1337 | }, 1338 | "node_modules/jmespath": { 1339 | "version": "0.16.0", 1340 | "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", 1341 | "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", 1342 | "engines": { 1343 | "node": ">= 0.6.0" 1344 | } 1345 | }, 1346 | "node_modules/js-yaml": { 1347 | "version": "4.1.0", 1348 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 1349 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 1350 | "dev": true, 1351 | "dependencies": { 1352 | "argparse": "^2.0.1" 1353 | }, 1354 | "bin": { 1355 | "js-yaml": "bin/js-yaml.js" 1356 | } 1357 | }, 1358 | "node_modules/json-schema-traverse": { 1359 | "version": "0.4.1", 1360 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 1361 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 1362 | "dev": true 1363 | }, 1364 | "node_modules/json-stable-stringify-without-jsonify": { 1365 | "version": "1.0.1", 1366 | "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", 1367 | "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", 1368 | "dev": true 1369 | }, 1370 | "node_modules/json-stringify-safe": { 1371 | "version": "5.0.1", 1372 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 1373 | "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", 1374 | "dev": true 1375 | }, 1376 | "node_modules/just-extend": { 1377 | "version": "6.2.0", 1378 | "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", 1379 | "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", 1380 | "dev": true 1381 | }, 1382 | "node_modules/levn": { 1383 | "version": "0.4.1", 1384 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", 1385 | "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", 1386 | "dev": true, 1387 | "dependencies": { 1388 | "prelude-ls": "^1.2.1", 1389 | "type-check": "~0.4.0" 1390 | }, 1391 | "engines": { 1392 | "node": ">= 0.8.0" 1393 | } 1394 | }, 1395 | "node_modules/locate-path": { 1396 | "version": "6.0.0", 1397 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 1398 | "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 1399 | "dev": true, 1400 | "dependencies": { 1401 | "p-locate": "^5.0.0" 1402 | }, 1403 | "engines": { 1404 | "node": ">=10" 1405 | }, 1406 | "funding": { 1407 | "url": "https://github.com/sponsors/sindresorhus" 1408 | } 1409 | }, 1410 | "node_modules/lodash.get": { 1411 | "version": "4.4.2", 1412 | "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", 1413 | "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", 1414 | "dev": true 1415 | }, 1416 | "node_modules/lodash.merge": { 1417 | "version": "4.6.2", 1418 | "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", 1419 | "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", 1420 | "dev": true 1421 | }, 1422 | "node_modules/log-symbols": { 1423 | "version": "4.1.0", 1424 | "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", 1425 | "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", 1426 | "dev": true, 1427 | "dependencies": { 1428 | "chalk": "^4.1.0", 1429 | "is-unicode-supported": "^0.1.0" 1430 | }, 1431 | "engines": { 1432 | "node": ">=10" 1433 | }, 1434 | "funding": { 1435 | "url": "https://github.com/sponsors/sindresorhus" 1436 | } 1437 | }, 1438 | "node_modules/mime-db": { 1439 | "version": "1.52.0", 1440 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 1441 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 1442 | "engines": { 1443 | "node": ">= 0.6" 1444 | } 1445 | }, 1446 | "node_modules/mime-types": { 1447 | "version": "2.1.35", 1448 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 1449 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 1450 | "dependencies": { 1451 | "mime-db": "1.52.0" 1452 | }, 1453 | "engines": { 1454 | "node": ">= 0.6" 1455 | } 1456 | }, 1457 | "node_modules/minimatch": { 1458 | "version": "3.1.2", 1459 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 1460 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 1461 | "dev": true, 1462 | "dependencies": { 1463 | "brace-expansion": "^1.1.7" 1464 | }, 1465 | "engines": { 1466 | "node": "*" 1467 | } 1468 | }, 1469 | "node_modules/mocha": { 1470 | "version": "10.8.2", 1471 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", 1472 | "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", 1473 | "dev": true, 1474 | "license": "MIT", 1475 | "dependencies": { 1476 | "ansi-colors": "^4.1.3", 1477 | "browser-stdout": "^1.3.1", 1478 | "chokidar": "^3.5.3", 1479 | "debug": "^4.3.5", 1480 | "diff": "^5.2.0", 1481 | "escape-string-regexp": "^4.0.0", 1482 | "find-up": "^5.0.0", 1483 | "glob": "^8.1.0", 1484 | "he": "^1.2.0", 1485 | "js-yaml": "^4.1.0", 1486 | "log-symbols": "^4.1.0", 1487 | "minimatch": "^5.1.6", 1488 | "ms": "^2.1.3", 1489 | "serialize-javascript": "^6.0.2", 1490 | "strip-json-comments": "^3.1.1", 1491 | "supports-color": "^8.1.1", 1492 | "workerpool": "^6.5.1", 1493 | "yargs": "^16.2.0", 1494 | "yargs-parser": "^20.2.9", 1495 | "yargs-unparser": "^2.0.0" 1496 | }, 1497 | "bin": { 1498 | "_mocha": "bin/_mocha", 1499 | "mocha": "bin/mocha.js" 1500 | }, 1501 | "engines": { 1502 | "node": ">= 14.0.0" 1503 | } 1504 | }, 1505 | "node_modules/mocha/node_modules/brace-expansion": { 1506 | "version": "2.0.1", 1507 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", 1508 | "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", 1509 | "dev": true, 1510 | "dependencies": { 1511 | "balanced-match": "^1.0.0" 1512 | } 1513 | }, 1514 | "node_modules/mocha/node_modules/glob": { 1515 | "version": "8.1.0", 1516 | "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", 1517 | "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", 1518 | "dev": true, 1519 | "dependencies": { 1520 | "fs.realpath": "^1.0.0", 1521 | "inflight": "^1.0.4", 1522 | "inherits": "2", 1523 | "minimatch": "^5.0.1", 1524 | "once": "^1.3.0" 1525 | }, 1526 | "engines": { 1527 | "node": ">=12" 1528 | }, 1529 | "funding": { 1530 | "url": "https://github.com/sponsors/isaacs" 1531 | } 1532 | }, 1533 | "node_modules/mocha/node_modules/minimatch": { 1534 | "version": "5.1.6", 1535 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", 1536 | "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", 1537 | "dev": true, 1538 | "license": "ISC", 1539 | "dependencies": { 1540 | "brace-expansion": "^2.0.1" 1541 | }, 1542 | "engines": { 1543 | "node": ">=10" 1544 | } 1545 | }, 1546 | "node_modules/mocha/node_modules/supports-color": { 1547 | "version": "8.1.1", 1548 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", 1549 | "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", 1550 | "dev": true, 1551 | "dependencies": { 1552 | "has-flag": "^4.0.0" 1553 | }, 1554 | "engines": { 1555 | "node": ">=10" 1556 | }, 1557 | "funding": { 1558 | "url": "https://github.com/chalk/supports-color?sponsor=1" 1559 | } 1560 | }, 1561 | "node_modules/ms": { 1562 | "version": "2.1.3", 1563 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1564 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1565 | "dev": true, 1566 | "license": "MIT" 1567 | }, 1568 | "node_modules/natural-compare": { 1569 | "version": "1.4.0", 1570 | "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", 1571 | "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", 1572 | "dev": true 1573 | }, 1574 | "node_modules/nise": { 1575 | "version": "5.1.9", 1576 | "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz", 1577 | "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==", 1578 | "dev": true, 1579 | "dependencies": { 1580 | "@sinonjs/commons": "^3.0.0", 1581 | "@sinonjs/fake-timers": "^11.2.2", 1582 | "@sinonjs/text-encoding": "^0.7.2", 1583 | "just-extend": "^6.2.0", 1584 | "path-to-regexp": "^6.2.1" 1585 | } 1586 | }, 1587 | "node_modules/nock": { 1588 | "version": "13.5.4", 1589 | "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.4.tgz", 1590 | "integrity": "sha512-yAyTfdeNJGGBFxWdzSKCBYxs5FxLbCg5X5Q4ets974hcQzG1+qCxvIyOo4j2Ry6MUlhWVMX4OoYDefAIIwupjw==", 1591 | "dev": true, 1592 | "dependencies": { 1593 | "debug": "^4.1.0", 1594 | "json-stringify-safe": "^5.0.1", 1595 | "propagate": "^2.0.0" 1596 | }, 1597 | "engines": { 1598 | "node": ">= 10.13" 1599 | } 1600 | }, 1601 | "node_modules/normalize-path": { 1602 | "version": "3.0.0", 1603 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 1604 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 1605 | "dev": true, 1606 | "engines": { 1607 | "node": ">=0.10.0" 1608 | } 1609 | }, 1610 | "node_modules/once": { 1611 | "version": "1.4.0", 1612 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 1613 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 1614 | "dev": true, 1615 | "dependencies": { 1616 | "wrappy": "1" 1617 | } 1618 | }, 1619 | "node_modules/optionator": { 1620 | "version": "0.9.3", 1621 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", 1622 | "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", 1623 | "dev": true, 1624 | "dependencies": { 1625 | "@aashutoshrathi/word-wrap": "^1.2.3", 1626 | "deep-is": "^0.1.3", 1627 | "fast-levenshtein": "^2.0.6", 1628 | "levn": "^0.4.1", 1629 | "prelude-ls": "^1.2.1", 1630 | "type-check": "^0.4.0" 1631 | }, 1632 | "engines": { 1633 | "node": ">= 0.8.0" 1634 | } 1635 | }, 1636 | "node_modules/p-limit": { 1637 | "version": "3.1.0", 1638 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 1639 | "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 1640 | "dev": true, 1641 | "dependencies": { 1642 | "yocto-queue": "^0.1.0" 1643 | }, 1644 | "engines": { 1645 | "node": ">=10" 1646 | }, 1647 | "funding": { 1648 | "url": "https://github.com/sponsors/sindresorhus" 1649 | } 1650 | }, 1651 | "node_modules/p-locate": { 1652 | "version": "5.0.0", 1653 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 1654 | "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 1655 | "dev": true, 1656 | "dependencies": { 1657 | "p-limit": "^3.0.2" 1658 | }, 1659 | "engines": { 1660 | "node": ">=10" 1661 | }, 1662 | "funding": { 1663 | "url": "https://github.com/sponsors/sindresorhus" 1664 | } 1665 | }, 1666 | "node_modules/parent-module": { 1667 | "version": "1.0.1", 1668 | "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", 1669 | "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", 1670 | "dev": true, 1671 | "dependencies": { 1672 | "callsites": "^3.0.0" 1673 | }, 1674 | "engines": { 1675 | "node": ">=6" 1676 | } 1677 | }, 1678 | "node_modules/path-exists": { 1679 | "version": "4.0.0", 1680 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 1681 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 1682 | "dev": true, 1683 | "engines": { 1684 | "node": ">=8" 1685 | } 1686 | }, 1687 | "node_modules/path-is-absolute": { 1688 | "version": "1.0.1", 1689 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 1690 | "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", 1691 | "dev": true, 1692 | "engines": { 1693 | "node": ">=0.10.0" 1694 | } 1695 | }, 1696 | "node_modules/path-key": { 1697 | "version": "3.1.1", 1698 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 1699 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 1700 | "dev": true, 1701 | "engines": { 1702 | "node": ">=8" 1703 | } 1704 | }, 1705 | "node_modules/path-to-regexp": { 1706 | "version": "6.2.1", 1707 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", 1708 | "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==", 1709 | "dev": true 1710 | }, 1711 | "node_modules/picomatch": { 1712 | "version": "2.3.1", 1713 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 1714 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 1715 | "dev": true, 1716 | "engines": { 1717 | "node": ">=8.6" 1718 | }, 1719 | "funding": { 1720 | "url": "https://github.com/sponsors/jonschlinkert" 1721 | } 1722 | }, 1723 | "node_modules/prelude-ls": { 1724 | "version": "1.2.1", 1725 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", 1726 | "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", 1727 | "dev": true, 1728 | "engines": { 1729 | "node": ">= 0.8.0" 1730 | } 1731 | }, 1732 | "node_modules/propagate": { 1733 | "version": "2.0.1", 1734 | "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", 1735 | "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", 1736 | "dev": true, 1737 | "engines": { 1738 | "node": ">= 8" 1739 | } 1740 | }, 1741 | "node_modules/proxy-from-env": { 1742 | "version": "1.1.0", 1743 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 1744 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" 1745 | }, 1746 | "node_modules/punycode": { 1747 | "version": "2.3.1", 1748 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", 1749 | "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", 1750 | "dev": true, 1751 | "engines": { 1752 | "node": ">=6" 1753 | } 1754 | }, 1755 | "node_modules/querystring": { 1756 | "version": "0.2.0", 1757 | "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", 1758 | "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", 1759 | "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", 1760 | "engines": { 1761 | "node": ">=0.4.x" 1762 | } 1763 | }, 1764 | "node_modules/queue-microtask": { 1765 | "version": "1.2.3", 1766 | "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", 1767 | "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", 1768 | "dev": true, 1769 | "funding": [ 1770 | { 1771 | "type": "github", 1772 | "url": "https://github.com/sponsors/feross" 1773 | }, 1774 | { 1775 | "type": "patreon", 1776 | "url": "https://www.patreon.com/feross" 1777 | }, 1778 | { 1779 | "type": "consulting", 1780 | "url": "https://feross.org/support" 1781 | } 1782 | ] 1783 | }, 1784 | "node_modules/randombytes": { 1785 | "version": "2.1.0", 1786 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", 1787 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", 1788 | "dev": true, 1789 | "license": "MIT", 1790 | "dependencies": { 1791 | "safe-buffer": "^5.1.0" 1792 | } 1793 | }, 1794 | "node_modules/readdirp": { 1795 | "version": "3.6.0", 1796 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 1797 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 1798 | "dev": true, 1799 | "dependencies": { 1800 | "picomatch": "^2.2.1" 1801 | }, 1802 | "engines": { 1803 | "node": ">=8.10.0" 1804 | } 1805 | }, 1806 | "node_modules/require-directory": { 1807 | "version": "2.1.1", 1808 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 1809 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 1810 | "dev": true, 1811 | "engines": { 1812 | "node": ">=0.10.0" 1813 | } 1814 | }, 1815 | "node_modules/resolve-from": { 1816 | "version": "4.0.0", 1817 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", 1818 | "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", 1819 | "dev": true, 1820 | "engines": { 1821 | "node": ">=4" 1822 | } 1823 | }, 1824 | "node_modules/reusify": { 1825 | "version": "1.0.4", 1826 | "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", 1827 | "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", 1828 | "dev": true, 1829 | "engines": { 1830 | "iojs": ">=1.0.0", 1831 | "node": ">=0.10.0" 1832 | } 1833 | }, 1834 | "node_modules/rimraf": { 1835 | "version": "3.0.2", 1836 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", 1837 | "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", 1838 | "dev": true, 1839 | "dependencies": { 1840 | "glob": "^7.1.3" 1841 | }, 1842 | "bin": { 1843 | "rimraf": "bin.js" 1844 | }, 1845 | "funding": { 1846 | "url": "https://github.com/sponsors/isaacs" 1847 | } 1848 | }, 1849 | "node_modules/run-parallel": { 1850 | "version": "1.2.0", 1851 | "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", 1852 | "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", 1853 | "dev": true, 1854 | "funding": [ 1855 | { 1856 | "type": "github", 1857 | "url": "https://github.com/sponsors/feross" 1858 | }, 1859 | { 1860 | "type": "patreon", 1861 | "url": "https://www.patreon.com/feross" 1862 | }, 1863 | { 1864 | "type": "consulting", 1865 | "url": "https://feross.org/support" 1866 | } 1867 | ], 1868 | "dependencies": { 1869 | "queue-microtask": "^1.2.2" 1870 | } 1871 | }, 1872 | "node_modules/safe-buffer": { 1873 | "version": "5.2.1", 1874 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1875 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 1876 | "dev": true, 1877 | "funding": [ 1878 | { 1879 | "type": "github", 1880 | "url": "https://github.com/sponsors/feross" 1881 | }, 1882 | { 1883 | "type": "patreon", 1884 | "url": "https://www.patreon.com/feross" 1885 | }, 1886 | { 1887 | "type": "consulting", 1888 | "url": "https://feross.org/support" 1889 | } 1890 | ], 1891 | "license": "MIT" 1892 | }, 1893 | "node_modules/sax": { 1894 | "version": "1.2.1", 1895 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", 1896 | "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" 1897 | }, 1898 | "node_modules/serialize-javascript": { 1899 | "version": "6.0.2", 1900 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", 1901 | "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", 1902 | "dev": true, 1903 | "license": "BSD-3-Clause", 1904 | "dependencies": { 1905 | "randombytes": "^2.1.0" 1906 | } 1907 | }, 1908 | "node_modules/shebang-command": { 1909 | "version": "2.0.0", 1910 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 1911 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 1912 | "dev": true, 1913 | "dependencies": { 1914 | "shebang-regex": "^3.0.0" 1915 | }, 1916 | "engines": { 1917 | "node": ">=8" 1918 | } 1919 | }, 1920 | "node_modules/shebang-regex": { 1921 | "version": "3.0.0", 1922 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 1923 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 1924 | "dev": true, 1925 | "engines": { 1926 | "node": ">=8" 1927 | } 1928 | }, 1929 | "node_modules/sinon": { 1930 | "version": "17.0.1", 1931 | "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", 1932 | "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", 1933 | "dev": true, 1934 | "dependencies": { 1935 | "@sinonjs/commons": "^3.0.0", 1936 | "@sinonjs/fake-timers": "^11.2.2", 1937 | "@sinonjs/samsam": "^8.0.0", 1938 | "diff": "^5.1.0", 1939 | "nise": "^5.1.5", 1940 | "supports-color": "^7.2.0" 1941 | }, 1942 | "funding": { 1943 | "type": "opencollective", 1944 | "url": "https://opencollective.com/sinon" 1945 | } 1946 | }, 1947 | "node_modules/string-width": { 1948 | "version": "4.2.3", 1949 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1950 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1951 | "dev": true, 1952 | "dependencies": { 1953 | "emoji-regex": "^8.0.0", 1954 | "is-fullwidth-code-point": "^3.0.0", 1955 | "strip-ansi": "^6.0.1" 1956 | }, 1957 | "engines": { 1958 | "node": ">=8" 1959 | } 1960 | }, 1961 | "node_modules/strip-ansi": { 1962 | "version": "6.0.1", 1963 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1964 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1965 | "dev": true, 1966 | "dependencies": { 1967 | "ansi-regex": "^5.0.1" 1968 | }, 1969 | "engines": { 1970 | "node": ">=8" 1971 | } 1972 | }, 1973 | "node_modules/strip-json-comments": { 1974 | "version": "3.1.1", 1975 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 1976 | "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 1977 | "dev": true, 1978 | "engines": { 1979 | "node": ">=8" 1980 | }, 1981 | "funding": { 1982 | "url": "https://github.com/sponsors/sindresorhus" 1983 | } 1984 | }, 1985 | "node_modules/supports-color": { 1986 | "version": "7.2.0", 1987 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 1988 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 1989 | "dev": true, 1990 | "dependencies": { 1991 | "has-flag": "^4.0.0" 1992 | }, 1993 | "engines": { 1994 | "node": ">=8" 1995 | } 1996 | }, 1997 | "node_modules/text-table": { 1998 | "version": "0.2.0", 1999 | "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", 2000 | "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", 2001 | "dev": true 2002 | }, 2003 | "node_modules/to-regex-range": { 2004 | "version": "5.0.1", 2005 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 2006 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 2007 | "dev": true, 2008 | "dependencies": { 2009 | "is-number": "^7.0.0" 2010 | }, 2011 | "engines": { 2012 | "node": ">=8.0" 2013 | } 2014 | }, 2015 | "node_modules/traverse": { 2016 | "version": "0.6.7", 2017 | "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.7.tgz", 2018 | "integrity": "sha512-/y956gpUo9ZNCb99YjxG7OaslxZWHfCHAUUfshwqOXmxUIvqLjVO581BT+gM59+QV9tFe6/CGG53tsA1Y7RSdg==", 2019 | "dev": true, 2020 | "funding": { 2021 | "url": "https://github.com/sponsors/ljharb" 2022 | } 2023 | }, 2024 | "node_modules/type-check": { 2025 | "version": "0.4.0", 2026 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", 2027 | "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", 2028 | "dev": true, 2029 | "dependencies": { 2030 | "prelude-ls": "^1.2.1" 2031 | }, 2032 | "engines": { 2033 | "node": ">= 0.8.0" 2034 | } 2035 | }, 2036 | "node_modules/type-detect": { 2037 | "version": "4.0.8", 2038 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", 2039 | "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", 2040 | "dev": true, 2041 | "engines": { 2042 | "node": ">=4" 2043 | } 2044 | }, 2045 | "node_modules/type-fest": { 2046 | "version": "0.20.2", 2047 | "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", 2048 | "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", 2049 | "dev": true, 2050 | "engines": { 2051 | "node": ">=10" 2052 | }, 2053 | "funding": { 2054 | "url": "https://github.com/sponsors/sindresorhus" 2055 | } 2056 | }, 2057 | "node_modules/uri-js": { 2058 | "version": "4.4.1", 2059 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 2060 | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 2061 | "dev": true, 2062 | "dependencies": { 2063 | "punycode": "^2.1.0" 2064 | } 2065 | }, 2066 | "node_modules/url": { 2067 | "version": "0.10.3", 2068 | "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", 2069 | "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", 2070 | "dependencies": { 2071 | "punycode": "1.3.2", 2072 | "querystring": "0.2.0" 2073 | } 2074 | }, 2075 | "node_modules/url/node_modules/punycode": { 2076 | "version": "1.3.2", 2077 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", 2078 | "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" 2079 | }, 2080 | "node_modules/util": { 2081 | "version": "0.12.5", 2082 | "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", 2083 | "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", 2084 | "dependencies": { 2085 | "inherits": "^2.0.3", 2086 | "is-arguments": "^1.0.4", 2087 | "is-generator-function": "^1.0.7", 2088 | "is-typed-array": "^1.1.3", 2089 | "which-typed-array": "^1.1.2" 2090 | } 2091 | }, 2092 | "node_modules/uuid": { 2093 | "version": "8.0.0", 2094 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", 2095 | "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", 2096 | "bin": { 2097 | "uuid": "dist/bin/uuid" 2098 | } 2099 | }, 2100 | "node_modules/which": { 2101 | "version": "2.0.2", 2102 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 2103 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 2104 | "dev": true, 2105 | "dependencies": { 2106 | "isexe": "^2.0.0" 2107 | }, 2108 | "bin": { 2109 | "node-which": "bin/node-which" 2110 | }, 2111 | "engines": { 2112 | "node": ">= 8" 2113 | } 2114 | }, 2115 | "node_modules/which-typed-array": { 2116 | "version": "1.1.10", 2117 | "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.10.tgz", 2118 | "integrity": "sha512-uxoA5vLUfRPdjCuJ1h5LlYdmTLbYfums398v3WLkM+i/Wltl2/XyZpQWKbN++ck5L64SR/grOHqtXCUKmlZPNA==", 2119 | "dependencies": { 2120 | "available-typed-arrays": "^1.0.5", 2121 | "call-bind": "^1.0.2", 2122 | "for-each": "^0.3.3", 2123 | "gopd": "^1.0.1", 2124 | "has-tostringtag": "^1.0.0", 2125 | "is-typed-array": "^1.1.10" 2126 | }, 2127 | "engines": { 2128 | "node": ">= 0.4" 2129 | }, 2130 | "funding": { 2131 | "url": "https://github.com/sponsors/ljharb" 2132 | } 2133 | }, 2134 | "node_modules/workerpool": { 2135 | "version": "6.5.1", 2136 | "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", 2137 | "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", 2138 | "dev": true, 2139 | "license": "Apache-2.0" 2140 | }, 2141 | "node_modules/wrap-ansi": { 2142 | "version": "7.0.0", 2143 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 2144 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 2145 | "dev": true, 2146 | "dependencies": { 2147 | "ansi-styles": "^4.0.0", 2148 | "string-width": "^4.1.0", 2149 | "strip-ansi": "^6.0.0" 2150 | }, 2151 | "engines": { 2152 | "node": ">=10" 2153 | }, 2154 | "funding": { 2155 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 2156 | } 2157 | }, 2158 | "node_modules/wrappy": { 2159 | "version": "1.0.2", 2160 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 2161 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 2162 | "dev": true 2163 | }, 2164 | "node_modules/xml2js": { 2165 | "version": "0.6.2", 2166 | "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", 2167 | "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", 2168 | "dependencies": { 2169 | "sax": ">=0.6.0", 2170 | "xmlbuilder": "~11.0.0" 2171 | }, 2172 | "engines": { 2173 | "node": ">=4.0.0" 2174 | } 2175 | }, 2176 | "node_modules/xmlbuilder": { 2177 | "version": "11.0.1", 2178 | "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", 2179 | "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", 2180 | "engines": { 2181 | "node": ">=4.0" 2182 | } 2183 | }, 2184 | "node_modules/y18n": { 2185 | "version": "5.0.8", 2186 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 2187 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 2188 | "dev": true, 2189 | "engines": { 2190 | "node": ">=10" 2191 | } 2192 | }, 2193 | "node_modules/yargs": { 2194 | "version": "16.2.0", 2195 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", 2196 | "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", 2197 | "dev": true, 2198 | "dependencies": { 2199 | "cliui": "^7.0.2", 2200 | "escalade": "^3.1.1", 2201 | "get-caller-file": "^2.0.5", 2202 | "require-directory": "^2.1.1", 2203 | "string-width": "^4.2.0", 2204 | "y18n": "^5.0.5", 2205 | "yargs-parser": "^20.2.2" 2206 | }, 2207 | "engines": { 2208 | "node": ">=10" 2209 | } 2210 | }, 2211 | "node_modules/yargs-parser": { 2212 | "version": "20.2.9", 2213 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", 2214 | "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", 2215 | "dev": true, 2216 | "license": "ISC", 2217 | "engines": { 2218 | "node": ">=10" 2219 | } 2220 | }, 2221 | "node_modules/yargs-unparser": { 2222 | "version": "2.0.0", 2223 | "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", 2224 | "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", 2225 | "dev": true, 2226 | "dependencies": { 2227 | "camelcase": "^6.0.0", 2228 | "decamelize": "^4.0.0", 2229 | "flat": "^5.0.2", 2230 | "is-plain-obj": "^2.1.0" 2231 | }, 2232 | "engines": { 2233 | "node": ">=10" 2234 | } 2235 | }, 2236 | "node_modules/yocto-queue": { 2237 | "version": "0.1.0", 2238 | "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 2239 | "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 2240 | "dev": true, 2241 | "engines": { 2242 | "node": ">=10" 2243 | }, 2244 | "funding": { 2245 | "url": "https://github.com/sponsors/sindresorhus" 2246 | } 2247 | } 2248 | } 2249 | } 2250 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scanii-lambda", 3 | "version": "2.11.0", 4 | "description": "Uva Software's Scanii.com Lambda/SAM Integration", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node_modules/mocha/bin/_mocha tests --recursive --exit --no-timeouts" 8 | }, 9 | "author": "", 10 | "license": "Apache-2.0", 11 | "dependencies": { 12 | "aws-sdk": "^2.1583.0", 13 | "axios": "^1.8.2" 14 | }, 15 | "devDependencies": { 16 | "aws-sdk-mock": "^5.9.0", 17 | "eslint": "^8.57.0", 18 | "mocha": "^10.8.2", 19 | "nock": "^13.5.4", 20 | "sinon": "^17.0.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /template.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: Scanii.com automatic S3 integration 4 | 5 | Parameters: 6 | bucketName: 7 | Description: The bucket you would like to monitor for events 8 | Type: String 9 | scaniiApiKey: 10 | Description: Your scanii.com API key 11 | Type: String 12 | scaniiApiSecret: 13 | Description: Your scanii.com API secret 14 | Type: String 15 | scaniiApiEndpoint: 16 | Description: Which endpoint would you like to use? (https://docs.scanii.com/article/161-endpoints-and-regions) 17 | Type: String 18 | Default: api-us1.scanii.com 19 | AllowedValues: 20 | - api-eu1.scanii.com 21 | - api-eu2.scanii.com 22 | - api-ap1.scanii.com 23 | - api-ap2.scanii.com 24 | - api-us1.scanii.com 25 | - api-ca1.scanii.com 26 | actionTagObject: 27 | Description: Should custom tags be added to S3 objects after processing? 28 | Type: String 29 | Default: yes 30 | AllowedValues: 31 | - true 32 | - false 33 | actionDeleteObjectOnFinding: 34 | Description: Should S3 objects be DELETED once a finding is identified? 35 | Type: String 36 | Default: no 37 | AllowedValues: 38 | - true 39 | - false 40 | 41 | Resources: 42 | ScaniiSubmitFn: 43 | Type: AWS::Serverless::Function 44 | DependsOn: ScaniiCallbackFn 45 | Properties: 46 | CodeUri: . 47 | FunctionName: !Sub "${AWS::StackName}-Submit" 48 | Handler: lib/s3-handler.handler 49 | Runtime: nodejs20.x 50 | MemorySize: 256 51 | Timeout: 300 52 | Description: Submits objects to be analyzed by scanii.com 53 | AutoPublishAlias: live # enables automatic version tracking 54 | 55 | Environment: 56 | Variables: 57 | API_KEY: !Sub ${scaniiApiKey} 58 | API_SECRET: !Sub ${scaniiApiSecret} 59 | API_ENDPOINT: !Sub ${scaniiApiEndpoint} 60 | CALLBACK_URL: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/callback" 61 | 62 | Policies: 63 | - S3ReadPolicy: 64 | BucketName: 65 | Ref: bucketName 66 | 67 | ScaniiCallbackFn: 68 | Type: AWS::Serverless::Function 69 | Properties: 70 | CodeUri: . 71 | FunctionName: !Sub "${AWS::StackName}-Callback" 72 | Handler: lib/ag-handler.handler 73 | Runtime: nodejs20.x 74 | MemorySize: 256 75 | Timeout: 60 76 | Description: Handles scanii.com's result callbacks 77 | AutoPublishAlias: live # enables automatic version tracking 78 | 79 | Environment: 80 | Variables: 81 | API_KEY: !Sub ${scaniiApiKey} 82 | API_SECRET: !Sub ${scaniiApiSecret} 83 | ACTION_TAG_OBJECT: !Sub ${actionTagObject} 84 | ACTION_DELETE_OBJECT: !Sub ${actionDeleteObjectOnFinding} 85 | 86 | Events: 87 | ScaniiCallback: 88 | Type: Api 89 | Properties: 90 | Path: /{proxy+} 91 | Method: any 92 | 93 | Policies: 94 | - Statement: 95 | Effect: "Allow" 96 | Action: 97 | - "s3:DeleteObject" 98 | - "s3:DeleteObjectTagging" 99 | - "s3:GetObject" 100 | - "s3:GetObjectTagging" 101 | - "s3:GetObjectVersionTagging" 102 | - "s3:PutObjectTagging" 103 | - "s3:PutObjectVersionTagging" 104 | Resource: !Sub "arn:aws:s3:::${bucketName}/*" 105 | 106 | Outputs: 107 | ApiURL: 108 | Description: "Status!" 109 | Value: "Scanii-lambda reporting for duty!" 110 | -------------------------------------------------------------------------------- /tests/actions.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const it = require("mocha/lib/mocha.js").it; 3 | const describe = require("mocha/lib/mocha.js").describe; 4 | const beforeEach = require("mocha/lib/mocha.js").beforeEach; 5 | const afterEach = require("mocha/lib/mocha.js").afterEach; 6 | const actions = require('../lib/actions'); 7 | const AWS = require('aws-sdk-mock'); 8 | 9 | describe('Actions tests', () => { 10 | 11 | let putObjectTagCallCount = 0; 12 | beforeEach(() => { 13 | putObjectTagCallCount = 0; 14 | }); 15 | 16 | afterEach(function () { 17 | AWS.restore(); 18 | }); 19 | 20 | it('should add correct tags if findings', async () => { 21 | const result = { 22 | "id": "2e4612793298b1d691202e75dc125f6e", 23 | "checksum": "30d3007d8fa7e76f2741805fbaf1c8bba9a00051", 24 | "content_length": "1251174", 25 | "findings": ["finding1"], 26 | "creation_date": "2016-01-24T15:05:53.260Z", 27 | "content_type": "image/jpeg", 28 | "metadata": { 29 | "signature": "abc", 30 | "bucket": "test-bucket", 31 | "key": "test-key" 32 | } 33 | }; 34 | 35 | AWS.mock('S3', 'putObjectTagging', async (params, callback) => { 36 | assert.ok(params.Bucket === result.metadata.bucket); 37 | assert.ok(params.Key === result.metadata.key); 38 | 39 | assert.ok(params.Tagging.TagSet[0].Key === "ScaniiFindings"); 40 | assert.ok(params.Tagging.TagSet[0].Value === result.findings.join(' ')); 41 | 42 | assert.ok(params.Tagging.TagSet[1].Key === "ScaniiId"); 43 | assert.ok(params.Tagging.TagSet[1].Value === result.id); 44 | 45 | assert.ok(params.Tagging.TagSet[2].Key === "ScaniiContentType"); 46 | assert.ok(params.Tagging.TagSet[2].Value === result.content_type); 47 | callback(); 48 | putObjectTagCallCount++; 49 | }); 50 | 51 | AWS.mock('S3', 'getObjectTagging', async (params, callback) => { 52 | callback(null, { 53 | TagSet: [] 54 | }); 55 | }); 56 | 57 | await actions.tagObject(result.metadata.bucket, result.metadata.key, result); 58 | assert(putObjectTagCallCount === 1); 59 | }); 60 | 61 | it('should add correct tags if no findings', async () => { 62 | const result = { 63 | "id": "2e4612793298b1d691202e75dc125f6e", 64 | "checksum": "30d3007d8fa7e76f2741805fbaf1c8bba9a00051", 65 | "content_length": "1251174", 66 | "findings": [], 67 | "creation_date": "2016-01-24T15:05:53.260Z", 68 | "content_type": "image/jpeg", 69 | "metadata": { 70 | "signature": "abc", 71 | "bucket": "test-bucket", 72 | "key": "test-key" 73 | } 74 | }; 75 | 76 | AWS.mock('S3', 'putObjectTagging', async (params, callback) => { 77 | assert.ok(params.Bucket === result.metadata.bucket); 78 | assert.ok(params.Key === result.metadata.key); 79 | 80 | assert.ok(params.Tagging.TagSet[0].Key === "ScaniiFindings"); 81 | assert.ok(params.Tagging.TagSet[0].Value === 'None'); 82 | 83 | assert.ok(params.Tagging.TagSet[1].Key === "ScaniiId"); 84 | assert.ok(params.Tagging.TagSet[1].Value === result.id); 85 | 86 | assert.ok(params.Tagging.TagSet[2].Key === "ScaniiContentType"); 87 | assert.ok(params.Tagging.TagSet[2].Value === result.content_type); 88 | callback(); 89 | putObjectTagCallCount++; 90 | return true; 91 | }); 92 | 93 | AWS.mock('S3', 'getObjectTagging', async (params, callback) => { 94 | callback(null, { 95 | TagSet: [] 96 | }); 97 | }); 98 | 99 | await actions.tagObject(result.metadata.bucket, result.metadata.key, result); 100 | assert(putObjectTagCallCount === 1); 101 | 102 | }); 103 | it('should truncate tag values', async () => { 104 | const result = { 105 | "id": "2e4612793298b1d691202e75dc125f6e", 106 | "checksum": "30d3007d8fa7e76f2741805fbaf1c8bba9a00051", 107 | "content_length": "1251174", 108 | "findings": ["a".repeat(300)], 109 | "creation_date": "2016-01-24T15:05:53.260Z", 110 | "content_type": "image/jpeg", 111 | "metadata": { 112 | "signature": "abc", 113 | "bucket": "test-bucket", 114 | "key": "test-key" 115 | } 116 | }; 117 | 118 | AWS.mock('S3', 'putObjectTagging', async (params, callback) => { 119 | assert.ok(params.Tagging.TagSet[0].Key === "ScaniiFindings"); 120 | assert.ok(params.Tagging.TagSet[0].Value.length < 256); 121 | callback(); 122 | putObjectTagCallCount++; 123 | return true; 124 | }); 125 | 126 | AWS.mock('S3', 'getObjectTagging', async (params, callback) => { 127 | callback(null, { 128 | TagSet: [] 129 | }); 130 | }); 131 | 132 | await actions.tagObject(result.metadata.bucket, result.metadata.key, result); 133 | assert(putObjectTagCallCount === 1); 134 | }); 135 | 136 | it('should append not replace tags', async () => { 137 | 138 | AWS.mock('S3', 'putObjectTagging', async (params, callback) => { 139 | assert.ok(params.Bucket === result.metadata.bucket); 140 | assert.ok(params.Key === result.metadata.key); 141 | assert.ok(params.Tagging.TagSet.length === 4); 142 | putObjectTagCallCount++; 143 | 144 | }); 145 | 146 | AWS.mock('S3', 'getObjectTagging', async (params, callback) => { 147 | callback(null, { 148 | TagSet: [ 149 | { 150 | Key: "Tag1", 151 | Value: "Value1" 152 | } 153 | ] 154 | }); 155 | }); 156 | 157 | const result = { 158 | "id": "2e4612793298b1d691202e75dc125f6e", 159 | "checksum": "30d3007d8fa7e76f2741805fbaf1c8bba9a00051", 160 | "content_length": "1251174", 161 | "findings": ["malware.a", "malware.b"], 162 | "creation_date": "2016-01-24T15:05:53.260Z", 163 | "content_type": "image/jpeg", 164 | "metadata": { 165 | "signature": "abc", 166 | "bucket": "test-bucket", 167 | "key": "test-key" 168 | } 169 | }; 170 | 171 | await actions.tagObject(result.metadata.bucket, result.metadata.key, result); 172 | assert(putObjectTagCallCount === 1); 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /tests/ag-handler.js: -------------------------------------------------------------------------------- 1 | const handler = require('../lib/ag-handler.js').handler; 2 | const assert = require('assert'); 3 | const it = require("mocha/lib/mocha.js").it; 4 | const describe = require("mocha/lib/mocha.js").describe; 5 | const beforeEach = require("mocha/lib/mocha.js").beforeEach; 6 | const afterEach = require("mocha/lib/mocha.js").afterEach; 7 | const utils = require('../lib/utils.js'); 8 | const CONFIG = require('../lib/config').CONFIG; 9 | const AWS = require('aws-sdk-mock'); 10 | 11 | 12 | describe('Api Gateway handler tests', () => { 13 | beforeEach(() => { 14 | 15 | // wrapping some fakes around the AWS sdk: 16 | AWS.mock('S3', 'getSignedUrl', () => 'https://example.com/1234?q=124'); 17 | 18 | AWS.mock('S3', 'deleteObject', async () => { 19 | }); 20 | AWS.mock('S3', 'putObjectTagging', async () => { 21 | }); 22 | 23 | CONFIG.ACTION_DELETE_OBJECT = true; 24 | CONFIG.SECRET = "secret"; 25 | CONFIG.KEY = "key"; 26 | CONFIG.CALLBACK_URL = "https://example.com/callback/"; 27 | }); 28 | 29 | 30 | afterEach(() => { 31 | AWS.restore(); 32 | }); 33 | 34 | it('should handle a callback without findings', async () => { 35 | await handler(hydrateEvent({ 36 | "id": "2e4612793298b1d691202e75dc125f6e", 37 | "checksum": "30d3007d8fa7e76f2741805fbaf1c8bba9a00051", 38 | "content_length": "1251174", 39 | "findings": [], 40 | "creation_date": "2016-01-24T15:05:53.260Z", 41 | "content_type": "image/jpeg", 42 | "metadata": { 43 | "signature": utils.generateSignature("test-bucket", "test-key"), 44 | "bucket": "test-bucket", 45 | "key": "test-key" 46 | } 47 | }), {}, (error, result) => { 48 | "use strict"; 49 | assert(error === null, "there should be no errors"); 50 | assert(result.statusCode === 200); 51 | }); 52 | }); 53 | 54 | it('should handle a bogus callback', async () => { 55 | await handler(hydrateEvent({"hello": "world"}), 56 | {}, (error, result) => { 57 | "use strict"; 58 | assert(error === null, "there should be no errors"); 59 | assert(result.statusCode === 500, "should return the file id"); 60 | assert(result.body.includes("no id provided")); 61 | }); 62 | }); 63 | 64 | it('should require bucket/key in callback metadata', async () => { 65 | await handler(hydrateEvent({ 66 | "id": "2e4612793298b1d691202e75dc125f6e", 67 | "checksum": "30d3007d8fa7e76f2741805fbaf1c8bba9a00051", 68 | "content_length": "1251174", 69 | "findings": [], 70 | "creation_date": "2016-01-24T15:05:53.260Z", 71 | "content_type": "image/jpeg", 72 | "metadata": { 73 | "signature": utils.generateSignature("test-bucket", "test-key"), 74 | } 75 | }), 76 | {}, (error, result) => { 77 | "use strict"; 78 | assert(error === null, "there should be no errors"); 79 | assert(result.statusCode === 500, "should return the file id"); 80 | assert(result.body.includes("no bucket supplied in metadata")); 81 | }); 82 | }); 83 | 84 | it('should handle callbacks with findings', async () => { 85 | 86 | await handler(hydrateEvent({ 87 | "id": "2e4612793298b1d691202e75dc125f6e", 88 | "checksum": "30d3007d8fa7e76f2741805fbaf1c8bba9a00051", 89 | "content_length": "1251174", 90 | "findings": ['finding1', 'finding2'], 91 | "creation_date": "2016-01-24T15:05:53.260Z", 92 | "content_type": "image/jpeg", 93 | "metadata": { 94 | "signature": utils.generateSignature("test-bucket", "test-key"), 95 | "bucket": "test-bucket", 96 | "key": "test-key" 97 | } 98 | }), {}, (error, result) => { 99 | "use strict"; 100 | assert(error === null, "there should be no errors"); 101 | assert(result.statusCode === 200); 102 | }); 103 | }); 104 | 105 | it('should ensure callback signatures match', async () => { 106 | await handler(hydrateEvent({ 107 | "id": "2e4612793298b1d691202e75dc125f6e", 108 | "checksum": "30d3007d8fa7e76f2741805fbaf1c8bba9a00051", 109 | "content_length": "1251174", 110 | "findings": ['finding1', 'finding2'], 111 | "creation_date": "2016-01-24T15:05:53.260Z", 112 | "content_type": "image/jpeg", 113 | "metadata": { 114 | "signature": utils.generateSignature("test-bucket", "test-key"), 115 | "bucket": "test-bucket", 116 | "key": "test-key" 117 | } 118 | }), {}, (error, result) => { 119 | "use strict"; 120 | assert(error === null, "there should be no errors"); 121 | assert(result.statusCode === 200); 122 | }); 123 | }); 124 | it('should ensure callback signatures match - negative', async () => { 125 | 126 | await handler(hydrateEvent({ 127 | "id": "2e4612793298b1d691202e75dc125f6e", 128 | "checksum": "30d3007d8fa7e76f2741805fbaf1c8bba9a00051", 129 | "content_length": "1251174", 130 | "findings": ['finding1', 'finding2'], 131 | "creation_date": "2016-01-24T15:05:53.260Z", 132 | "content_type": "image/jpeg", 133 | "metadata": { 134 | "signature": utils.generateSignature("test-bucket", "wrong-key"), 135 | "bucket": "test-bucket", 136 | "key": "test-key" 137 | } 138 | }), {}, (error, result) => { 139 | "use strict"; 140 | assert(error === null, "there should be no errors"); 141 | assert(result.statusCode === 500); 142 | }); 143 | }); 144 | 145 | it('should enforce signatures in callbacks', async () => { 146 | await handler(hydrateEvent({ 147 | "id": "2e4612793298b1d691202e75dc125f6e", 148 | "checksum": "30d3007d8fa7e76f2741805fbaf1c8bba9a00051", 149 | "content_length": "1251174", 150 | "findings": ['finding1', 'finding2'], 151 | "creation_date": "2016-01-24T15:05:53.260Z", 152 | "content_type": "image/jpeg", 153 | "metadata": { 154 | "signature": "1234", 155 | "bucket": "test-bucket", 156 | "key": "test-key" 157 | } 158 | }), {}, (error, result) => { 159 | "use strict"; 160 | "use strict"; 161 | assert(error === null, "there should be no errors"); 162 | assert(result.statusCode === 500, "should return the file id"); 163 | assert(result.body.includes("invalid signature")); 164 | }); 165 | }); 166 | 167 | it('should handle api gateway proxy callbacks', async () => { 168 | await handler(hydrateEvent({ 169 | "id": "2e4612793298b1d691202e75dc125f6e", 170 | "checksum": "30d3007d8fa7e76f2741805fbaf1c8bba9a00051", 171 | "content_length": "1251174", 172 | "findings": ['finding1', 'finding2'], 173 | "creation_date": "2016-01-24T15:05:53.260Z", 174 | "content_type": "image/jpeg", 175 | "metadata": { 176 | "signature": utils.generateSignature("test-bucket", "test-key"), 177 | "bucket": "test-bucket", 178 | "key": "test-key" 179 | } 180 | }), {}, (error, result) => { 181 | "use strict"; 182 | assert(error === null, "there should be no errors"); 183 | assert(result.statusCode === 200); 184 | }); 185 | }); 186 | 187 | it('should handle api gateway proxy callbacks and findings', async () => { 188 | await handler(hydrateEvent({ 189 | "id": "2e4612793298b1d691202e75dc125f6e", 190 | "checksum": "30d3007d8fa7e76f2741805fbaf1c8bba9a00051", 191 | "content_length": "1251174", 192 | "findings": ['finding1', 'finding2'], 193 | "creation_date": "2016-01-24T15:05:53.260Z", 194 | "content_type": "image/jpeg", 195 | "metadata": { 196 | "signature": utils.generateSignature("test-bucket", "test-key"), 197 | "bucket": "test-bucket", 198 | "key": "test-key" 199 | } 200 | }), {}, (error, result) => { 201 | "use strict"; 202 | assert(error === null, "there should be no errors"); 203 | assert(result.statusCode === 200); 204 | }); 205 | }); 206 | it('should handle api gateway callbacks with errors', async () => { 207 | await handler(hydrateEvent({ 208 | "error": "error message", 209 | "id": "a62a6f0ba82f6ac11e95d09b8bdf965c", 210 | "metadata": { 211 | "signature": utils.generateSignature("test-bucket", "test-key"), 212 | "bucket": "test-bucket", 213 | "key": "test-key" 214 | } 215 | }), {}, (error, result) => { 216 | "use strict"; 217 | assert(error === null, "there should be no errors"); 218 | assert(result.statusCode === 200); 219 | }); 220 | }); 221 | }); 222 | 223 | const hydrateEvent = (body) => { 224 | return { 225 | "body": JSON.stringify(body), 226 | "resource": "/{proxy+}", 227 | "path": "/path/to/resource", 228 | "httpMethod": "POST", 229 | "isBase64Encoded": "false", 230 | "queryStringParameters": { 231 | "foo": "bar" 232 | }, 233 | "pathParameters": { 234 | "proxy": "/path/to/resource" 235 | }, 236 | "stageVariables": { 237 | "baz": "qux" 238 | }, 239 | "headers": { 240 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 241 | "Accept-Encoding": "gzip, deflate, sdch", 242 | "Accept-Language": "en-US,en;q=0.8", 243 | "Cache-Control": "max-age=0", 244 | "CloudFront-Forwarded-Proto": "https", 245 | "CloudFront-Is-Desktop-Viewer": "true", 246 | "CloudFront-Is-Mobile-Viewer": "false", 247 | "CloudFront-Is-SmartTV-Viewer": "false", 248 | "CloudFront-Is-Tablet-Viewer": "false", 249 | "CloudFront-Viewer-Country": "US", 250 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com", 251 | "Upgrade-Insecure-Requests": "1", 252 | "User-Agent": "Custom User Agent String", 253 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", 254 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", 255 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2", 256 | "X-Forwarded-Port": "443", 257 | "X-Forwarded-Proto": "https" 258 | }, 259 | "requestContext": { 260 | "accountId": "123456789012", 261 | "resourceId": "123456", 262 | "stage": "prod", 263 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", 264 | "requestTime": "09/Apr/2015:12:34:56 +0000", 265 | "requestTimeEpoch": 1428582896000, 266 | "identity": { 267 | "cognitoIdentityPoolId": null, 268 | "accountId": null, 269 | "cognitoIdentityId": null, 270 | "caller": null, 271 | "accessKey": null, 272 | "sourceIp": "127.0.0.1", 273 | "cognitoAuthenticationType": null, 274 | "cognitoAuthenticationProvider": null, 275 | "userArn": null, 276 | "userAgent": "Custom User Agent String", 277 | "user": null 278 | }, 279 | "path": "/prod/path/to/resource", 280 | "resourcePath": "/{proxy+}", 281 | "httpMethod": "POST", 282 | "apiId": "1234567890", 283 | "protocol": "HTTP/1.1" 284 | } 285 | } 286 | }; 287 | 288 | -------------------------------------------------------------------------------- /tests/client.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const nock = require('nock') 3 | const scanii = require("../lib/client"); 4 | 5 | describe('client tests', () => { 6 | afterEach(function () { 7 | nock.cleanAll(); 8 | }); 9 | 10 | it('should retry failed fetch requests', async function () { 11 | nock('https://api-us1.scanii.com') 12 | .post('/v2.2/files/fetch') 13 | .reply(555); 14 | 15 | const client = new scanii.ScaniiClient('foo', 'bar', "https://api-us1.scanii.com", 2) 16 | try { 17 | await client.fetch('https://acme.com', 'https://acme.com/callback', {}); 18 | } catch (e) { 19 | assert.ok(e.attempts === 2) 20 | } 21 | return Promise.resolve() 22 | }); 23 | 24 | it('should retry failed retrieve requests', async function () { 25 | nock('https://api-us1.scanii.com') 26 | .post('/v2.2/files/123') 27 | .reply(555); 28 | 29 | const client = new scanii.ScaniiClient('foo', 'bar', "https://api-us1.scanii.com", 2) 30 | try { 31 | await client.retrieve('123'); 32 | } catch (e) { 33 | assert.ok(e.attempts === 2) 34 | } 35 | return Promise.resolve() 36 | }); 37 | 38 | }) 39 | -------------------------------------------------------------------------------- /tests/config.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const it = require("mocha/lib/mocha.js").it; 3 | const describe = require("mocha/lib/mocha.js").describe; 4 | const beforeEach = require("mocha/lib/mocha.js").beforeEach; 5 | const afterEach = require("mocha/lib/mocha.js").afterEach; 6 | const CONFIG = require('../lib/config').CONFIG; 7 | const actions = require('../lib/actions'); 8 | const AWS = require('aws-sdk-mock'); 9 | 10 | describe('Config tests', () => { 11 | 12 | let deleteCounter = 0; 13 | let tagCounter = 0; 14 | const result = { 15 | "id": "2e4612793298b1d691202e75dc125f6e", 16 | "checksum": "30d3007d8fa7e76f2741805fbaf1c8bba9a00051", 17 | "content_length": "1251174", 18 | "findings": ["finding1"], 19 | "creation_date": "2016-01-24T15:05:53.260Z", 20 | "content_type": "image/jpeg", 21 | "metadata": { 22 | "signature": "abc", 23 | "bucket": "test-bucket", 24 | "key": "test-key" 25 | } 26 | }; 27 | 28 | beforeEach(() => { 29 | // resetting 30 | deleteCounter = tagCounter = 0; 31 | 32 | // for some reason we need to monkey patch this: 33 | AWS.mock('S3', 'deleteObject', async () => { 34 | deleteCounter++; 35 | }); 36 | 37 | AWS.mock('S3', 'putObjectTagging', async () => { 38 | tagCounter++; 39 | }); 40 | 41 | AWS.mock('S3', 'getObjectTagging', async (params, callback) => { 42 | callback(null, { 43 | TagSet: [] 44 | }); 45 | }); 46 | }); 47 | 48 | afterEach(function () { 49 | AWS.restore(); 50 | }); 51 | 52 | it("if all actions are disabled, no action should be taken", async () => { 53 | CONFIG.ACTION_DELETE_OBJECT = false; 54 | CONFIG.ACTION_TAG_OBJECT = false; 55 | 56 | await actions.onFindings("bucket1", "key1", result); 57 | assert.strictEqual(0, deleteCounter); 58 | assert.strictEqual(0, tagCounter); 59 | }); 60 | it("if a single action is enabled, a single action should be taken", async () => { 61 | CONFIG.ACTION_DELETE_OBJECT = true; 62 | CONFIG.ACTION_TAG_OBJECT = false; 63 | 64 | await actions.onFindings("bucket1", "key1", result); 65 | assert.strictEqual(1, deleteCounter); 66 | assert.strictEqual(0, tagCounter); 67 | }); 68 | it("if a single action is enabled, a single action should be taken - 2", async () => { 69 | CONFIG.ACTION_DELETE_OBJECT = false; 70 | CONFIG.ACTION_TAG_OBJECT = true; 71 | 72 | await actions.onFindings("bucket1", "key1", result); 73 | assert.strictEqual(0, deleteCounter); 74 | assert.strictEqual(1, tagCounter); 75 | }); 76 | it("if 2 actions are enabled then 2 actions should be taken", async () => { 77 | CONFIG.ACTION_DELETE_OBJECT = true; 78 | CONFIG.ACTION_TAG_OBJECT = true; 79 | 80 | await actions.onFindings("bucket1", "key1", result); 81 | assert.strictEqual(1, deleteCounter); 82 | assert.strictEqual(1, tagCounter); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /tests/s3-handler.js: -------------------------------------------------------------------------------- 1 | const handler = require('../lib/s3-handler').handler; 2 | const scanii = require('../lib/client'); 3 | const assert = require('assert'); 4 | const it = require("mocha/lib/mocha.js").it; 5 | const describe = require("mocha/lib/mocha.js").describe; 6 | const beforeEach = require("mocha/lib/mocha.js").beforeEach; 7 | const afterEach = require("mocha/lib/mocha.js").afterEach; 8 | const AWS = require('aws-sdk-mock'); 9 | const nock = require('nock'); 10 | const CONFIG = require('../lib/config').CONFIG; 11 | const sinon = require('sinon'); 12 | const {defaults} = require("../lib/config"); 13 | 14 | "use strict"; 15 | 16 | describe('S3 handler tests', () => { 17 | const sandbox = sinon.createSandbox(); 18 | 19 | beforeEach(() => { 20 | sandbox.spy(scanii.ScaniiClient); 21 | 22 | CONFIG.CALLBACK_URL = "https://example.com/callback/"; 23 | CONFIG.KEY = "k"; 24 | CONFIG.SECRET = "s"; 25 | CONFIG.MAX_ATTEMPTS = 1; 26 | CONFIG.MAX_ATTEMPT_DELAY_MSEC = 1_000; 27 | CONFIG.SIGNED_URL_DURATION = 10; 28 | }); 29 | 30 | afterEach(() => { 31 | nock.cleanAll(); 32 | sandbox.restore(); 33 | defaults(); 34 | }); 35 | 36 | it('should process a create object event', async () => { 37 | nock('https://api-us1.scanii.com') 38 | .post('/v2.2/files/fetch') 39 | .reply(202, Buffer.from("{\"id\":\"12356789\"}"), {"Location": "https://api-us1.scanii.com/v2.2/files/1234"}); 40 | 41 | await handler({ 42 | "Records": [ 43 | { 44 | "eventVersion": "2.0", 45 | "eventSource": "aws:s3", 46 | "awsRegion": "us-west-2", 47 | "eventTime": "2015-10-01T23:28:54.280Z", 48 | "eventName": "ObjectCreated:Put", 49 | "userIdentity": { 50 | "principalId": "AWS:principal" 51 | }, 52 | "requestParameters": { 53 | "sourceIPAddress": "98.167.155.191" 54 | }, 55 | "responseElements": { 56 | "x-amz-request-id": "EEC943B096DE3DF9", 57 | "x-amz-id-2": "W/myEjyXFBsOA6N0byxW0tOxMA4m1fmv9KAVcovvG0nD9W1s5aX5+Wx61tlCop8LbZAw1Nz0mnc=" 58 | }, 59 | "s3": { 60 | "s3SchemaVersion": "1.0", 61 | "configurationId": "948c2c1a-a028-4564-93fc-76cea7622633", 62 | "bucket": { 63 | "name": "scanii-mu", 64 | "ownerIdentity": { 65 | "principalId": "principal" 66 | }, 67 | "arn": "arn:aws:s3:::scanii-mu" 68 | }, 69 | "object": { 70 | "key": "Screen+Shot+2016-01-19+at+7.24.37+PM.png", 71 | "size": 519, 72 | "eTag": "aa1e5c8a6a07217c25f55aa8e96ea37a", 73 | "sequencer": "00560DC1B62F962FCD" 74 | } 75 | } 76 | } 77 | ] 78 | }, {}, (error, result) => { 79 | assert(error === null, "there should be no errors"); 80 | assert(result.statusCode === 200, "should return the file id"); 81 | }); 82 | }); 83 | 84 | it('should fail to process a s3 event missing the object key', async () => { 85 | 86 | nock('https://api-us1.scanii.com') 87 | .post('/v2.2/files/fetch') 88 | .reply(202, Buffer.from("{\"id\":\"12356789\"}"), {"Location": "https://api-us1.scanii.com/v2.2/files/1234"}); 89 | 90 | await handler({ 91 | "Records": [ 92 | { 93 | "eventVersion": "2.0", 94 | "eventSource": "aws:s3", 95 | "awsRegion": "us-west-2", 96 | "eventTime": "2015-10-01T23:28:54.280Z", 97 | "eventName": "ObjectCreated:Put", 98 | "userIdentity": { 99 | "principalId": "AWS:principal" 100 | }, 101 | "requestParameters": { 102 | "sourceIPAddress": "98.167.155.191" 103 | }, 104 | "responseElements": { 105 | "x-amz-request-id": "EEC943B096DE3DF9", 106 | "x-amz-id-2": "W/myEjyXFBsOA6N0byxW0tOxMA4m1fmv9KAVcovvG0nD9W1s5aX5+Wx61tlCop8LbZAw1Nz0mnc=" 107 | }, 108 | "s3": { 109 | "s3SchemaVersion": "1.0", 110 | "configurationId": "948c2c1a-a028-4564-93fc-76cea7622633", 111 | "bucket": { 112 | "name": "scanii-mu", 113 | "ownerIdentity": { 114 | "principalId": "principal" 115 | }, 116 | "arn": "arn:aws:s3:::scanii-mu" 117 | }, 118 | "object": { 119 | "size": 519, 120 | "eTag": "aa1e5c8a6a07217c25f55aa8e96ea37a", 121 | "sequencer": "00560DC1B62F962FCD" 122 | } 123 | } 124 | } 125 | ] 126 | }, {}, (error, result) => { 127 | assert(error === null, "there should be no errors"); 128 | assert(result.statusCode === 500, "should return the file id"); 129 | assert(result.body.includes("key not present")); 130 | }); 131 | }); 132 | 133 | it('should fail to process a s3 event missing the object bucket', async () => { 134 | 135 | nock('https://api-us1.scanii.com') 136 | .post('/v2.2/files/fetch') 137 | .reply(202, Buffer.from("{\"id\":\"12356789\"}"), {"Location": "https://api-us1.scanii.com/v2.2/files/1234"}); 138 | 139 | 140 | await handler({ 141 | "Records": [ 142 | { 143 | "eventVersion": "2.0", 144 | "eventSource": "aws:s3", 145 | "awsRegion": "us-west-2", 146 | "eventTime": "2015-10-01T23:28:54.280Z", 147 | "eventName": "ObjectCreated:Put", 148 | "userIdentity": { 149 | "principalId": "AWS:principal" 150 | }, 151 | "requestParameters": { 152 | "sourceIPAddress": "98.167.155.191" 153 | }, 154 | "responseElements": { 155 | "x-amz-request-id": "EEC943B096DE3DF9", 156 | "x-amz-id-2": "W/myEjyXFBsOA6N0byxW0tOxMA4m1fmv9KAVcovvG0nD9W1s5aX5+Wx61tlCop8LbZAw1Nz0mnc=" 157 | }, 158 | "s3": { 159 | "s3SchemaVersion": "1.0", 160 | "configurationId": "948c2c1a-a028-4564-93fc-76cea7622633", 161 | "bucket": { 162 | "ownerIdentity": { 163 | "principalId": "principal" 164 | }, 165 | "arn": "arn:aws:s3:::scanii-mu" 166 | }, 167 | "object": { 168 | "size": 519, 169 | "eTag": "aa1e5c8a6a07217c25f55aa8e96ea37a", 170 | "key": "Screen+Shot+2016-01-19+at+7.24.37+PM.png", 171 | "sequencer": "00560DC1B62F962FCD" 172 | } 173 | } 174 | } 175 | ] 176 | }, {}, (error, result) => { 177 | assert(error === null, "there should be no errors"); 178 | assert(result.statusCode === 500, "should return the file id"); 179 | assert(result.body.includes("bucket not present")); 180 | }); 181 | }); 182 | it('should fail to process a directory', async () => { 183 | 184 | nock('https://api-us1.scanii.com') 185 | .post('/v2.2/files/fetch') 186 | .reply(202, Buffer.from("{\"id\":\"12356789\"}"), {"Location": "https://api-us1.scanii.com/v2.2/files/1234"}); 187 | 188 | await handler({ 189 | "Records": [ 190 | { 191 | "eventVersion": "2.0", 192 | "eventSource": "aws:s3", 193 | "awsRegion": "us-west-2", 194 | "eventTime": "2015-10-01T23:28:54.280Z", 195 | "eventName": "ObjectCreated:Put", 196 | "userIdentity": { 197 | "principalId": "AWS:principal" 198 | }, 199 | "requestParameters": { 200 | "sourceIPAddress": "98.167.155.191" 201 | }, 202 | "responseElements": { 203 | "x-amz-request-id": "EEC943B096DE3DF9", 204 | "x-amz-id-2": "W/myEjyXFBsOA6N0byxW0tOxMA4m1fmv9KAVcovvG0nD9W1s5aX5+Wx61tlCop8LbZAw1Nz0mnc=" 205 | }, 206 | "s3": { 207 | "s3SchemaVersion": "1.0", 208 | "configurationId": "948c2c1a-a028-4564-93fc-76cea7622633", 209 | "bucket": { 210 | "name": "scanii-mu", 211 | "ownerIdentity": { 212 | "principalId": "principal" 213 | }, 214 | "arn": "arn:aws:s3:::scanii-mu" 215 | }, 216 | "object": { 217 | "size": 519, 218 | "eTag": "aa1e5c8a6a07217c25f55aa8e96ea37a", 219 | "key": "Screen+Shot+2016-01-19+at+7.24.37+PM.png", 220 | "sequencer": "00560DC1B62F962FCD" 221 | } 222 | } 223 | } 224 | ] 225 | }, {}, (error, result) => { 226 | assert(error === null, "there should be no errors"); 227 | assert(result.body.includes("cannot process directory")); 228 | }); 229 | }); 230 | it('should honor configurable signed url timeout', async () => { 231 | 232 | nock('https://api-us1.scanii.com') 233 | .post('/v2.2/files/fetch') 234 | .reply(202, Buffer.from("{\"id\":\"12356789\"}"), {"Location": "https://api-us1.scanii.com/v2.2/files/1234"}); 235 | 236 | AWS.mock('S3', 'getSignedUrl', (operator,params) => { 237 | assert.ok(params.Expires === CONFIG.SIGNED_URL_DURATION); 238 | return true; 239 | }) 240 | 241 | return await handler({ 242 | "Records": [ 243 | { 244 | "eventVersion": "2.0", 245 | "eventSource": "aws:s3", 246 | "awsRegion": "us-west-2", 247 | "eventTime": "2015-10-01T23:28:54.280Z", 248 | "eventName": "ObjectCreated:Put", 249 | "userIdentity": { 250 | "principalId": "AWS:principal" 251 | }, 252 | "requestParameters": { 253 | "sourceIPAddress": "98.167.155.191" 254 | }, 255 | "responseElements": { 256 | "x-amz-request-id": "EEC943B096DE3DF9", 257 | "x-amz-id-2": "W/myEjyXFBsOA6N0byxW0tOxMA4m1fmv9KAVcovvG0nD9W1s5aX5+Wx61tlCop8LbZAw1Nz0mnc=" 258 | }, 259 | "s3": { 260 | "s3SchemaVersion": "1.0", 261 | "configurationId": "948c2c1a-a028-4564-93fc-76cea7622633", 262 | "bucket": { 263 | "name": "scanii-mu", 264 | "ownerIdentity": { 265 | "principalId": "principal" 266 | }, 267 | "arn": "arn:aws:s3:::scanii-mu" 268 | }, 269 | "object": { 270 | "key": "Screen+Shot+2016-01-19+at+7.24.37+PM.png", 271 | "size": 519, 272 | "eTag": "aa1e5c8a6a07217c25f55aa8e96ea37a", 273 | "sequencer": "00560DC1B62F962FCD" 274 | } 275 | } 276 | } 277 | ] 278 | 279 | }, {}, (error, result) => { 280 | assert.ok(error === null, "there should be no errors"); 281 | assert.ok(result.statusCode === 200, "signed url timeout not configurable"); 282 | 283 | }); 284 | }); 285 | }) 286 | 287 | -------------------------------------------------------------------------------- /tests/utils.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const it = require("mocha/lib/mocha.js").it; 3 | const describe = require("mocha/lib/mocha.js").describe; 4 | const beforeEach = require("mocha/lib/mocha.js").beforeEach; 5 | const utils = require('../lib/utils'); 6 | const {CONFIG} = require("../lib/config"); 7 | 8 | describe('Util tests', () => { 9 | 10 | beforeEach(() => { 11 | }); 12 | 13 | it('should throw error if config secret is not set', async () => { 14 | CONFIG.SECRET = false; 15 | const result = { 16 | "id": "2e4612793298b1d691202e75dc125f6e", 17 | "checksum": "30d3007d8fa7e76f2741805fbaf1c8bba9a00051", 18 | "content_length": "1251174", 19 | "findings": [], 20 | "creation_date": "2016-01-24T15:05:53.260Z", 21 | "content_type": "image/jpeg", 22 | "metadata": { 23 | "signature": "abc", 24 | "bucket": "test-bucket", 25 | "key": "test-key" 26 | } 27 | }; 28 | try { 29 | const signature = utils.generateSignature(result.metadata.bucket, result.metadata.key); 30 | assert.ok(result.metadata.signature === signature); 31 | } catch (error) { 32 | assert(error.code === 'ERR_ASSERTION') 33 | } 34 | }); 35 | 36 | 37 | it('should format tag value #1', async () => { 38 | const value = utils.formatTagValue(['content.malicious.porcupine-malware-36555-unofficial', 'content.malicious.trojan-agent-bwqq' 39 | ]); 40 | 41 | assert.deepStrictEqual(value, "content.malicious.porcupine-malware-36555-unofficial content.malicious.trojan-agent-bwqq"); 42 | }); 43 | }); 44 | --------------------------------------------------------------------------------